Your own Chrome, driven by a terminal agent.
Real profile, real logins, a tab group you can watch and undo.
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
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.
| 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.
cd ~/Code/browser-bridge
./install.shThat 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:
- Open
chrome://extensions - Turn on Developer mode (top right)
- Load unpacked → select
~/Code/browser-bridge/extension - Pin "Browser Bridge" to the toolbar
The badge turns into a purple dot when the agent is connected.
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 frontNothing 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 opencreates the tab behind your current one (--activeis 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 screenshotcaptures 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 tocaptureVisibleTab, 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.
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:
- The agent asks for a site (for example
chromectl open github.com) - The command fails with
origin_not_allowedand the popup gets a request card - You press Allow this site (or Block)
- 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.
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.
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 stopFlags: --tab N to target a specific agent tab, --json for raw output.
- 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,EnterandEscapeare handled explicitly. chromectl evalneedsevalto be allowed by the site. Sites with a strict Content-Security-Policy (most production apps) block it, and you geteval_blocked; usesnapshot,query,textorfindthere.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.
| 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/.
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.
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.htmlThe 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.
MIT, no paid tier, no telemetry, no account.
- Discord: https://discord.gg/bUadyRwkJS
- Ko-fi: https://ko-fi.com/ghosty99
- Ethereum:
0xbBef115aD853236a350EaF04777a2AdB2e7c0317(Etherscan)
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.
chromectl daemon stop
rm ~/.local/bin/chromectl
rm ~/.claude/skills/browser-control ~/.config/opencode/skills/browser-control
rm -rf ~/.config/browser-bridgeThen remove the extension from chrome://extensions.