MCP.Market

browser skill

by code-yeongyu·code-yeongyu/oh-my-openagent·71k stars

Drives a real browser through the omowright library from the js eval kernel: sites the user is already signed into, forms and clicks, JS-rendered pages, screenshots, web QA, extension popups, a human handoff for login, CAPTCHA or OTP, and a browser you own for scraping, bot-scored targets, network capture and QA traces. Use for any interactive browser task; not for a plain search or an unblocked static fetch.

A100/100content scan

Is the browser skill safe?

Clean: nothing in its files matched our rules. We read 15 files in the folder on 2026-11-14.

No findings.

Install the browser skill

A skill is a folder. Copy it into your agent's skills folder and the agent loads it when the task matches its description.

git clone --depth 1 https://github.com/code-yeongyu/oh-my-openagent.git /tmp/oh-my-openagent
mkdir -p ~/.claude/skills
cp -r /tmp/oh-my-openagent/packages/shared-skills/skills/browser ~/.claude/skills/browser
available in every project

In the Claude apps, zip the folder and upload it from the Skills settings. The folder on GitHub

The instructions your agent would load

SKILL.md as published, without the frontmatter. Read it on GitHub

Browser

One library, two engines. omowright ships inside this skill; choose the engine before you act:

Attached is the default, because it is the only engine carrying the user's logins and the only one where a human is a single call away. It works in an Agent Window inside the user's own browser profile, never in a copy of that profile, and exists only where the user installed and enabled the BrowserSkill extension. Never substitute one engine for the other silently: if the attached engine is not set up, run the onboarding script and tell the user its one remaining step.

Step 0 — inside OmO Desktop: everything above, plus the app's built-in browser

When OMOBROWSERENGINE is set (the OmO desktop app sets it for every session), everything above works exactly as in the terminal: connectBrowserSkill() for the user's own browser, connectPipe() / connectCloakProfile() / connect() for a browser of your own, with the same table to choose between them. The app adds one more option:

const browser = await omowright.connectBuiltinBrowser()
const [tab] = await browser.listTargets()
const page = await browser.attachPage(tab.id)
await page.goto(url)

It is an OmOWright connection like connect(). Work in few calls: take page.snapshot() once, act on its refs, and chain several actions in one eval cell (page.locator(ref).fill(), .click(), .selectOption(value), .setInputFiles(path)), snapshotting again only after the page changed. It has one tab: navigate it rather than opening new ones. BuiltinBrowserUnavailableError permits another engine only for unavailable, timeout, or invalidgrant. For userstopped or any other app refusal, stop browser work and report it; do not open another browser.

Step 1 — load omowright and prove the stack

const { loadOmowright } = await import("<skill-root>/scripts/omowright.mjs")
const { omowright } = await loadOmowright()          // { connectBrowserSkill, bskSnapshot, connectPipe, ... }
node "<skill-root>/scripts/browser-doctor.mjs" --json

Install into the browser the user actually uses, never into whatever happens to be on disk. Before installing, check your memory for the user's browser; otherwise read the doctor's browser (picked from the OS default browser, running apps and recent use — candidates shows the evidence). If memory and the doctor disagree, or the doctor says choose-browser, ask the user. Pass the answer as --browser= and record it in memory. A Chrome that is merely installed is not their browser.

Never launch a headless browser because the attached one is missing. It has none of the user's sessions, so every login turns into a ladder you should not be climbing. Say which state you hit and ask.

The loop (attached)

