Agent Tab Bridge is a local-first Chromium extension for explicitly bridging an opted-in browser tab to local agent tooling.
It is designed for workflows where a user is already authenticated in their normal browser profile and wants to temporarily allow local tools to inspect or execute JavaScript in one tab. Access is opt-in per tab from the extension popup.
- Disabled by default.
- Enablement is per active tab.
- An enabled tab stays bridged across reloads and navigation until the user disables it, closes it, or Chrome refuses extension access to the destination.
- The extension requests access to all URLs so the explicit per-tab opt-in works on any page Chrome permits. The popup warns about this access before every enablement.
- The bridge URL must be localhost:
http://127.0.0.1:<port>orhttp://localhost:<port>. - The bridge serves CORS headers only to origins you list in
allowedOrigins(empty by default), so arbitrary websites cannot queue commands. - Set a
tokeninconfig.jsonto requireAuthorization: Bearer <token>on every bridge request except/health. With no token, the bridge runs unauthenticated and warns on startup. - Commands and results stay on the local machine unless you explicitly forward them elsewhere.
config.json is the single source of truth, shared by the local bridge and the extension. It is gitignored (it may hold a token); config.example.json is the committed template.
npm run setup # create config.json from config.example.json
# edit config.json as needed| Field | Used by | Purpose |
|---|---|---|
host, port |
bridge | Address the server binds to |
bridgeUrl |
extension, CLI | Localhost URL the extension and CLI dial |
allowedOrigins |
bridge | Origins allowed to make cross-origin (browser) requests |
token |
bridge, extension, CLI | Shared bearer token; empty disables auth |
overrides |
extension, bridge | Map production asset URLs to local files (see Script overrides) |
manifest.json declares <all_urls> because Manifest V3 reads host permissions statically and the bridge must be able to re-inject its keepalive after cross-origin navigation. Chrome still blocks extension scripting on protected surfaces such as chrome:// pages.
Older config.json files may still contain an allowlist field. It is ignored and can be removed.
- Run
npm run setupto createconfig.json. - Open
chrome://extensions. - Enable Developer mode.
- Click Load unpacked.
- Select this repository folder.
- Adjust
config.jsonas needed, then reload the extension. The options page shows the active config read-only.
The bundled skill teaches Claude Code and Codex to discover and use the bridge. From the repository root, install it for both agents with:
for skills_root in "$HOME/.claude/skills" "$HOME/.agents/skills"; do
mkdir -p "$skills_root"
destination="$skills_root/agent-tab-bridge"
if [ ! -e "$destination" ] && [ ! -L "$destination" ]; then
ln -s "$PWD/.claude/skills/agent-tab-bridge" "$destination"
fi
doneThe command leaves an existing installation untouched. Restart the agent session after installing the skill.
npm run bridgeThe bridge listens on http://127.0.0.1:3107 by default.
Instead of starting the bridge from a terminal, build the menu bar app once and run it:
npm run menubar # builds menubar/AgentTabBridge.app
open menubar/AgentTabBridge.appThe menu bar item shows whether the bridge is running and how many tabs are bridged, lists those tabs, starts/stops the bridge, and has a Launch at login toggle. It manages bridge/server.js as a child process (logs to ~/Library/Logs/AgentTabBridge.log) and won't spawn a second server if one is already running. Rebuild with npm run menubar after pulling changes; the repo and node paths are baked in at build time. Enabling Launch at login the first time may need approval in System Settings → General → Login Items.
- Open a webpage in Chrome.
- Click the Agent Tab Bridge extension button.
- Click Enable for this tab.
- Send commands from the local CLI:
node bridge/client.js ping
node bridge/client.js snapshot
node bridge/client.js eval "document.title"Screenshots return a PNG data URL:
node bridge/client.js screenshotYou can enable more than one tab; the toolbar badge shows how many are bridged and the popup lists them. Commands run in a single tab, so when several are enabled, name one with --tab <id>:
node bridge/client.js tabs # list enabled tabs and their ids
node bridge/client.js --tab 433920089 snapshotWith exactly one tab enabled the commands above work without --tab. With more than one and no --tab, the CLI stops and lists the tabs.
Queue a command:
curl -sS http://127.0.0.1:3107/api/command \
-H 'content-type: application/json' \
-d '{"type":"eval","code":"document.title"}'Read bridge state:
curl -sS http://127.0.0.1:3107/api/clients
curl -sS http://127.0.0.1:3107/api/resultsping: confirms the enabled tab is connected.snapshot: returns URL, title, ready state, selected text, visible text, and active element summary.screenshot: returns a visible-tab PNG data URL.storage: dumps localStorage/sessionStorage keys with lengths and decodes any JWTs found in the values. PassincludeValues: true(CLI:--values) to include raw values (truncated to 4000 chars). Works on CSP-restricted pages because it injects a function, not an eval'd string.eval: executes JavaScript in the enabled tab. The default execution world isMAIN; passworld: "ISOLATED"to run in the extension isolated world. On pages whose CSP forbidsunsafe-eval(both worlds fail with anEvalError), the command automatically retries through the debugger API (CDPRuntime.evaluate), which is exempt from page CSP; passbackend: "cdp"(CLI:--cdp) to force that path. Chrome shows an "is debugging this browser" infobar for the duration of a CDP eval.
Eval results must be serializable. On the default path DOM elements are summarized instead of returned directly; on the CDP path results must be JSON-serializable (DOM nodes/functions are rejected by the protocol).
Swap a production script for a local file so you can prototype changes against a real, authenticated environment. The extension redirects the matching request (via declarativeNetRequest) to the bridge, which serves your file from overrides/.
-
Put your file in
overrides/(e.g.overrides/flow-remoteEntry.js). -
Add a mapping to
config.jsonand reload the extension:"overrides": [ { "match": "remoteEntry.js", "file": "flow-remoteEntry.js" } ]
-
Enable the target tab from the popup, then reload the page. The popup warns while overrides are active.
matchis adeclarativeNetRequesturlFilter(substring /*) tested against the request URL.- Overrides are scoped to enabled tabs and strip the page's
content-security-policyon those tabs, because a strictscript-srcwould otherwise block the localhost script. This is a real, temporary security relaxation — keep overrides empty when you're not prototyping. - Files are served with
Cache-Control: no-store, so edits apply on the next reload. No page reload happens automatically. - No SRI on UiPath bundles today, so swapped scripts aren't rejected by integrity checks (verified against captured traffic).