Skip to content
Code Review
Skill

/chrome-cdp

Drive a headless Chrome over the Chrome DevTools Protocol (CDP) for browser QA — navigate, click, fill forms, read the DOM/accessibility tree, screenshot, and assert. Use whenever a task requires loading a web page and interacting with it like a user. Chrome is launched by a

BOOST
From plugin
shippie
2.5k1 skill
Install
$ npx -y skills add mattzcarey/shippie --skill chrome-cdp --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/chrome-cdp

Context preview

The summary Claude sees to decide when to auto-load this skill.

Drive a headless Chrome over the Chrome DevTools Protocol (CDP) for browser QA — navigate, click, fill forms, read the DOM/accessibility tree, screenshot, and assert. Use whenever a task requires loading a web page and interacting with it like a user. Chrome is launched by a

SKILL.md

chrome-cdp.SKILL.md
name: chrome-cdp
description: >-
  Drive a headless Chrome over the Chrome DevTools Protocol (CDP) for browser QA —
  navigate, click, fill forms, read the DOM/accessibility tree, screenshot, and assert.
  Use whenever a task requires loading a web page and interacting with it like a user.
  Chrome is launched by a bash step (recipe below); this skill attaches over CDP — no
  MCP server, no Puppeteer/Playwright needed to drive.

Driving Chrome over CDP (no MCP, no Playwright-for-driving)

`scripts/cdp.mjs` is a dependency-free CLI (Node 22+ built-in WebSocket). You launch headless Chrome from a `bash` step, then attach to it on a port. Every command is:

node .agents/skills/chrome-cdp/scripts/cdp.mjs --port $PORT <command> <target> [args]

where `<target>` is a **targetId prefix** from `cdp list` (copy the prefix shown, e.g. `18EB6379`).

1. Launch ONCE per flow — survives across bash calls

Each `bash` tool call is a fresh process group, so a launched browser is only reachable later if it is **detached**. The helper handles that portably (setsid on Linux, nohup+disown on macOS), uses `$CHROME_BIN`, bakes in `--no-sandbox --disable-dev-shm-usage` (mandatory as root / in a container), and blocks until the CDP endpoint is ready. Use a unique port per flow (parallel-safe):

PORT=$(( 9222 + ${FLOW_INDEX:-0} ))
bash .agents/skills/chrome-cdp/scripts/launch-chrome.sh "$PORT"

2. Drive it. `--port $PORT` re-discovers the ws endpoint via /json/version each call —

no shell variable survives between bash calls, only the port does.

CDP="node .agents/skills/chrome-cdp/scripts/cdp.mjs --port $PORT"
T=$($CDP list | head -1 | awk '{print $1}')        # the open page's targetId prefix
$CDP nav  "$T" "$BASE_URL/login"
$CDP snap "$T"                                       # accessibility tree → derive getByRole locators
$CDP fill "$T" "input[name=email]" "qa@example.com" # focus + select + insertText (React/Vue-safe)
$CDP fill "$T" "input[name=password]" "hunter2"
$CDP click "$T" "button[type=submit]"               # JS el.click() via eval — covers standard buttons/links
# $CDP clickxy "$T" 412 388                          # real Input.dispatchMouseEvent at CSS px, for
#                                                     #   real-input-only handlers (see `shot` DPR note)
$CDP eval "$T" "location.pathname"                  # assert post-conditions
$CDP shot "$T" /tmp/f$PORT-01.png                   # then `read` the PNG (vision) to assert visually
$CDP evalraw "$T" "DOM.getDocument" '{}'             # raw-CDP escape hatch (method + JSON params)

Notes:

  • **`fill <target> <selector> <text>`** is the reliable way to set a form field — it focuses the

element, selects existing content, then `Input.insertText` so frameworks see real input events.

  • Raw **`type <target> <text>`** has **no selector**; it inserts at whatever currently has focus.
  • **`click`** is `el.click()` via eval — won't fire real-input-only handlers (drag, native pickers);

use **`clickxy`** with CSS pixels for those.

  • Prefer **`snap`** (accessibility tree) to find stable, semantic elements (by role / name / label),

then translate them to robust **CSS selectors** (e.g. `input[name=email]`, `button[type=submit]`, `[aria-label="Email"]`) for the committed CDP test — far more resilient than coordinates.

2a. Record a flow → generate a test → add assertions → run (preferred authoring loop)

Don't hand-write the replay from memory. Set `$CDP_RECORD` to a scratch JSONL path and drive the WHOLE flow once: `cdp.mjs` appends one JSON op per **successful** `nav`/`fill`/`click`/ `type`/`clickxy` — so the log holds selectors that actually worked. Then `gen-test.mjs` turns that log into a faithful `e2e/tests/<slug>.cdp.mjs` (imports `../cdp-client.mjs`, replays your captured actions, leaves a `// TODO: add assertions` marker + safe placeholder asserts).

`$CDP_RECORD` is read fresh on each `cdp.mjs` call, but **`export` does NOT survive between separate bash tool calls** (each is a fresh process group). Drive the whole flow in ONE bash call (recommended), or inline `CDP_RECORD=/tmp/$SLUG.jsonl` on every command.

SLUG=login
export CDP_RECORD=/tmp/$SLUG.jsonl
rm -f "$CDP_RECORD"                                   # don't append onto a stale log
CDP="node .agents/skills/chrome-cdp/scripts/cdp.mjs --port $PORT"
T=$($CDP list | head -1 | awk '{print $1}')
$CDP nav  "$T" "$BASE_URL/login"
$CDP fill "$T" "input[name=email]" "qa@example.com"  # use `snap` to pick resilient selectors
$CDP fill "$T" "input[name=password]" "hunter2"
$CDP click "$T" "button[type=submit]"
# only SUCCESSFUL actions are logged — a failed selector leaves no line in the log

# generate the test skeleton FROM the recording (not from memory):
node .agents/skills/chrome-cdp/scripts/gen-test.mjs \
  --from "$CDP_RECORD" --out e2e/tests/$SLUG.cdp.mjs --name "$SLUG" --base "$BASE_URL"

Then OPEN `e2e/tests/$SLUG.cdp.mjs`, replace the `// TODO: add assertions` block with the flow's real user-visible guarantee (`waitForText` / `assert.match` on `b.text(...)`, etc.) — keep the replayed actions (verified selectors). VERIFY with `run_spec`; fix the test (asserts/ waits, or re-record a bad selector) until it exits 0. Teardown: `unset CDP_RECORD` (or delete `/tmp/$SLUG.jsonl`) and stop Chrome at flow end.

Concurrent drivers on different ports MUST use different `$CDP_RECORD` paths (e.g. `/tmp/$SLUG.jsonl`, unique per flow) or their logs interleave.

3. Remote browser override seam (do NOT use in v0)

node .agents/skills/chrome-cdp/scripts/cdp.mjs \
  --ws-endpoint "$CDP_WS_ENDPOINT" --headers "$CDP_HEADERS" snap "$T"

4. Teardown at flow end (you own the lifecycle; flue won't reap it)

node .agents/skills/chrome-cdp/scripts/cdp.mjs --port $PORT stop || true
pkill -f "remote-debugging-port=$PORT" || true

Coordinates (for `clickxy`)

`shot` saves an image at native resolution: image px = CSS

Read more
Ships withshippie

extendable code review and QA agent 🚢

Get the whole plugin
Stats
2,512
Stars
241
Forks
Active
Maintenance
TypeScript
Language
MIT
License
5d ago
Last commit
3y ago
Created
3h ago
Added

Repo: mattzcarey/shippie