SKILL.md
browsing-as-you
Run ONE long-lived, logged-in Chrome with remote debugging on; every agent and tool attaches to it. No per-task browser launches, no re-login, no macOS "Allow" prompts. Tabs and the window open in the background (open -g), never stealing focus.
Golden rule: never launch your own Chrome — attach to the shared instance at http://127.0.0.1:9222. Deep detail lives in reference/integration.md (setup, concurrency, triage) and reference/launchd.md (autostart).
1. Start (idempotent, concurrent-safe)
scripts/chrome-agent.sh start # prints http://127.0.0.1:9222 when live
Triage: health → ok|down|wedged (exit 0|3|4) · doctor for diagnostics · recover a wedged one · stop/kill touch only the bridge profile (your real Chrome is safe). Full table + runbook: reference/integration.md.
Agent: bring it up yourself — don't bail, don't ask. If you land mid-task
andhealthisdown/wedged, just runstart(idempotent, concurrent-safe)
and, for a cookie site,seed(prompt-free — see §2); then carry on and
actually drive the browser. Reporting "the bridge was down" as a blocker, or
asking the user to start it, defeats the whole point of the skill. The user
should not be pinged for perms. The only human step islogin(typing a
password), and only for the localStorage/SSO sites flagged in §2 — never for
plain cookie sites like GitHub.
2. Authenticate (the bridge starts logged out)
It uses a dedicated profile — it must, since Chrome 136 ignores the debug port on your default profile. Give it your sessions one of two ways:
scripts/cdp.py seed # import logins from your Default Chrome
scripts/cdp.py seed --filter github.com # (cookies only; also --profile/--dry-run)
scripts/chrome-agent.sh login # OR sign in once in the bridge (stop first)
| Site type | via seed |
durable answer |
|---|---|---|
| Normal cookie sites (GitHub, most SaaS, X) | ✅ survives restart | seed once |
| DBSC (Google, YouTube) | live only — drops on restart | login once |
| localStorage / SSO SPAs (Cloudflare, Devin) | ❌ not covered — looks authed, renders sign-in | login once |
localStorage/SSO dashboards: prefer
login, notseed. Cloudflare and
similar SPAs keep part of their auth in localStorage or a device-bound SSO key
that cookie-onlyseedcan't carry. The catch:seedreports success, cookies
are injected, andlocation.hrefstill reads as authed — but the SPA quietly
renders its sign-in screen (no redirect, so a URL check is fooled). Don't
trust a URL heuristic; confirm with a DOM probe:cdp.py probe https://dash.cloudflare.com --front→ `login-wall |
likely-authed. (--front` matters: CF's dash is visibility-gated and renders
blank in a hidden tab, which reads asunknown.) Alogin-wallafterseed
means: runchrome-agent.sh loginfor that site.
seed is cookies-only and reads your real profile locally (nothing leaves the machine, nothing is written back). login creates the device key inside the bridge profile, so Gmail, Docs/Drive, X, and Cloudflare stay authed across restarts and reboots — verified. Adding a new site later: seed --filter <host> if you're already signed in there, else one login. At most one login per site, then it persists; an agent can't type your password — that's the only human step.
3. Drive it
Preferred: chrome-devtools-mcp pointed at the browser (--browser-url http://127.0.0.1:9222; setup in reference/integration.md). Or scripts/cdp.py:
| Need | Command | ||
|---|---|---|---|
| open background tab, wait for load | cdp.py open <url> -w → prints targetId |
||
| open + foreground a visibility-gated SPA | cdp.py open <url> --front -w |
||
| eval JS · screenshot · navigate | cdp.py eval '<js>' -t <id> · shot out.png -t <id> · nav <url> -t <id> |
||
| trusted click (coords or selector) | cdp.py click '#submit' -t <id> · click 240,180 -t <id> |
||
| trusted type (into focused / a selector) | cdp.py type 'hello' -t <id> · type 'us-east' --into '[role=combobox]' --enter -t <id> |
||
| trusted key (Enter/Tab/Arrow…) | cdp.py key ArrowDown -t <id> · key Enter -t <id> |
||
| auth probe (is it really logged in?) | cdp.py probe <url> --front → `login-wall \ |
likely-authed \ | unknown` |
| upload files to a GitHub issue/PR (no submit click) | cdp.py --json gh-attach --repo o/r --pr 19 -f a.png -f b.log → asset URLs |
||
| foreground · close · sweep blanks | cdp.py front -t <id> · close <id> · gc |
||
| isolated context | cdp.py ctx new → open <url> --context <id> → ctx close <id> |
Trusted verbs (click/type/key) use the CDP Input domain — real isTrusted events — so they drive react-select dropdowns, role=combobox widgets, and native checkboxes that ignore synthetic eval-dispatched events. click/type --into take a CSS selector (scrolled into view) or x,y coords.
--json (before the subcommand) for machine-readable output. Concurrency: each agent owns its targetId and passes -t; set CHROMEAGENTSTRICT=1 to make eval/nav refuse an implicit tab. Full rules: reference/integration.md.
GitHub attachments — safe two-phase upload
To attach local files (screenshots, logs, any file type) to a GitHub issue or PR without committing binaries to the repo, never drive the page's submit button: GitHub puts Close / Merge / Delete / Reopen / Submit-review controls right next to the comment box, and a broad "first enabled submit" selector can click a destructive one after the upload. Split it in two:
Phase 1 — upload only (browser), prints the user-attachments URLs:
cdp.py --json gh-attach --repo OWNER/REPO --pr 19 -f ./shot-1.png -f ./trace.log
# --issue N for issues · --url <full link> · -t <tab already on the page>
gh-attach sets the files on GitHub's hidden file input over CDP and reads the URL(s) back from the composer — it issues zero clicks, so destructive controls are unreachable by construction. It fails closed: if the comment box isn't found (logged out / no permission) or a URL never appears, it restores the composer, leaves the page untouched, and exits non-zero. It uploads any file type (assets/<uuid> for images, files/<n>/<name> otherwise); it never posts.
Phase 2 — post the comment yourself via the API (no page button). gh-attach prints the ready line; it uses the issues endpoint (PR comments included):
gh api repos/OWNER/REPO/issues/19/comments -f body="$(...uploaded markdown...)"
Posting through the browser is intentionally unsupported —
--allow-submitis
refused. Treat PR Close/Merge/Reopen/Delete as out of scope unless the user
explicitly asks. Self-test the helper logic withmake test-browsing.
Guardrails
- Be a good citizen. Human pace, not machine-gun. A CAPTCHA / "unusual
traffic" / interstitial is a STOP sign, not a retry trigger — back off and tell the user. Prefer official APIs for bulk work. A blank/stuck SPA is usually just a hidden background tab → --front, not bot detection.
- Trusted input is not for CAPTCHA.
click/type/keyare for app forms,
not human-verification. click refuses any target inside a reCAPTCHA / hCaptcha / Cloudflare Turnstile widget (exit 2) — solving a challenge stays a human step. Don't try to coordinate-click around the guard.
- Dedicated profile, loopback only.
seedreads your real profile but never
writes to it; the debug port binds to 127.0.0.1 — never expose it. stop/ kill/recover only ever touch the bridge profile.
- Real keychain by default so
loginis durable (one "Always Allow" at
most). CHROMEAGENTMOCK_KEYCHAIN=1 gives a prompt-free, seed-only browser.
- Clean up tabs you open (
gc/close).
Setup & autostart (human, once)
seed or login to authenticate, wire up chrome-devtools-mcp (reference/integration.md), and for hands-free use scripts/chrome-agent.sh autostart on — a self-pathing login LaunchAgent (Linux: systemd, see reference/launchd.md). Needs uv; chrome-devtools-mcp needs Node 22+; seed is macOS-only.