Drive your real, logged-in Chrome from CLI and AI agents. A Chrome MV3 extension plus a local Node relay expose a standard CDP endpoint, so any Chrome DevTools Protocol client (Playwright's CDP connect, agent-browser, or your own) can automate the browser you actually use: your sessions, your cookies, no re-login.
Built around two hard requirements:
- No
--remote-debugging-port. The extension connects out to the relay throughchrome.debugger, so this works even where Chrome's debug port is blocked by MDM or enterprise policy, and you never run your daily browser in debug mode. - The agent never takes over. Agent tabs are created unfocused in a labeled tab group inside your current window. Your focus is never stolen, and your tabs are never listed, attached, or closed by the agent.
CDP client (agent-browser, Playwright over CDP, ...)
| ws://127.0.0.1:9334/devtools/browser/<uuid>
v
relay/relay.mjs Node relay, 127.0.0.1:9334
| ws://127.0.0.1:9334/extension (extension dials OUT to the relay)
v
extension/background.js MV3 service worker
| chrome.debugger API
v
Chrome tab
Headless browsers and fresh profiles can't do authenticated work: banking dashboards, internal tools, webmail, anything behind SSO. The usual alternative, relaunching your real Chrome with --remote-debugging-port, exposes every tab to every local process, breaks under enterprise policy, and means running your daily driver in debug mode. This bridge gives agents a narrow, supervised path into the real browser instead.
- Standard CDP surface.
/json/version,/json/list, browser- and page-level WebSocket endpoints. Existing CDP clients connect without modification. - Agent tab isolation (
AGENT_OWN_TAB, default on). Each connecting client gets a fresh tab, created inactive, grouped under a cyan "⚡ Agent" tab group in your window. The relay hides your tabs from the client entirely: they don't appear inTarget.getTargets, can't be attached, andcloseTabrefuses anything the agent didn't create. - Opt-in session driving (
CLAIM_URL). When you want the agent in a logged-in tab,CLAIM_URL=github.com node relay/relay.mjshands it the first tab matching that URL substring, and nothing else. - No focus stealing. Tab activation is a no-op by policy: the extension acknowledges
Target.activateTargetwithout switching what you're looking at, and window focus is never taken. Navigation, evaluation, and reads all work on background tabs. The trade-off: Chrome only delivers synthetic input (mouse clicks, keystrokes) to a window's active tab, so on background tabs drive interactions viaRuntime.evaluate(element.click(), setting field values) — or opt in below. - Opt-in mouse input (
MOUSE_ACTIVATE=on). For flows that need real synthetic mouse events, the relay activates the agent tab within its own window (never focusing the window) once before dispatching mouse input. The click e2e runs in this mode. - No debugger banner. Chrome's yellow "started debugging this browser" infobar isn't just noise: its cancel path detaches every
chrome.debuggersession at once. The included launcher, Dock wrapper, andflag-watchwatchdog keep Chrome running with--silent-debugger-extension-api(see below). - Self-healing. launchd keeps the relay alive, a 20s alarm keeps the MV3 service worker alive, a 15s grace period bridges extension restarts, and
browser-doctor.shchecks and repairs the whole chain. - Clean teardown. On client disconnect the relay detaches every tab that client attached and closes every tab it created. User tabs survive, always.
# 1. Install dependencies + the relay as a login LaunchAgent (autostarts, KeepAlive)
npm install
./install.sh
# 2. Load the extension (one-time, Chrome can't do this from a script):
# chrome://extensions -> Developer mode -> Load unpacked -> select extension/
# 3. Verify
curl -s 127.0.0.1:9334/status # expect "extensionConnected":true
# 4. Drive
agent-browser connect 9334
agent-browser open https://example.com
agent-browser screenshot /tmp/shot.pngAnything that speaks CDP works; agent-browser is just the client used in the examples and tests.
To run the relay in the foreground instead: npm run relay (or PORT=9334 node relay/relay.mjs). Note the extension dials ws://127.0.0.1:9334/extension; if you change the relay port, update RELAY_URL in extension/background.js to match.
--silent-debugger-extension-api is the only mechanism that removes the banner (there is no enterprise policy or preference for it), and it only applies at process start. Three pieces make it stick:
launch-chrome.shlaunches Chrome with the flag (refuses if Chrome is already running, since flags can't be injected into a live process).wrapper/build-wrapper.shbuilds "Google Chrome (No Banner).app": a Dock-able launcher wearing Chrome's icon that declares http/https, so you can set it as your default browser. Link clicks while Chrome is closed then launch it flagged instead of bare.flag-watch.sh(a LaunchAgent installed byinstall.sh) catches everything else: login reopen, Spotlight,open -a. If it finds Chrome running without the flag it shows one dialog per Chrome process offering a one-click relaunch;--restore-last-sessionbrings your tabs back.
- macOS TCC will kill launchd jobs that read from
~/Desktop,~/Documents, or~/Downloads, with exit code 78 and an empty error log (writing the log is blocked too).install.shtherefore deploys the relay runtime to~/Library/Application Support/chrome-agent-bridgeand points launchd there; the repo stays wherever you cloned it. The unpacked extension can load from the repo, since Chrome is a GUI app with Desktop access. - Chrome caches an unpacked extension's code until you press the reload button on
chrome://extensions. Browser restarts do not pick up edits. If behavior doesn't match the code, that's why. ./browser-doctor.shchecks LaunchAgent → relay → extension → flag, repairs what it safely can, and./browser-doctor.sh --deepdrives a page end-to-end.
Read this before installing; the whole point of the tool is programmatic access to your real browser.
- Local trust boundary. The relay binds
127.0.0.1only and is never reachable from the network. But any local process can connect to it and drive an agent tab in your browser, in your session context. This is the same trust model as Chrome's own--remote-debugging-port, narrowed: by default a connector gets a fresh blank tab, not your tabs. - What isolation does and doesn't protect.
AGENT_OWN_TABkeeps clients from seeing or touching your existing tabs. It does not sandbox the session: an agent tab shares your profile's cookies, so a malicious local client could open a site you're logged into in its own tab. If your threat model includes hostile local processes, don't run this (or any CDP endpoint). CLAIM_URLis the deliberate escape hatch. It grants one existing tab, matched by URL substring, to the client. Set it per-invocation, not permanently.- The extension enforces its own guards independent of the relay:
chrome://and extension pages can't be attached, and close requests for non-agent tabs are refused. - Logs stay local. The relay writes
relay/relay.log(tab titles/URLs it touched, debug breadcrumbs). It's gitignored; treat it as sensitive.
Three end-to-end suites, each on an isolated port with a throwaway Chrome-for-Testing profile, so they never touch your real browser or a live relay:
node test/e2e.mjs # navigate + isolation: agent lands on its own tab, user tabs untouched
node test/e2e-createtarget.mjs # Target.createTarget path (agent-browser tab new)
node test/e2e-click.mjs # synthetic mouse clicks land on backgrounded tabsThey need Chrome for Testing (agent-browser install, or point CHROME_BIN at any CfT binary) and agent-browser on PATH (override with AGENT_BROWSER).
- macOS (launchd, TCC handling, and the wrapper are Mac-specific; the relay and extension themselves are portable)
- Node 18+ (only runtime dependency:
ws) - Chrome or Chromium with MV3 support
MIT