Run multiple real Chrome browsers on your desktop and drive them locally or from another machine -- perhaps an AI agent or an LLM CLI -- using https://github.com/vercel-labs/agent-browser without configuring ports, sessions and the like.
It allows a scraper, agent, LLM-CLI or other test harness, typically running in a data centre, a remote machine or headless computer to connect to a browser physically running on your desktop. Why is this useful? It gives that remote machine a real, observable browser, on a residential/office IP. Not a headless browser, not a quirky user-agent, but whatever version of Chrome you'd use for normal user-driven browsing. A human can watch and intervene if required (e.g. to finish a hard login by hand) before automation resumes. Sessions are optionally retained.
Put another way, it's like a self-hosted version of Browser Use, AgentCore, Browserbase, Browserless and Kernel's hosted browsers.
If you want an LLM or coding agent to use BrowserHost (to fetch live web pages or test a web app it's building), point it at one of these:
docs/llm-instructions.mdβ drop-in, tool-agnostic instructions for any LLM/agent: how to installagent-browser, find your desktop's host, and drive the browser. Includes a worked example.- Claude Code skill:
browser-via-browserhostβ a ready-made Claude Code skill that triggers automatically when a task needs a real browser and remembers your host locally. Copy the folder into~/.claude/skills/(or your project's.claude/skills/) to install it.
BrowserHost is a single Go binary that runs two listeners:
:9000β CDP proxy. A fixed universal endpoint (/cdp) that routes each connection to a per-session Chrome instance by asessionquery parameter, launching Chrome on demand.:9001β control API + web UI. A JSON control plane and a live web dashboard for creating, watching, and tearing down sessions.
See section on Security below for some important warnings. In short, don't run this program on a computer which allows unfettered access to port 9000.
There is no required client software. The canonical client is stock
agent-browser pointed at the universal URL (see Using it).
An optional convenience CLI, bh, wraps that contract (builds the URL, sets attribution,
and offers ls/new/rm/log/health) β see docs/client.md. Build it
with go build -o bh ./cmd/bh.
See docs/server-spec.md for the full product spec.
- Go 1.25+ (built with 1.26) β only to build, not to run. You can download the binary and just run it.
- Chrome or Chromium installed. BrowserHost auto-detects the binary on Windows and
macOS, or you can point at it explicitly with
-chrome-bin.
go build -o browserhost.exe ./cmd/browserhostThe web UI is embedded into the binary (go:embed), so the result is a single
self-contained executable β no separate front-end build step and no runtime
dependencies beyond Chrome.
Windows is the primary target; macOS is supported. To build for another OS/arch, set
GOOS/GOARCH:
# macOS (Apple Silicon)
$env:GOOS="darwin"; $env:GOARCH="arm64"; go build -o browserhost ./cmd/browserhost
# Linux
$env:GOOS="linux"; $env:GOARCH="amd64"; go build -o browserhost ./cmd/browserhostgo test ./....\browserhost.exeOn start it logs the resolved Chrome path and the listen addresses, e.g.:
using chrome: C:\Program Files\Google\Chrome\Application\chrome.exe
browserhost 0.1.0 β proxy :9000, ui http://<hostname>:9001
Open the web UI at http://localhost:9001 to create sessions and watch live state. Note,
creating sessions via this UI is optional; you can just call the server as per the directions
below and the session will be created automatically.
Note on the network transport. BrowserHost binds locally and is transport-agnostic by design β it assumes you provide the tunnel (SSH, VPN, Tailscale, etc.) between the data-centre client and your desktop. Tailscale works really well for this.
There is zero inherrent security in the browserhost process. Running it opens port 9000 and 9001, both of which listen and respond without any authentication whatsoever. This means that anybody that can reach port 9000 of the machine on which it runs can open a browser on that machine and send instructions to it to do anything it wishes.
You're advised in the strongest terms to not run this software on a computer which can be reached from other untrusted hosts. If you have a network of untrusted hosts, firewall connections to port 9000 to come only from trusted and expected hosts.
All configuration is via flags (defaults shown):
| Flag | Default | Description |
|---|---|---|
-proxy-addr |
:9000 |
Listen address for the CDP proxy. |
-ui-addr |
:9001 |
Listen address for the control API and web UI. |
-chrome-bin |
(auto-detect) | Path to the Chrome/Chromium binary. |
-data-root |
(per-OS cache dir) | Root directory for per-session profile dirs (where cookies/logins persist). |
-public-host |
(machine hostname) | Host shown in client-facing dial URLs. Set this to the name the client reaches your tunnel by. Never emits 127.0.0.1. |
-launch-timeout |
15s |
Max wait for a freshly launched Chrome to become ready. |
-idle-ttl |
30m |
Terminate sessions idle (no connections) longer than this. |
-command-log-size |
200 |
Per-session command-log ring buffer size. |
-shutdown-kills-browsers |
false |
On clean shutdown, kill the managed Chrome instances (otherwise they keep running). |
Example:
.\browserhost.exe -public-host desktop.tailnet.ts.net -idle-ttl 1h -shutdown-kills-browsersEvery client points at the same fixed URL and varies only the session query
parameter β clients never manage ports, launching, or per-session URLs:
agent-browser --cdp "ws://<host>:9000/cdp?session=<name>" <subcommand> [args]- The session name is a query parameter (
?session=<name>), not part of the path, and not agent-browser's local--sessionflag (that never reaches the wire). - First connect to an unknown name auto-launches Chrome for it. Reconnecting to the
same name reuses the running browser, so cookies/login persist across commands and
across separate
agent-browserinvocations. - Optional self-attribution: append
&who=<label>so the UI shows who demanded the session, e.g....?session=s1&who=scraper@dc1. wss://works when you terminate TLS in front of the proxy.
Example session:
# Cold start: launches a Chrome for "s1", loads the page
agent-browser --cdp "ws://desktop:9000/cdp?session=s1" open https://example.com
# Later, from a different process: reuses the same browser and its state
agent-browser --cdp "ws://desktop:9000/cdp?session=s1" snapshotBrowse to http://localhost:9001 to see a live table of sessions (name, state, port,
who, created/last-activity, active connections) with the universal dial URL to copy, an
expandable per-session command log, and Create / Terminate actions.
JSON over HTTP. Useful for create-before-connect (so a session exists and is attributed in the UI before any command connects):
| Method | Path | Body / Params | Behaviour |
|---|---|---|---|
GET |
/api/sessions |
β | List all sessions with state and dial URL. |
GET |
/api/sessions/{name} |
β | One session's detail incl. recent command log. |
POST |
/api/sessions |
{"name","who"} |
Create-before-connect; launches Chrome and blocks until ready. Idempotent. |
DELETE |
/api/sessions/{name} |
?keepProfile=true|false |
Terminate Chrome. keepProfile=true (default) keeps the profile so login persists; false deletes it. |
GET |
/api/sessions/{name}/log |
?since=<cursor> |
Command-log entries for live UI updates. |
GET |
/api/health |
β | Liveness, version, and the Chrome binary path in use. |
# Create-before-connect with attribution
curl -X POST http://localhost:9001/api/sessions \
-H 'Content-Type: application/json' \
-d '{"name":"s4","who":"scraper@dc1"}'
# Tear down, discarding the profile for a clean slate
curl -X DELETE 'http://localhost:9001/api/sessions/s4?keepProfile=false'Session names must match ^[A-Za-z0-9._-]+$.
- Session Manager owns the
name β sessionmap with per-session locking, so concurrent first-hits to a new name launch exactly one Chrome. It launches Chrome with a dedicated--user-data-dir(not headless β observability is the point), waits for theDevToolsActivePortfile, reuses it on later connects, and sweeps idle sessions. - Proxy validates the
sessionparam, resolves or launches the browser, then splices the client WebSocket to Chrome's browser-level WebSocket, pumping CDP frames both ways with no read limit (large DOM/screenshot frames exceed the default 32 KB limit). A session outlives any single connection β Chrome is never killed because a splice ended. - Localhost-leak guard: Chrome's internal
ws://127.0.0.1:<port>/...URL is used only inside the server to dial; clients only ever see the universalws://<public-host>:9000/cdp?session=<name>.
cmd/browserhost/ server entrypoint (main.go)
cmd/bh/ optional client CLI (see docs/client.md)
internal/api/ control API + UI serving (:9001)
internal/proxy/ CDP splice + frame pump (:9000)
internal/session/ session manager, launcher, sweeper
internal/platform/ Chrome discovery + process kill (Win/macOS)
internal/config/ flag parsing
web/ embedded single-page UI
docs/ product spec
spike/ original CDP-relay proof of concept
MIT Β© 2026 David Adams