Skip to content

About

Drive your real Chrome from any CLI agent: parked tab group, numbered snapshots, click/type/scroll/screenshot over localhost.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

17 Commits

Folders and files

Repository files navigation

Browser Bridge

Your own Chrome, driven by a terminal agent.
Real profile, real logins, a tab group you can watch and undo.

Discord Ko-fi ETH License

Browser Bridge: a purple bridge between a terminal and a browser window

A Chromium extension plus a local daemon that lets a terminal AI agent (Claude Code, opencode, or anything that can run a shell command) drive your real browser: open a tab group, read pages, click, type, scroll, screenshot.

It is the same idea as Claude in Chrome, but the agent talks to Chrome over chromectl instead of a chat sidebar, so any CLI agent can use it.

Repository: github.com/ghostyshell/browser-bridge

  agent (you / Claude Code / opencode)
      │  chromectl snapshot | click | type ...
      ▼
  daemon  127.0.0.1:8737          ~/.config/browser-bridge/
      │  websocket (ping every 20s keeps the MV3 worker alive)
      ▼
  Chrome extension  ──►  your profile, your logins, your session
      │
      ▼
  tab group "Agent"   ──►  only tabs the agent created or adopted

Why an extension instead of DevTools Protocol

Chromium 136+ refuses --remote-debugging-port on your default profile, so CDP cannot reach the browser you actually use. An extension runs inside that profile with your sessions, and a tiny native-free websocket relay gives the agent access. Nothing is exposed to the network: the daemon binds 127.0.0.1 only.

What it can do

Action Command
Open a page in the agent tab group chromectl open <url>
Take over a tab you already have open chromectl adopt --tab <id>
Snapshot the page as numbered, actionable elements chromectl snapshot
Read visible text / links chromectl text, chromectl links
Query a selector structurally chromectl query "span.titleline > a"
Click chromectl click 12
Type, optionally submit chromectl type 3 "hello" --submit
Keys chromectl key Enter, chromectl key Meta+a
Scroll chromectl scroll down, chromectl scroll --ref 12
Wait for something chromectl wait css=#results --timeout 8000
Screenshot chromectl screenshot
Run JS in the page chromectl eval "document.title"

Full reference: chromectl help.

Install

cd ~/Code/browser-bridge
./install.sh

That installs dependencies, compiles the TypeScript, links chromectl into ~/.local/bin, links the skill into ~/.claude/skills and ~/.config/opencode/skills, and starts the daemon.

Then load the extension once:

  1. Open chrome://extensions
  2. Turn on Developer mode (top right)
  3. Load unpacked → select ~/Code/browser-bridge/extension
  4. Pin "Browser Bridge" to the toolbar

The badge turns into a purple dot when the agent is connected.

Background by default

Opening a page does not steal your focus. Tabs land in the Agent group and the agent reads, clicks and types in them while you keep working in your own tab:

chromectl open https://example.com        # stays in the background
chromectl activate                        # bring the agent's tab to the front

Nothing is borrowed and nothing is raised. A tab that is not active in its window gets no animation frames, no trusted input and reports itself hidden, so the extension attaches the debugger to it and turns on focus emulation (Emulation.setFocusEmulationEnabled plus Page.setWebLifecycleState active) for as long as it is working there. The page then behaves as if it were focused while your window stays behind, and every click, keystroke and paste is dispatched as real Input.* events (isTrusted: true), which is what strict editors, CSP pages and Material widgets accept. This is the same mechanism Playwright and browser-use use. chromectl activate is the only thing that brings a tab or window forward.

Debugger attachment shows Chromium's "is debugging this browser" notice while it is attached; the session detaches after 20 seconds idle.

The same rule holds for everything the agent does, the way Claude in Chrome works in its own tab group while you keep browsing:

  • chromectl open creates the tab behind your current one (--active is the opt-in), and a late activation Chrome sneaks in when the tab joins the group is undone.
  • A tab the agent's page spawns (target=_blank, window.open) lands in the Agent group and your tab is re-activated within the same tick.
  • chromectl screenshot captures through the debugger, so a background tab is photographed as it is right now, not as a stale frame, and nothing is brought forward. Only if the debugger cannot attach (DevTools open on that tab, a restricted page) does it fall back to captureVisibleTab, which needs the tab in front for a moment; the CLI prints a note when that happened.

scripts/smoke-test.sh runs the whole flow against a throwaway Chrome profile and asserts that your active tab never changes. Unpacked extensions share one id across every browser that loaded the same folder, so the test pins the daemon to the throwaway browser (POST /pin) and proves the pin with a nonce before it touches anything.

Permission model

