Semantic browser control for agents.
Open source by Revitt, via Don Works.
Website · Install · MCPlexer · Issues
brw runs a real, visible Chrome/Chromium browser and exposes it over MCP and
HTTP. Agents use stable refs like e17 instead of CSS selectors or screenshots
for normal web work.
- Controls headed Chrome/Chromium through CDP.
- Exposes stdio MCP tools for agent harnesses.
- Exposes an HTTP JSON API for custom clients.
- Returns semantic snapshots from DOM plus accessibility data.
- Reads page prose, links, headings, forms, tables, and structured product data.
- Clicks, types, fills, selects, scrolls, drags, uploads, waits, and asserts by ref.
- Returns a post-action observation after every action.
- Uses screenshots only as visual fallback, with optional Set-of-Marks overlays.
- Supports tabs, downloads, console, network capture, request replay, and cancellation.
- Reuses a persistent non-default Chrome profile for signed-in flows.
- Bridges to an already-authenticated installed Chrome profile through a Chrome extension.
- Runs cleanly over SSH so the browser profile stays on the machine that owns it.
Internal pre-release head-to-head runs against Claude-in-Chrome showed the
direction we built for: on semantic web tasks, brw needed fewer turns,
less token spend, less wall time, and lower estimated cost. The main reason:
agents act from refs and action observations instead of repeatedly interpreting
screenshots.
Claude-in-Chrome's durable advantage was installed-profile auth. brw answers
that with the brw Chrome extension and SSH-first remote runtime: keep Chrome,
cookies, passkeys, downloads, and human takeover on the browser machine, while
MCP runs over stdio through SSH.
The full MCP surface is large. For lean agent contexts, run:
brwd --mcp --mcp-tools coreFor a ready-to-paste agent system prompt that encodes the fast, token-efficient
loop (act by ref, read the post-action observation instead of re-snapshotting,
use deltas, screenshot only as a fallback), run brwd --print-system-prompt.
See docs/agent-guide.md.
Raw private benchmark transcripts are not shipped in this repository because they can contain prompts, machine paths, and session metadata. Treat the public benchmark note as directional until a reproducible public harness lands.
See docs/benchmarks.md.
Download a native installer from the GitHub releases page:
- Windows:
.msi - macOS:
.pkg - Linux:
.debor.rpm
After installing, run brwd directly from your terminal or MCP client.
git clone https://github.com/Don-Works/brw.git
cd brw
make buildRun as an MCP server:
./bin/brwd --mcp --http offRun the HTTP API:
./bin/brwd --http 127.0.0.1:17310Open a page and read controls:
curl -s 127.0.0.1:17310/api/browser/open \
-H 'content-type: application/json' \
-d '{"url":"https://example.com"}'
curl -s 127.0.0.1:17310/api/page/snapshot | jqRemote control is a first-class path. The browser stays visible on the remote machine. SSH carries stdio MCP.
Generate a client config:
brwctl mcp-config \
--workspace brw \
--profile work-profile \
--transport remote \
--profile-policy ~/.config/brw/browser-profiles.json \
--mode bridgeThe policy decides which browser profile and transport a workspace may use. For installed Chrome profiles, prefer a long-lived remote bridge daemon plus a generated SSH stdio wrapper:
brwctl remote-mcp-wrapper \
--host browser-host \
--user browser-user \
--remote-brwd ~/.local/bin/brwd \
--output ~/.local/bin/brw-browser-mcpChrome 136+ blocks remote debugging against the default Chrome data directory.
For auth that already exists in an installed browser profile, use the brw
extension. It bridges the daemon to your real, signed-in browser over
ws://127.0.0.1, drives visible tabs via the Chrome debugger protocol, and
refuses every cookie CDP method and the site-storage domains — so it never reads
HttpOnly cookies or bulk-exports a site's stored credentials, and sensitive form
fields are redacted from snapshots, reads, and captured network headers.
The extension is open source (AGPL-3.0) and ships with a pinned public key, so it always loads with the same stable id — identical for load-unpacked, the self-hosted CRX, and the Web Store build:
amocjcgddnoakjijfggdpnefdnboilpe
That id is the daemon's DefaultBridgeExtensionID, so an unconfigured bridge
trusts the real extension with no policy edit. Only set bridge_extension_id
for a different re-signed build.
Chromium is open source and not gated by the Chrome Web Store, so you can
force-install + auto-update the extension from a single policy file. brw
self-hosts the signed package and an Omaha/gupdate auto-update manifest:
- Signed package (CRX): https://brw.donworks.co.uk/brw.crx
- Auto-update manifest: https://brw.donworks.co.uk/updates.xml
ExtensionInstallForcelistentry:amocjcgddnoakjijfggdpnefdnboilpe;https://brw.donworks.co.uk/updates.xml
Drop the ready-made policy for your platform:
- Linux (no MDM needed): copy
brw-chromium-policy.jsoninto/etc/chromium/policies/managed/(or/etc/opt/chrome/policies/managed/for Chrome). Chromium installs from the manifest and auto-updates. - macOS: install the configuration profile
brw-chromium.mobileconfigmanually or via MDM. macOS force-install requires a managed profile / MDM; it is not settable from user-domain defaults. - Windows: import
brw-chromium-policy.reg, or set the equivalent GPO atHKLM\SOFTWARE\Policies\Chromium\ExtensionInstallForcelist.
brwctl generates these: brwctl pack-extension --key <pem> (CRX),
brwctl update-xml --crx-url <url> (manifest), and
brwctl macos-policy --update-url <url> --install-mode force_installed
(.mobileconfig). The private signing key lives outside the repo.
Zero-click option: brwd --extension <dir> launches Chromium with the
extension already loaded (it passes Chrome's --load-extension through), so
there is nothing to install or click. Chrome 137+ dropped reliable
--load-extension, so this path is Chromium-only.
Verified: Chromium 151 loads the extension with the correct id and bridges to
brwd end-to-end, and the auto-update endpoint (updates.xml + CRX) is valid
and served with the correct content-types.
- Load unpacked: run
make install-extensionto print the folder and openchrome://extensions, then enable Developer mode → Load unpacked → selectextension/. - Chrome Web Store (one-click): an unlisted listing is in review for one-click install + auto-updates, sharing the same id (not live yet).
See the Install page, docs/install.md, and docs/auth-model.md.
Core MCP tools include:
brw_open,brw_list_tabs,brw_focus_tab,brw_close_tabbrw_list_tab_groups,brw_group_tabs,brw_ungroup_tabsbrw_read,brw_read_data,brw_snapshot,brw_findbrw_click,brw_click_text,brw_type,brw_fillbrw_select,brw_press,brw_scroll,brw_hoverbrw_drag,brw_upload_file,brw_wait_forbrw_batch,brw_cancel,brw_observebrw_screenshot,brw_screenshot_elementbrw_emulate_devicefor DevTools mobile/responsive emulationbrw_network_requests,brw_network_capture,brw_replay_requestbrw_console,brw_downloads,brw_tracebrw_assert_visible,brw_assert_text,brw_assert_valuebrw_page_tools,brw_call_page_tool(WebMCP)brw_notify,brw_commit
Use --mcp-tools core to advertise only the common-flow tool set while keeping
all tools callable.
MCP stdio lifecycle: brwd --mcp exits cleanly on SIGTERM/SIGINT (including
while blocked waiting for input), when its stdin closes, and when it is orphaned
by its parent. For supervisors that spawn one brwd --mcp --upstream-http
proxy per session and may abandon it without closing stdin, add
--mcp-idle-exit 90m (or BRW_MCP_IDLE_EXIT): the proxy is a disposable
stateless shim, so it exits 0 after the idle window and the supervisor respawns
it on the next call instead of accumulating zombie children.
Backend-specific notes:
brw_emulate_deviceuses Chrome DevTools Protocol device emulation, not OS window resizing. Presets such asiphone_se,iphone_14,pixel_7,galaxy_s20, andipad_miniapply CSS viewport size, DPR, mobile viewport meta behavior, touch emulation, and mobile UA/platform overrides. Passclear:trueto reset a tab.brw_upload_fileaccepts the file from exactly one source:path/paths(files already on the browser host),bytes_base64(inline base64 contents, no host filesystem access needed), orurl(the daemon fetches it over http(s)). Forbytes_base64/urlthe daemon materializes a temp file on the browser host and removes it after the upload; usefilenameto control the name the page sees.brw_evaluatetruncates oversized results with an explicit…[truncated: returned N of M bytes]marker instead of ever returning an empty result; page through large payloads with theoffset/max_bytesparams.brw_downloadscaptures downloads on the direct-CDP backend (supported: true). On the extension-bridge backend it returns an empty list withsupported: falseplus an explanatorynote, because the bridge cannot observe CDP download events; branch onsupportedto detect this case.- Snapshots descend into open and closed shadow roots and same-origin
iframes. Cross-origin iframes cannot be read (the browser isolates them);
instead of failing silently, a snapshot surfaces them in
metadata.cross_origin_frames(box + origin) with across_origin_note, so an agent can fall back tobrw_screenshot+brw_click_xy. brw_snapshotacceptsformat:"compact"for a one-line-per-element text rendering (ref, role, name, key state) that costs markedly fewer tokens than the default JSON — prefer it for small models.- WebMCP: with
--enable-webmcp, brw acts as the agent-side runtime for the W3Cnavigator.modelContextdraft. Cooperating sites can register page tools thatbrw_page_toolslists andbrw_call_page_toolinvokes — more reliable and token-efficient than driving the DOM. Default off (it is observable to pages). - Navigation guardrail:
--blocked-domains/--allowed-domains(orBRW_BLOCKED_DOMAINS/BRW_ALLOWED_DOMAINS) gatebrw_open,brw_open_incognito, andbrw_replay_requestso a prompt-injected agent cannot steer the browser to off-limits domains (subdomains included; block wins over allow). Opt-in; off by default.
With the extension bridge, agents can organize visible Chrome work into named
tab groups. Use brw_list_tab_groups to choose the next client-side run
name (for example brw-1, brw-2, or a short task label), pass group to
brw_open to create or reuse that titled group, then pass the
returned/listed group_id on later brw_open or brw_group_tabs calls
to keep the run's tabs together. Ungrouped/default tabs remain visible to
brw_list_tabs and can still be targeted normally by tab_id. Tab groups
are UI organization only; use profiles or incognito contexts for cookie/storage
isolation.
Tab isolation (default). When driving your real Chrome over the bridge, the
daemon defaults to isolation: brw acts only on tabs it owns. It opens its tabs
in its own group (--bridge-tab-group, brw by default), in the background,
and a no-tab_id action targets brw's own working tab — never the tab you have
focused. If brw owns no tab yet, the first page action opens a fresh one instead
of hijacking whatever you are looking at. To act on one of your existing tabs,
pass its tab_id (from brw_list_tabs). This keeps automation (and parallel
agent runs) from stomping your open tabs. To restore the legacy behavior where
brw follows your manually-focused tab, run with --bridge-follow-focus (or
BRW_BRIDGE_FOLLOW_FOCUS=1).
brw uses a normal visible browser and persistent user profile. It does not add
stealth code, CAPTCHA bypass, MFA bypass, fraud-check bypass, consent bypass, or
cookie extraction.
Browser-control HTTP binds to loopback by default. For remote use, prefer stdio MCP over SSH.
brw drives a real signed-in Chrome and is operated by a possibly
prompt-injected agent, so the loopback surfaces are hardened against same-machine
browser attackers rather than assumed safe:
- HTTP control plane (
127.0.0.1:17310) rejects cross-origin browser requests (CSRF) and, on a loopback bind, enforces aHostallowlist (DNS-rebinding). A non-loopback bind (e.g. a Tailscale/LAN address for the documented "behind SSH/Tailscale with caller auth" path) skips theHostallowlist — its legitimateHostmay be a MagicDNS name — but still rejects cross-origin browser requests. CLI/MCP clients send no browserOrigin, so they are unaffected. - Extension bridge (
127.0.0.1:17311) authenticates the extension with a per-launch token: the daemon mints it each start, persists it0600at~/.brw/bridge-token, and serves it only over loopback to the extension (a web page's cross-origin fetch gets an opaque response). The0.2.0+extension presents it in its first frame. This is non-breaking: a wrong token is always rejected, but a not-yet-reloaded older extension that sends no token still connects (logged once), so upgrading the daemon never bricks an installed extension. SetBRW_BRIDGE_REQUIRE_TOKEN=1to make the token mandatory once every extension is on0.2.0. Empty-Origin (non-browser) websocket clients are rejected regardless. - Cookie/storage promise is enforced, not just asserted. The extension
refuses every cookie CDP method and the whole family of site-storage domains
(
Storage,DOMStorage,IndexedDB,CacheStorage,Database), so even a rogue server that answered the extension's socket cannot read HttpOnly cookies (which page JS cannot reach) or bulk-export a site's stored data throughbrw. Credential request headers (Authorization,Cookie, …) are redacted from captured network traffic for the same reason.Runtime.evaluatestays available becausebrwneeds it to drive the page, so a caller can still read a non-HttpOnlydocument.cookieor an input value — the enforced line is "no HttpOnly cookies, no bulk storage export," not "no script access at all."
The extension-side protections (token, cookie denylist, dialog handling) take
effect whenever the 0.2.0 extension next loads — reload it in
chrome://extensions, or it loads automatically the next time Chromium is
relaunched with --load-extension. Nothing breaks in the meantime.
brw is part of Don Works, Revitt's open-source arm.
Related:
- Don Works — the umbrella (github.com/Don-Works).
- MCPlexer — MCP gateway and cross-harness AI runtime (github.com/Don-Works/mcplexer).
- Revitt — the parent company.
AGPL-3.0. See LICENSE.