High-performance Rust CLI that connects to a running Chrome browser via the DevTools Protocol. Talks directly to Chrome's CDP WebSocket — no MCP overhead, no headless browser stack. One command in, one result out.
src/
├── main.rs # Entry point + daemon dispatch
├── lib.rs # CLI (clap) + command routing
├── cdp.rs # Raw CDP over WebSocket (JSON-RPC) + persistent session
├── browser.rs # Auto-connect (DevToolsActivePort)
├── daemon.rs # Background daemon (persistent connection)
├── client.rs # Talks to daemon via Unix socket
├── protocol.rs # IPC message types (DaemonRequest / DaemonResponse)
├── friendly.rs # Target ID → word-pair names
├── format.rs # OutputFormat (text/json/toon) + format_structured
├── result.rs # CommandResult type
├── error.rs # CLI error types and codes
├── constants.rs # Shared constants
├── telemetry.rs # Logging and telemetry
└── commands/
├── executor.rs # Command dispatch + persistent-session reuse
├── navigate.rs
├── pages.rs # list/new/close/select/wait-for
├── screenshot.rs
├── snapshot.rs
├── read_page.rs # read-page (Readability + HTML→Markdown)
├── memory.rs # take-heapsnapshot (CDP streaming) + inspect-heapsnapshot-node / compare-heapsnapshots (offline)
├── evaluate.rs
├── input.rs # click/fill/type/press/hover
├── emulation.rs # emulate (viewport/geolocation/blocklist)
├── console.rs # console drain / live collection
├── network.rs # network drain / live collection
├── sw_logs.rs # extension service-worker log collection
└── third_party.rs # list-3p-tools/execute-3p-tool
Detailed documentation for individual commands:
- read-page — page content extraction as markdown
skill/chrome-devtools/SKILL.md is the source of truth for the agent-facing
skill/documentation — it's what teaches an agent (e.g. Claude Code, opencode) how
to use this CLI (targeting, standard patterns, gotchas, failure handling, etc.).
It gets installed/copied into an agent's skills directory (e.g.
~/.config/opencode/skills/chrome-devtools/SKILL.md); that installed copy is a
deployed artifact, not the source — always edit skill/chrome-devtools/SKILL.md
in this repo, then re-sync/reinstall it, rather than editing the installed copy
directly. skill/chrome-devtools/CUSTOM_SCRIPTING.md documents run-script and
adapter in more depth.
A background daemon keeps a persistent CDP WebSocket connection. On Unix it
listens on $TMPDIR/chrome-devtools-daemon-<uid>.sock (uid-suffixed to isolate
users sharing /tmp); on Windows, on a loopback TCP port published via an
unsuffixed %TEMP% addr file. First CLI invocation spawns it; subsequent
commands reuse it. 5-minute idle timeout; endpoint/PID files are cleaned up on
panics too, and on Unix on SIGTERM/SIGINT (Windows Ctrl-C cleanup is
best-effort — a background daemon has no console).
CdpClient::connect (cdp.rs) bounds the WebSocket handshake with a timeout
(CHROME_CONNECT_TIMEOUT_SECS, default 10s). Without it, a pending Chrome
remote-debugging consent dialog would hang the handshake indefinitely — and
since the daemon binds its socket before connecting to Chrome (see comments
in daemon.rs), the CLI's wait_for_daemon() would succeed immediately while
the actual request silently hung forever waiting on the daemon's response, with
no error and no way for a caller (especially an unattended agent) to tell what
was wrong. The timeout error message is written to be agent-actionable: retry
at most once, then stop and ask a human rather than looping or calling
kill-daemon.
Every page gets a deterministic friendly name (e.g. warm-squid) derived from
Chrome's internal target ID. Commands should always use --target <name> to pin
to a specific page — page indices shift as tabs are opened/closed.
The daemon maintains a persistent CDP session on the active page that
continuously collects Network.* and Runtime.* events. console and
network commands drain whatever has accumulated since the last call.
All commands default to human-readable text. --json and --toon (compact,
LLM-friendly) produce structured output. Mutually exclusive.
inspect-heapsnapshot-node, compare-heapsnapshots, and kill-daemon are
intercepted early in run() before any Chrome connection or daemon spawn.
inspect-heapsnapshot-node and compare-heapsnapshots parse local
.heapsnapshot files purely offline. Note that snapshot diffing matches nodes
by V8 heap object ID, which is only stable within a single Chrome session —
both snapshots must come from the same session to produce a meaningful diff.
kill-daemon drops the daemon's already-approved Chrome connection, so it's
guarded (kill_daemon_decision in lib.rs): interactive (TTY) callers get a
[y/N] confirmation prompt; non-interactive callers (agents, scripts) are
refused outright unless --force is passed. It must never be used as a
"retry" step for connection failures — see the timeout note above.
The daemon retains its startup CWD, so the CLI resolves all relative file-path
arguments (--output, --file-path) to absolute paths in build_request
before sending them to the daemon.
cargo build --release # Binary: ./target/release/chrome-devtools
cargo test # Run all tests
cargo test commands::read_page # Run tests for a specific module- Comments explain why, not what
- Each command is a pure async function taking
&mut CdpClient,session_id,OutputFormat, and command-specific args - Pure conversion/formatting logic is extracted into testable functions
- Tests live in
#[cfg(test)] mod testswithin the same file - Error handling uses
anyhow::Resultwith descriptive messages - CDP calls go through
CdpClient::send_to_target()