The agent can only touch tabs in its own Agent tab group, and only on origins you approved. Approval happens in the extension popup, which the agent cannot click:

  1. The agent asks for a site (for example chromectl open github.com)
  2. The command fails with origin_not_allowed and the popup gets a request card
  3. You press Allow this site (or Block)
  4. The agent retries

Allow every site skips the prompt. Use it only when you are watching.

The gate is enforced in the extension, not the CLI, so an agent cannot grant itself access by running another shell command.

The snapshot

chromectl snapshot turns the page into lines an LLM can reason about:

Bridge fixture
http://127.0.0.1:8899/fixture.html
viewport 1200x728 @ 0% (scrollY 0/728)  ·  9 elements  ·  1 forms, 0 iframes

[0] heading1 "Bridge fixture"
[1] link "Go to page two" href=http://127.0.0.1:8899/page2.html
[2] textbox "Your name"
[3] combobox "Plan" value="Free"
[4] checkbox "I agree to the terms" unchecked
[5] button "Say hello"
[6] button "Reveal (slow)"
[7] textbox "Edit me" value="Edit me"
[8] button "Clicked 0"

Refs are written onto the DOM as data-bb-ref when you snapshot. They are valid until the next snapshot or a navigation, which is why the agent should snapshot again after anything that changes the page.

Commands worth knowing

chromectl status                  # daemon, extension, agent tabs, approvals
chromectl tabs                    # agent tabs (--all for every tab)
chromectl policy                  # what is approved
chromectl request https://x.com   # ask for a site without running an action
chromectl reset                   # close every agent tab
chromectl daemon logs 60          # last 60 daemon log lines
chromectl daemon stop             # hard stop

Flags: --tab N to target a specific agent tab, --json for raw output.

Limits (by design)

  • Screenshots are the visible viewport, not full page.
  • Cross-origin iframes are not traversed; same-page shadow DOM is not traversed.
  • Synthetic keys cannot trigger native browser shortcuts. Meta/Ctrl+A, Tab, Enter and Escape are handled explicitly.
  • chromectl eval needs eval to be allowed by the site. Sites with a strict Content-Security-Policy (most production apps) block it, and you get eval_blocked; use snapshot, query, text or find there.
  • alert() / confirm() block the page and will time out the command. Close the dialog in the browser.
  • File upload pickers, native downloads and permission prompts need a human.
  • Chrome pages (chrome://, the Web Store, PDF viewer) cannot be scripted.

Troubleshooting

Symptom Fix
extension_not_connected Chrome closed, or the extension is disabled. Check the popup badge.
Badge is empty Daemon not running: chromectl daemon start.
origin_not_allowed Approve the site in the popup.
not_agent_tab The tab is outside the group. Use chromectl adopt --tab N if that is intended.
restricted_page Chrome will not script that URL. Use a normal http(s) page.
Commands hang chromectl daemon logs and check the extension is enabled.

Logs: ~/.config/browser-bridge/daemon.log. Screenshots land in ~/.config/browser-bridge/shots/.

Layout

src/daemon/      relay: /rpc over loopback, bearer token, ws to the extension
src/cli/         chromectl
src/extension/   service worker, content script, popup
src/shared/      payload types shared by all three
scripts/build.mjs  esbuild -> dist/ and extension/*.js

Sources are TypeScript, strict mode. npm run build compiles; the compiled extension/*.js and dist/ are gitignored because they are build output.

Tests

npm run smoke runs the whole suite. scripts/dev-chrome.sh launches a throwaway Chrome profile with the extension installed over CDP, so you can exercise the whole path without touching your real browser:

scripts/dev-chrome.sh
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8899/fixture.html  # needs a server
chromectl open http://127.0.0.1:8899/fixture.html

The extension is installed via Extensions.loadUnpacked because Chromium 137+ ignores --load-extension.

Works in Edge and other Chromium browsers as well as Chrome; the browser in use is reported in chromectl status.

Support

MIT, no paid tier, no telemetry, no account.

Attribution

Written by ghost. The extension, daemon and CLI are original work; the RPC shape and snapshot format were informed by Claude in Chrome's interaction model. Icons are generated by scripts/make-icons.py.

If you build on it, keep the MIT notice and link back here.

Uninstall

chromectl daemon stop
rm ~/.local/bin/chromectl
rm ~/.claude/skills/browser-control ~/.config/opencode/skills/browser-control
rm -rf ~/.config/browser-bridge

Then remove the extension from chrome://extensions.

About

Drive your real Chrome from any CLI agent: parked tab group, numbered snapshots, click/type/scroll/screenshot over localhost.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages