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:

chrome-use open <url> the CLI · exits when done Unix domain socket ~/.chrome-use/<session>.sock · one JSON round-trip daemon one per --session · stays resident holds the CDP connection · @ref map · script engine standard CDP over WebSocket · ws://127.0.0.1:<port>/<guid> relay · chrome-use __nm-host a process Chrome launches for the extension answers Target.* discovery locally · forwards the rest Chrome native messaging · stdio (4-byte length prefix + JSON frame) ab-connect extension MV3, installed once · chrome.debugger, attached per tab your already-logged-in real Chrome tab real cookies · real fingerprint · sites read a real person

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:

  1. the CDP connection to the browser (CdpClient)
  2. the @ref → DOM-node map (RefMap, see the snapshot section below)
  3. 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.
ℹ️ Why the daemon stays resident
The CDP connection and @ref map are built once and reused, saving a reconnect and re-snapshot per command. This is also the base that lets 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:

  1. 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-url at 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.
  2. translates between the extension protocol and standard CDP, answering discovery commands like Target.getTargets locally instead of round-tripping to the extension every time (cli/src/native/relay.rs).
⚠️ Debug anchors
A missing ~/.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:

  1. 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).
  2. 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:

bash
# 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
browserthe Chrome you're already usinga separate Chromium the daemon launches
loginsall already therean empty temp profile (--profile for a persistent one)
chaindaemon → relay → extension → chrome.debuggerdaemon connects CDP directly, no relay or extension
anti-detectionnot needed: it's a real browser, zero injectionnative overrides (UA, timezone, webdriver, etc., see stealth.rs), no JS patches
fitsneed logins, need to beat anti-bot, user takes over anytimeclean-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:

bash
$ 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:

  1. a ref is daemon memory state. After the daemon restarts (e.g. an idle exit) all old refs are void; just snapshot again.
  2. after a big page change a ref may point at a node that's gone. The RefMap stores each ref's role, name, and other fingerprints, and --heal relocates 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.

🔧 See it yourself
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.