Getting Started
How It Works
One chrome-use command leaves the terminal and crosses four processes before it touches a page: the CLI itself, a per-session daemon,
a relay process Chrome launches for the extension, and the ab-connect extension inside the browser. This page takes the chain apart: which process each hop lives in,
what protocol it speaks, and who holds the state. By the end you can explain what the relay-cdp-url file is, why an @ref goes stale,
and why it's an extension rather than a debug port.
The four hops of one command
Take chrome-use open https://example.com. The full chain:
Every hop is a standard protocol, no private magic: CLI to daemon is a local socket, daemon to relay is plain CDP,
relay to extension is Chrome native messaging, extension to page is chrome.debugger (the same one DevTools uses).
The CLI is thin; state lives in the daemon
In CLI mode the chrome-use binary holds no browser state. It serializes the command to one JSON line, writes it to a Unix domain socket,
reads one JSON line back, prints it, and exits. The socket path is ~/.chrome-use/<session>.sock
(with XDG_RUNTIME_DIR set it uses $XDG_RUNTIME_DIR/chrome-use/, see get_daemon_socket_dir in cli/src/native/daemon.rs).
Windows has no Unix sockets, so it uses a TCP port on 127.0.0.1 hashed from the session name.
The first command to a session finds no socket, so the CLI forks the daemon and retries. Beside the daemon sit a few sidecar files:
<session>.pid, <session>.version, <session>.stream (the real-time stream server's port).
The daemon holds three things across commands:
- the CDP connection to the browser (
CdpClient) - the @ref → DOM-node map (
RefMap, see the snapshot section below) - the JS engine for
chrome-use script(boa, pure Rust). It runs in the daemon, not the page, so script state survives a hard navigation — see Single-pass Scripting.
batch and script "run many steps in one round-trip." The daemon exits after ~10 minutes idle, cleaning up the tabs it opened.
The relay is a process Chrome launches
The third hop is the confusing one: you don't start the relay — Chrome does.
At install time, chrome-use extension install writes a manifest into Chrome's native-messaging host directory
(host name com.leeguoo.chrome_use, compatible with the old com.agent_browser.connect) that registers the chrome-use binary's path and the extension ids allowed to talk to it.
When the ab-connect extension calls connectNative, Chrome launches chrome-use in a hidden __nm-host mode (run_nm_host in cli/src/connect.rs),
and the two speak the native-messaging protocol over stdio: a 4-byte little-endian length prefix plus one JSON frame.
This nm-host process does two things:
- binds a random port on 127.0.0.1, opens a WebSocket endpoint
ws://127.0.0.1:<port>/<guid>, and writes the URL into~/.chrome-use/relay-cdp-urlat mode 0600. The guid is unguessable and the file is readable only by the current user, so no other local process gets the entry point. The daemon reads this file and connects to it as if it were an ordinary Chrome CDP endpoint. - translates between the extension protocol and standard CDP, answering discovery commands like
Target.getTargetslocally instead of round-tripping to the extension every time (cli/src/native/relay.rs).
~/.chrome-use/relay-cdp-url usually means the relay never came up (the extension didn't connect);
stdin EOF in nm-host.log usually means Chrome reclaimed the MV3 background worker. Details on Troubleshooting.
The extension does one thing
ab-connect is an MV3 extension whose core job is to run CDP commands per tab via chrome.debugger and hand results back to the relay over native messaging.
Its permission footprint is 7 items, with no <all_urls>. Two properties that matter for fingerprinting:
- it attaches only to the agent's own tabs. The page you're looking at is never attached, so you never see the "chrome-use has started debugging this browser" banner (since extension 0.5.5, it attaches only agent-owned tabs).
- this path injects no JS patches. The page sees your real Chrome, real cookies, real fingerprint, so CreepJS reads 0% bot. The anti-detection isn't disguise; there's simply nothing to disguise (see Stealth & Anti-detection).
In other words, the extension is the only process in the chain that touches the page, and it touches it with the same protocol as DevTools.
Why an extension, not --remote-debugging-port
The classic way to drive a real Chrome is --remote-debugging-port=9222 and a direct CDP connection. Chrome 136 broke half of that:
every time a client connects, Chrome pops a blocking "Allow remote debugging?" dialog. One click per connection means no automation.
The port also has to be opened before Chrome starts, which asks the user to launch their daily browser a different way.
Native messaging avoids this because the trust runs the other way: an outside process doesn't knock on Chrome's port — Chrome launches and authenticates the host by the extension id registered in the host manifest. You click "Add to Chrome" once, then zero confirmations forever. Local safety rests on two things: only extensions listed in the manifest can start the host; and the relay's WebSocket URL carries an unguessable guid written to a 0600 file.
Two modes
The four-hop chain above is the default mode: connect to your real, logged-in Chrome. There's a second path, --launch:
# default: extension relay, your real Chrome
$ chrome-use open https://example.com
# launch a separate isolated stealth Chromium
$ chrome-use --launch open https://example.com
| extension relay (default) | --launch | |
|---|---|---|
| browser | the Chrome you're already using | a separate Chromium the daemon launches |
| logins | all already there | an empty temp profile (--profile for a persistent one) |
| chain | daemon → relay → extension → chrome.debugger | daemon connects CDP directly, no relay or extension |
| anti-detection | not needed: it's a real browser, zero injection | native overrides (UA, timezone, webdriver, etc., see stealth.rs), no JS patches |
| fits | need logins, need to beat anti-bot, user takes over anytime | clean-environment testing, parallel one-off tasks |
Both modes share the same daemon architecture and the same commands; the only difference is who the daemon connects to.
One colored tab group per --session
When several agents share one real Chrome, isolation is by tab group:
$ chrome-use --session research open https://a.com # research group
$ chrome-use --session shopping open https://b.com # shopping group, mutually invisible
When the daemon starts it records the session name in DAEMON_SESSION, and every tab it opens lands in that session's own colored Chrome tab group.
Isolation is enforced at the relay layer (client_groups in relay.rs): after connecting, the daemon declares its group, and from then on its
Target.getTargets returns only tabs in that group. It can't see your tabs or other agents' tabs, so it can't click them either.
Ownership rules cover the details: a new tab is tagged with its creator's group; a pop-up inherits the opener's group, so cross-process jumps (an OAuth pop-up) don't get lost;
chrome-use adopt is the only cross-group channel — after adoption that one tab is re-tagged into the adopter's group, the rest stay invisible. Session details on Sessions & Persistence.
Where snapshot and @ref come from
The text tree that chrome-use snapshot -i prints isn't HTML — it's the accessibility tree. The daemon pulls the full AX tree via CDP's
Accessibility.getFullAXTree (cli/src/native/snapshot.rs), filters for interactive roles like button, link, textbox, and renders indented text:
button "Submit" [ref=e12]
textbox "Email" [ref=e13]
Each [ref=eN] is a number the daemon assigns while building the snapshot, backed by the RefMap (cli/src/native/element.rs):
a ref maps to CDP's backend_node_id, the stable handle for that DOM node in the browser process. On chrome-use click @e12,
the daemon looks up the node id and acts on the node directly — no CSS selector, no coordinates. Two usage rules follow:
- a ref is daemon memory state. After the daemon restarts (e.g. an idle exit) all old refs are void; just
snapshotagain. - after a big page change a ref may point at a node that's gone. The
RefMapstores each ref's role, name, and other fingerprints, and--healrelocates the ref to the closest node. Locating details on Finding Elements.
Lineage
chrome-use is originally based on vercel-labs/agent-browser (Apache-2.0). The CLI skeleton and some command semantics come from upstream, but the stealth and extension-relay architecture, anti-detection, humanize, multi-agent isolation, and the whole nm-host chain on this page grew here. It's a standalone project now. The full fork story is on Lineage & Upstream.
AGENT_BROWSER_DEBUG=1 makes the daemon log to ~/.chrome-use/<session>.log;
cat ~/.chrome-use/relay-cdp-url shows the relay's live endpoint; chrome-use doctor reports the health of every hop.
Troubleshooting on Troubleshooting, extension install on Real Chrome, anti-detection data on Stealth & Anti-detection.