const session = await omowright.connectBrowserSkill({ name: "<task>", focused: false })
const sync = async () => (await session.cooperate("sync")).context_id                      // one context per page action
try {
  await session.navigate("https://example.com/", { waitUntil: "load", context: await sync() })
  const { tree, refs, css } = await omowright.bskSnapshot(session, { interactive: true, context: await sync() })  // OmOWright tree + refs, no trace in the page
  await session.click({ selector: css.e3 }, { context: await sync() })                      // css[ref] is null inside shadow roots:
  const vom = await session.observe({ maxTokens: 4000 })                                    //   then read the daemon's own tree ...
  await session.click("@e7", { context: await sync() })                                     //   ... and click its @eN ref
  await session.fill(css.e5, "hello", { context: await sync() })
  await session.press("Enter", { context: await sync() })
  await session.waitForNavigation({ waitUntil: "load" })
  const shot = await session.screenshot()                                                   // { buffer, width, height, captureId }
} finally {
  await session

call; use a ref in the same cycle you read it.

  1. Read before every action. bskSnapshot refs and observe @eN refs are reissued on each

Borrowing prompts the user; never invent tab ids and never repeat a denied borrow.

  1. Navigation and large DOM changes stale every ref. Read again rather than reusing.
  2. Two identical failures mean change approach, not retry. A third identical attempt is a defect.
  3. Borrow a user tab explicitly (tabList({ scope: "user" }), tabBorrow(id), tabReturn(id)).

BrowserSkill 0.4 refuses an action (permissiondenied, data.reason: "cooperationsync_required") unless it carries { context } from the latest session.cooperate("sync"). A context is good for one action: sync, act with { context }, sync again. Navigation, every input method, evaluate (and so bskSnapshot), tabClose, resize, emulate, observe({ probeHover }) and screenshot({ fullPage: true }) need it; plain reads (observe, snapshot, getHtml, screenshot(), console, network, waitForNavigation) do not.

  1. Always stop() the session, on success and on failure.
  2. Sync before every page action. The person at the keyboard can work in the same tab, so

session.cooperate("wait", { timeoutMs: 25_000 }) until it reports ready, read the page again and decide afresh; never replay the refused action blindly.

  1. On cooperationsyncrequired, wait for the person, then replan. Call

(a misspelt contextId would otherwise send no context). bskSnapshot and screenshot({ fullPage: true }) do not throw: they drop the stray key, and the daemon then refuses the call with cooperationsyncrequired.

  1. Pass only the options a method takes. A gated action throws a TypeError on any other key

Every method, its options, and the failure codes are in references/commands.md.

When a human is the only way through

Login, CAPTCHA, OTP, a payment confirmation, a consent dialog:

const outcome = await session.requestHelp({ prompt: "<what you need done>", targets: ["@e4"], timeoutMs: 300_000 })

Then read the page again. Respect a cancelled or timed_out outcome; do not work around it by changing the extension's automation settings.

Rules

cookie or recovery code. The value of the attached engine is that the browser is already signed in.

  • Never read credentials through the page. No evaluate that extracts a password, token,

out everywhere. No flow here needs it.

  • Never clear cookies, cache or site data. It is the user's real profile; clearing it logs them

capture on every tab it drives, which is a known automation signal.

  • focused: false by default. The browser belongs to someone who is probably using it.
  • One short, named session per task, always stopped.
  • Bot-scored or WAF targets never go through the attached engine — route them to the ultimate-browsing skill, whose browser tier is the owned engine (connectCloakProfile()). The attached engine's daemon enables console

Where the rest lives

More skills from code-yeongyu/oh-my-openagent

  • Aast-grepSearches and rewrites code by AST shape across 25 languages. Use when the target is a syntax pattern (every call/class/import shaped like X, a codemod, a YAML rule) rather than literal text; for plain strings, comments, or filenames, use rg.
  • Acodex-qaQA the omo Codex Light edition (lazycodex / packages/omo-codex) itself, in strict isolation so ONLY our plugin is exercised, never the user's real ~/.codex. The first-party method drives the real `codex app-server` against an isolated CODEX_HOME plus a LOCAL mock model (no real API call), and proves a plugin hook fired by asserting hook/started + hook/completed notifications. Also: isolated install verification, per-component hook probes, a tmux TUI smoke, and runtime log observation (RUST_LOG / logs SQLite / /debug-config). Ships tested helper scripts each with a --self-test. Use whenever someone changes anything under packages/omo-codex or wants to QA, smoke-test, verify, or debug the Codex plugin, its hooks/components, the installer/config.toml, the app-server flow, or the Codex TUI. Triggers: codex qa, qa codex, codex-qa, test codex plugin, verify codex hook, codex app-server, lazycodex qa, isolated CODEX_HOME, prove codex hook fired, codex tui test.
  • Acoding-agent-sessionsFinds, reads, and reconstructs coding-agent sessions across Codex, Claude, OpenCode, OMO/Senpi, and other local agent logs. Use when asked to find or search past sessions, transcripts, or subagent runs, or to recover what an earlier session did.
  • Acomment-checkerUse when Codex needs to understand or respond to automatic comment-checker feedback emitted after an edit-like PostToolUse hook.
  • Adag-libraryStores a DAG definition once and re-runs it by name, instead of pasting the definition into every run. Use when the user wants to save a DAG, run a saved one, or schedule the same multi-agent graph repeatedly.
  • Ddata-scientistProcesses and analyzes data with resident-kernel engines (DuckDB, Polars) and one-shot tools. Use for CSV/parquet/JSON analysis, group-by/join/aggregation, time series, distributions, cleaning, or plotting a dataset.
  • AdebuggingRuns a hypothesis-driven debugging loop across any language or binary, escalating to orthogonal oracle angles and locking the fix with a failing test. Use for crashes, silent failures, hangs, wrong responses, memory leaks, async misbehavior, or reverse engineering.
  • Adev-browserBrowser automation with persistent page state. Use when users ask to navigate websites, fill forms, take screenshots, extract web data, test web apps, or automate browser workflows. Trigger phrases include "go to [url]", "click on", "fill out the form", "take a screenshot", "scrape", "automate", "test the website", "log into", or any browser interaction request.
  • Aeval-integrityAssess integrity of an eval, benchmark, metric, A/B test, holdout, LLM-judge, or user-study claim when the question is whether measurement, comparison, or evidence is independent and trustworthy.
  • AfrontendBuilds, styles, and polishes anything drawn with HTML, CSS, or SVG: web UI, pages, components, HTML reports rendered to PDF or print, figures, PNG/SVG diagrams, slides, and images for Notion or Slack. Use for any styling, layout, animation, or visual-quality task, or when asked to make something look or feel a certain way.
  • AfrontendBuilds, styles, and polishes anything drawn with HTML, CSS, or SVG: web UI, pages, components, HTML reports rendered to PDF or print, figures, PNG/SVG diagrams, slides, and images for Notion or Slack. Use for any styling, layout, animation, or visual-quality task, or when asked to make something look or feel a certain way.
  • Aget-unpublished-changesCompare HEAD with the latest published npm versions and list all unpublished changes by release layer. Triggers: unpublished changes, changelog, what changed, whats new.

All agent skills → · MCP servers