Chrome CDP Setup (macOS Dock launcher)
Makes the Dock's Chrome icon launch Chrome with --remote-debugging-port so any local
agent can connect over CDP. Chrome 136+ ignores that flag on the default profile
directory, and the Chrome 144+ chrome://inspect toggle that does work there makes a
human approve every connection — so the launcher points Chrome at a one-time APFS clone
of the real profile. All logins, bookmarks, and extensions carry over at near-zero disk
cost, the flags take precedence over the toggle, and nothing ever prompts.
Scripts live in this skill's scripts/ dir; run them with absolute paths
(~/.claude/skills/chrome-cdp-setup/scripts/...).
Which route
| Need |
Route |
| Unattended agent loop, headless, CI, several tools sharing one browser |
This skill: flags + a separate --user-data-dir, launched from the Dock wrapper. No prompts. |
| Real default profile, human at the keyboard, occasional |
chrome://inspect/#remote-debugging toggle, then PLAYWRIGHT_MCP_CDP_TIMEOUT=0 playwright-cli attach --cdp=chrome; the human clicks Allow once per connection. See REFERENCE.md "Approval mode". |
| Real default profile, one tab, no Chrome prompt |
playwright-cli attach --extension=chrome (Playwright extension). Per-tab access; its own approval popup takes a shared token via PLAYWRIGHT_MCP_EXTENSION_TOKEN. |
| Real default profile, whole browser, no prompt |
Not possible in Chrome 136+. The profile clone is the closest thing. |
Quick start
scripts/setup_chrome_cdp.sh # full setup (default port 9222)
scripts/verify_cdp.sh # signals + one-line verdict naming the state and the fix
uv run --with playwright python scripts/cdp_agent_test.py # external-agent smoke test
Setup workflow
- Preflight: confirm
/Applications/Google Chrome.app exists and /Applications is
writable; note profile size with du -sh ~/Library/Application\ Support/Google/Chrome.
- Run
scripts/setup_chrome_cdp.sh [port]. It is idempotent and:
- quits Chrome cleanly (session restores on relaunch) and stops if anything still
holds the port — flag mode binds it with no fallback
- clones
~/Library/Application Support/Google/Chrome → Chrome-CDP (cp -Rc;
skipped if the clone already exists)
- builds
/Applications/Google Chrome CDP.app (Chrome's own icon, ad-hoc signed,
arch -arm64 on Apple Silicon)
- swaps the Dock's Chrome tile for the wrapper (Dock prefs backed up first) and
restarts the Dock
- launches the wrapper, waits for
http://127.0.0.1:<port>/json/version, and on
Apple Silicon checks that Chrome came up native rather than under Rosetta
- Confirm with
scripts/verify_cdp.sh [port] — the last line must read OK. Any other
verdict (APPROVAL MODE, PORT STOLEN, PORT TAKEN, NOT LISTENING, DOWN,
UNHEALTHY) names the fix, and the exit status is 1.
- Prove end-to-end with
cdp_agent_test.py — it connects from an independent process,
lists the real tabs, opens its own tab, rewrites the DOM, screenshots, and
disconnects leaving Chrome running.
Connecting agents
- Playwright:
chromium.connect_over_cdp("http://127.0.0.1:9222")
- Puppeteer:
puppeteer.connect({ browserURL: "http://127.0.0.1:9222" })
- playwright-cli:
playwright-cli attach --cdp=http://127.0.0.1:9222. Not --cdp=chrome:
that reads the default profile's DevToolsActivePort and cannot reach the wrapper.
- Raw WS: re-fetch
webSocketDebuggerUrl from /json/version each launch (it rotates).
- Python agents need only
pip install playwright or uv run --with playwright — no
playwright install browser download, since they attach to the running Chrome.
- A first attach to a browser that has been up for hours with dozens of tabs can exceed
Playwright's 30 s connect timeout: it attaches to every target, and idle renderers
answer slowly. Raise the timeout (
PLAYWRIGHT_MCP_CDP_TIMEOUT for playwright-cli) or
retry once; the retry is fast.
Rules that prevent breakage
- Always start Chrome from the Dock icon. Spotlight, Chrome's "Relaunch to update", or
a link click while Chrome is closed all launch the non-CDP default-profile instance —
quit it and relaunch from the Dock. With the
chrome://inspect toggle on, that
instance also grabs port 9222 in approval mode: /json/version answers 404 and every
connection prompts. verify_cdp.sh names this state.
- Never run both instances at once (session divergence, duplicate extension connections).
- The CDP server takes a few seconds after launch: poll with
curl --retry 30 --retry-delay 1 --retry-all-errors.
- The active profile may be
Profile N, not Default — read profile.last_used from
the clone's Local State before asserting the clone is broken.
- Loopback only: never add
--remote-debugging-address. Any local process can drive
the browser and read that profile's cookies — the machine is the trust boundary.
- On Apple Silicon, confirm the running Chrome shows
Arch=ARM64
(lsappinfo info -app com.google.Chrome | grep -io 'Arch=[a-z0-9_]*'; older macOS
prints it lowercase). A stale LaunchServices record for the wrapper can silently run
the whole browser under Rosetta at several times the CPU cost — see "Rosetta trap"
in REFERENCE.md.
Details
Gotchas, approval mode, the Rosetta trap, raw-CDP notes, and revert steps: see
REFERENCE.md.
1---2name: chrome-cdp-setup3description: Set up Google Chrome on macOS to always launch with the Chrome DevTools Protocol enabled (127.0.0.1:9222) via a Dock launcher app, so local AI agents (Playwright, Puppeteer, browser-use, OpenClaw) can drive the real logged-in browser with no per-connection approval prompt. Use when the user wants to enable a CDP/remote-debugging port for Chrome, pick between the flag, chrome://inspect, and extension routes, connect an agent to Chrome, test a CDP connection, diagnose a 404 from /json/version or an "Allow remote debugging?" dialog, or troubleshoot/revert an existing Chrome-CDP Dock setup.4---56# Chrome CDP Setup (macOS Dock launcher)78Makes the Dock's Chrome icon launch Chrome with `--remote-debugging-port` so any local9agent can connect over CDP. Chrome 136+ ignores that flag on the default profile10directory, and the Chrome 144+ `chrome://inspect` toggle that does work there makes a11human approve every connection — so the launcher points Chrome at a one-time APFS clone12of the real profile. All logins, bookmarks, and extensions carry over at near-zero disk13cost, the flags take precedence over the toggle, and nothing ever prompts.1415Scripts live in this skill's `scripts/` dir; run them with absolute paths16(`~/.claude/skills/chrome-cdp-setup/scripts/...`).1718## Which route1920| Need | Route |21|---|---|22| Unattended agent loop, headless, CI, several tools sharing one browser | **This skill**: flags + a separate `--user-data-dir`, launched from the Dock wrapper. No prompts. |23| Real default profile, human at the keyboard, occasional | `chrome://inspect/#remote-debugging` toggle, then `PLAYWRIGHT_MCP_CDP_TIMEOUT=0 playwright-cli attach --cdp=chrome`; the human clicks Allow once per connection. See REFERENCE.md "Approval mode". |24| Real default profile, one tab, no Chrome prompt | `playwright-cli attach --extension=chrome` (Playwright extension). Per-tab access; its own approval popup takes a shared token via `PLAYWRIGHT_MCP_EXTENSION_TOKEN`. |25| Real default profile, whole browser, no prompt | Not possible in Chrome 136+. The profile clone is the closest thing. |2627## Quick start2829```bash30scripts/setup_chrome_cdp.sh # full setup (default port 9222)31scripts/verify_cdp.sh # signals + one-line verdict naming the state and the fix32uv run --with playwright python scripts/cdp_agent_test.py # external-agent smoke test33```3435## Setup workflow36371. Preflight: confirm `/Applications/Google Chrome.app` exists and `/Applications` is38 writable; note profile size with `du -sh ~/Library/Application\ Support/Google/Chrome`.392. Run `scripts/setup_chrome_cdp.sh [port]`. It is idempotent and:40 - quits Chrome cleanly (session restores on relaunch) and stops if anything still41 holds the port — flag mode binds it with no fallback42 - clones `~/Library/Application Support/Google/Chrome` → `Chrome-CDP` (`cp -Rc`;43 skipped if the clone already exists)44 - builds `/Applications/Google Chrome CDP.app` (Chrome's own icon, ad-hoc signed,45 `arch -arm64` on Apple Silicon)46 - swaps the Dock's Chrome tile for the wrapper (Dock prefs backed up first) and47 restarts the Dock48 - launches the wrapper, waits for `http://127.0.0.1:<port>/json/version`, and on49 Apple Silicon checks that Chrome came up native rather than under Rosetta503. Confirm with `scripts/verify_cdp.sh [port]` — the last line must read `OK`. Any other51 verdict (`APPROVAL MODE`, `PORT STOLEN`, `PORT TAKEN`, `NOT LISTENING`, `DOWN`,52 `UNHEALTHY`) names the fix, and the exit status is 1.534. Prove end-to-end with `cdp_agent_test.py` — it connects from an independent process,54 lists the real tabs, opens its own tab, rewrites the DOM, screenshots, and55 disconnects leaving Chrome running.5657## Connecting agents5859- Playwright: `chromium.connect_over_cdp("http://127.0.0.1:9222")`60- Puppeteer: `puppeteer.connect({ browserURL: "http://127.0.0.1:9222" })`61- playwright-cli: `playwright-cli attach --cdp=http://127.0.0.1:9222`. Not `--cdp=chrome`:62 that reads the *default* profile's `DevToolsActivePort` and cannot reach the wrapper.63- Raw WS: re-fetch `webSocketDebuggerUrl` from `/json/version` each launch (it rotates).64- Python agents need only `pip install playwright` or `uv run --with playwright` — no65 `playwright install` browser download, since they attach to the running Chrome.66- A first attach to a browser that has been up for hours with dozens of tabs can exceed67 Playwright's 30 s connect timeout: it attaches to every target, and idle renderers68 answer slowly. Raise the timeout (`PLAYWRIGHT_MCP_CDP_TIMEOUT` for playwright-cli) or69 retry once; the retry is fast.7071## Rules that prevent breakage7273- Always start Chrome from the Dock icon. Spotlight, Chrome's "Relaunch to update", or74 a link click while Chrome is closed all launch the non-CDP default-profile instance —75 quit it and relaunch from the Dock. With the `chrome://inspect` toggle on, that76 instance also grabs port 9222 in approval mode: `/json/version` answers 404 and every77 connection prompts. `verify_cdp.sh` names this state.78- Never run both instances at once (session divergence, duplicate extension connections).79- The CDP server takes a few seconds after launch: poll with80 `curl --retry 30 --retry-delay 1 --retry-all-errors`.81- The active profile may be `Profile N`, not `Default` — read `profile.last_used` from82 the clone's `Local State` before asserting the clone is broken.83- Loopback only: never add `--remote-debugging-address`. Any local process can drive84 the browser and read that profile's cookies — the machine is the trust boundary.85- On Apple Silicon, confirm the running Chrome shows `Arch=ARM64`86 (`lsappinfo info -app com.google.Chrome | grep -io 'Arch=[a-z0-9_]*'`; older macOS87 prints it lowercase). A stale LaunchServices record for the wrapper can silently run88 the whole browser under Rosetta at several times the CPU cost — see "Rosetta trap"89 in REFERENCE.md.9091## Details9293Gotchas, approval mode, the Rosetta trap, raw-CDP notes, and revert steps: see94[REFERENCE.md](REFERENCE.md).