One-page cheat sheet · discovery, message shapes, core domains
Launch Chrome with --remote-debugging-port=9222 (and a non-default
--user-data-dir, required since Chrome 136). Discover targets over plain HTTP,
open a target's webSocketDebuggerUrl, then trade JSON: send commands,
receive responses and events.src
| Shape | Has | Direction |
|---|---|---|
| Command | id + method (+ params) | client → browser |
| Response | id + result or error | browser → client (matched by id) |
| Event | method + params, no id | browser → client (unsolicited) |
Demultiplex by id: has one → a response; none → an event.
Responses and events interleave freely; ordering is not ping-pong.
| Endpoint | Gives you |
|---|---|
/json/version | Browser info + the browser-level webSocketDebuggerUrl |
/json or /json/list | Every target; each with a type and its own webSocketDebuggerUrl |
/json/protocol | The full protocol schema as JSON |
/json/new, /json/activate/{id}, /json/close/{id} | Create / focus / close a target |
Target types include page (a tab — the usual one),
service_worker, background_page, browser_ui. Filter for
page to drive a tab.
| Domain | For |
|---|---|
Page | Navigation, lifecycle events, screenshots, frames |
Runtime | Evaluate JS in the page; console API calls |
DOM | Read/query/modify the DOM tree by nodeId |
Network | Watch requests/responses; read response bodies |
Input | Synthesize keyboard / mouse / touch |
Target | Discover & attach to targets; sessions |
Browser | Browser-level: version, windows, permissions |
Events are opt-in: call Domain.enable (e.g. Page.enable,
Network.enable) before you expect that domain's events.
| Command | Key params → result |
|---|---|
Browser.getVersion | — → product, protocolVersion, userAgent |
Page.navigate | url → frameId, loaderId |
Runtime.evaluate | expression, returnByValue, awaitPromise → result |
DOM.getDocument | depth, pierce → root node |
DOM.querySelector | nodeId, selector → nodeId |
Page.captureScreenshot | format, quality, clip → data (base64) |
Network.getResponseBody | requestId → body, base64Encoded |
| Event | Fires when |
|---|---|
Page.loadEventFired | The page's load fired |
Page.frameNavigated | A frame committed a navigation |
Network.requestWillBeSent | A request is about to go out |
Network.responseReceived | Response headers arrived |
Runtime.consoleAPICalled | Page called console.* |
Target.attachToTarget {targetId, flatten:true} returns a sessionId.
In flat mode you add a top-level sessionId to each command to route it to
that target — so one WebSocket can drive many tabs. Target.setAutoAttach {flatten:true}
auto-attaches to related targets (iframes, workers). Flat mode is preferred; nested mode is
deprecated.docs
The debugging port is unauthenticated — full control to anyone who can
reach it. Use a throwaway --user-data-dir, bind to 127.0.0.1, never
expose it publicly. Prefer --remote-debugging-pipe (no open port) when you can.chrome