browser-control exposes a browser as a small, debuggable pipe: page snapshots, stable element refs, raw CDP, a background event watcher, and shell-runnable scripts. The coding agent stays the agent — browser-control just gives it hands and eyes.
Why browser-control
- Shell-native. Every capability is a subcommand that prints compact text or JSON. No SDK, no long-running server to babysit, no language lock-in — if your agent can run a shell command, it can drive a browser.
- Refs built for LLMs.
snapshothands back stable@e1/@e2handles you act on directly (click @e3), instead of brittle, hand-written selectors. CSS selectors andx,ycoordinates still work when you want them. - Raw CDP escape hatch. Anything the helper surface can't express,
evalandcdpcan. You never hit a wall and have to switch tools. - Self-observing. A hidden daemon keeps in-memory event / network / console rings, and every command failure drops a compact trace under
.browser-control/traces/so an agent can diagnose itself. - Provider-agnostic cloud. Drive a local Chrome or a remote session on Browser Use, Steel, Hyperbrowser, Browserbase — or any CDP provider — with the same commands.
- Reproducible. One static Rust binary, built from a pinned toolchain and a committed
Cargo.lock.
Installation
From crates.io
The crate is named
browser-control-cli(the namebrowser-controlwas taken), but the installed command isbrowser-control.
Prebuilt binaries
Grab a binary for your platform from the latest GitHub release — no Rust toolchain required.
# macOS / Linux, e.g. aarch64-apple-darwin:
|
From source
The release binary lands at target/release/browser-control. Put it on your PATH:
Requirements
- Rust — the toolchain pinned in
rust-toolchain.toml(installed automatically byrustup toolchain install). - Chrome / Chromium — any recent build reachable over CDP.
browser-control launchwill start one for you;doctorreports what it found.
Reproducible builds
The direct dependencies in Cargo.toml are exact-version constraints and transitive deps are frozen by Cargo.lock. Always build with --locked in CI or benchmark reproduction:
Quick start
Already have a browser listening on CDP? Skip launch and point at it:
# Short aliases also work:
Selectors
Every action accepts three target styles — reach for whichever fits:
| Style | Example | When |
|---|---|---|
| Ref | click @e3 |
Default. Stable handles from snapshot/observe; ideal for LLM workflows. |
| CSS | click '#submit' --wait 5 |
When you know the selector; --wait polls for it first. |
| Coordinates | click 100,200 |
Canvas, maps, anything without a stable node. |
eval IIFE-wraps a top-level return, so eval 'const x = 1; return x' works as written.
Commands
Observe
Act
press maps editing combos (ctrl/cmd + a/c/x/v/z, cmd+shift+z) to renderer editing commands, so select-all and friends work headless and on macOS where the app menu would otherwise swallow them.
Wait
Browser & tabs
Scripts & batch
|
run scripts receive BROWSER_CONTROL_BIN and BROWSER_CONTROL_WORKSPACE. Stdin execution is opt-in via --from-stdin — invoking browser-control with no subcommand and no flag is an error, so an empty or typo'd pipe can't silently bash-exec stdin.
Cloud browsers
The same commands drive a remote browser. Pick a provider with BROWSER_CONTROL_CLOUD_PROVIDER and browser-control speaks plain CDP to whatever session it gets back:
Built-in presets, each reading its own key:
| Provider | BROWSER_CONTROL_CLOUD_PROVIDER |
API key env |
|---|---|---|
| Browser Use (default) | browser-use |
BROWSER_USE_API_KEY |
| Steel | steel |
STEEL_API_KEY |
| Hyperbrowser | hyperbrowser |
HYPERBROWSER_API_KEY |
| Browserbase | browserbase |
BROWSERBASE_API_KEY (+ BROWSERBASE_PROJECT_ID) |
Any other provider works without a code change. Override
BROWSER_CONTROL_CLOUD_API,BROWSER_CONTROL_CLOUD_API_KEY,BROWSER_CONTROL_CLOUD_AUTH_HEADER, and the optionalBROWSER_CONTROL_CLOUD_{CREATE,LIST,STOP}_PATH/_STOP_METHOD/_CDP_FIELD.browser-controlonly needs the response field that carries the CDP URL.
Local Chrome profiles can be reused or synced into a remote cloud profile:
Observability
The hidden daemon command is an implementation detail — normal action / wait / dialog commands auto-start it when event history matters. It accepts tiny JSON requests over .browser-control/daemon.sock, keeps small in-memory rings for events / network / console, and writes .browser-control/daemon-state.json only as a debug artifact. Raw cdp uses the daemon when available and falls back to a direct CDP socket otherwise.
On any command failure, browser-control writes a compact trace under .browser-control/traces/<timestamp>/ with the error plus whatever daemon state / events / network / console history is available. Set BROWSER_CONTROL_NO_TRACE=1 to disable it.
Skills
init also copies bundled interaction and domain skill docs from skills/ into the workspace when available — short, focused notes (connection, freshness, final-answer, evidence-page, plus per-site domain guides) that a coding agent can read on demand to drive real sites more reliably.
Architecture
browser-control is intentionally small: a shell-native Rust CLI around CDP, designed for coding agents that already have their own planning, retry, and file-editing loop. More automation behavior stays outside the tool — the agent is the agent, and the browser is a small debuggable pipe controlled from the shell. This follows the bitter lesson: expose general mechanisms (CDP, snapshots, shell scripts, browser state) instead of baking brittle web-agent intelligence into the tool. Reach for snapshot first, refs for simple actions, and eval / cdp whenever the helper surface isn't enough.
A few deliberate robustness choices:
page-inforeturns{dialog: ...}instead of hanging when a native alert/confirm/prompt is open (the page's JS thread is frozen untildialog accept|dismiss).new-tab <url>createsabout:blank, attaches, then navigates, so a follow-upwait loadcannot return before navigation starts.- Tab auto-selection skips
chrome:///devtools:/// extension internals.
Code layout (src/): main.rs CLI + dispatch · cdp.rs CDP client + endpoint discovery · daemon.rs event daemon + IPC · actions.rs input actions · js.rs injected JS · lifecycle.rs launch/stop/doctor · workspace.rs .browser-control/ state · cloud.rs cloud-provider passthrough · output.rs rendering.
Verification
The companion WebVoyager benchmark package lives at omxyz/webvoyager.
Inspired by Vercel's agent-browser and browser-harness.