Share Chrome tabs with AI agents
翻譯準備中
外掛JavaScript · CSSGitHub @functicons自行安裝營運者未確認
試用過了嗎? 點一下就好,不需要登入。
尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
安裝型專案無需部署網域也可驗證。個人儲存庫可透過 GitHub 登入驗證,組織儲存庫可使用上方的儲存庫驗證檔案。
製作者的介紹僅有上面的一句話。 更多詳情請閱讀儲存庫 README。 儲存庫 README ↗
正在載入完整 README。
檢視完整 README ↗Give your AI agent eyes and hands on your browser — one tab at a time.
A Chrome extension + CLI + AI skill that lets you selectively share browser tabs with AI agents (like Claude Code) for real-time inspection, interaction, and debugging. Install the skill, share a tab, and let your AI agent take it from there.
run), returning only the result the agent needsconsole.log, errors, warnings in real time, with history bufferCmd+Shift+S)There are other ways to connect AI agents to Chrome. Here's how they compare:
| Chrome Tab Control | chrome-cdp-skill | Chrome DevTools MCP | Claude in Chrome | |
|---|---|---|---|---|
| Control existing tabs | ✅ Shared tabs only | ✅ All tabs | ✅ All tabs | ❌ Opens new tabs |
| Per-tab access control | ✅ User chooses | ❌ All tabs exposed | ❌ All tabs exposed | N/A |
No --remote-debugging-port | ✅ Not needed | ❌ Required | ❌ Required | ✅ Not needed |
| Annotation | ✅ Draw on page | ❌ No | ❌ No | ❌ No |
| Console/network monitoring | ✅ Real-time streaming | ❌ No | ✅ Via MCP tools | ❌ No |
| Works with any AI agent | ✅ Any CLI-capable agent | ✅ Any CLI-capable agent | ✅ Any MCP client | ❌ Claude only |
| npm dependencies | None | None | Yes | N/A (extension) |
| Setup complexity | Extension + one command | Enable debug port | Debug port + npm install | Extension only |
--remote-debugging-portChrome DevTools MCP and chrome-cdp-skill require Chrome to be launched with a special flag or remote debugging enabled. This exposes every tab — your banking site, your email, your private messages — to any process that connects to the debug port. It's a security risk that most developers are uncomfortable with.
Claude in Chrome opens fresh tabs for tasks. But often you want your AI agent to work with a page you're already looking at — debug a form that's misbehaving, extract data from a dashboard, or monitor network requests while you interact with the page.
With every other tool, you have to describe what you're looking at in words: "the button in the top-right corner", "the third row in the table", "that error message". It's slow, ambiguous, and frustrating — like a remote pair programming session without screen sharing.
Share only the tabs you want. Draw directly on the page — circle a bug, arrow to a button, type "fix this" next to a broken layout. Your agent sees the annotations in screenshots and can programmatically query what you're pointing at:
tc annotations <tab>
[rect] red at=(56,120) size=733x92
-> span "Minor UI changes for noise filter monitoring"
-> a[href="/dagang-w"] "dagang-w"
No debug port, no new tabs, no describing things in words — just point at your screen and say "help me with this."
┌─────────────────┐
│ AI Agent │
│ (Claude Code) │
└────────┬────────┘
│ reads SKILL.md
▼
┌──────────────┐ ┌───────────────────┐ ┌──────────────────┐
│ CLI Tool │ │ Tab Proxy │ │ Chrome Extension │
│ │◄───►│ │◄───►│ │
│ tab-control │Unix │ tab-proxy.mjs │stdio│ background.js │
│ -cli.mjs │sock │ │ │ │
└──────────────┘ └───────────────────┘ └──────────────────┘
│
▼
chrome.debugger API
│
▼
┌─────────────-┐
│ Chrome │
│ (shared tab)│
└─────────────-┘
chrome.debugger to that tabK7QM-4XRT-AB12) and copies it to your clipboardchrome://extensionsextension/ directorymake install-tab-proxy
make install-skill
make check # dependencies (Node, Chrome, tab proxy, skill)
make test # unit tests for argument parsing (no browser needed)
Two ways to share a tab:
The tab title will show a flashing ⚪🟡 indicator confirming it's shared.
Shared tabs are never listed. Each shared tab gets a random tab handle such as K7QM-4XRT-AB12, and the handle is the only way to reach it:
<tab>: tc snap K7QM-4XRT-AB12. tc info <tab> prints the tab's title and URL, so the agent can check it has the right tab.The handle lives only in the memory of the extension and the tab proxy; Chrome Tab Control never writes it to disk. It has 60 random bits, so it cannot practically be guessed. It keeps out every process you did not give it to, including other agent sessions. It does not stop malware already running as your user, which could read the handle wherever you paste it (e.g. an agent's session log).
Draw on any tab to visually communicate with the AI agent. Annotations work independently of sharing — no need to share a tab first. On shared tabs, annotations appear in CDP screenshots and can be queried via tc annotations.
Activate annotation mode (3 ways):
Cmd+Shift+A (Mac) / Ctrl+Shift+A (Windows/Linux)Drawing tools (floating toolbar or keyboard shortcuts):
| Tool | Toolbar | Key | How to draw |
|---|---|---|---|
| Rectangle | □ | R | Click and drag to define bounding box |
| Circle | ○ | C | Click and drag to define bounding box |
| Arrow | → | A | Click and drag from start to end |
| Text | T | T | Click to place, type text, Enter to confirm |
Moving annotations: Click any annotation to select it (blue dashed outline), then drag to move. Press Delete/Backspace to remove the selected annotation. Works in any mode — no need to switch tools.
Colors: Click the color dot in the toolbar to open a picker with 6 colors (red, blue, green, orange, purple, black). Or press number keys 1–6.
Other controls:
| Action | Toolbar | Key |
|---|---|---|
| Undo | ↩ | Cmd+Z / Ctrl+Z |
| Clear all | ✕ | — |
| Minimize toolbar | ▾ | M |
| Exit annotation | ✓ | Esc (press twice: first deselects tool, second exits) |
The toolbar is draggable (grab the ⠿ handle) and minimizable. Annotations persist after exiting — re-enter annotation mode to edit them.
Pin specific DOM elements so the AI agent knows exactly which elements you're referring to. More precise than annotations for targeting individual elements.
Activate (2 ways):
Cmd+Shift+E (Mac) / Ctrl+Shift+E (Windows/Linux)Usage:
Esc to exit — pins stay visible on the pageThe agent can query pinned elements via tc pins <tab> to get exact selectors, positions, and text content.
Copy all visible text from a selected area — useful when elements are not easily selectable.
Activate (2 ways):
Cmd+Shift+C (Mac) / Ctrl+Shift+C (Windows/Linux)Usage:
Capture a selected area of any tab as a PNG image.
Activate (3 ways):
Cmd+Shift+S (Mac) / Ctrl+Shift+S (Windows/Linux)Usage:
Esc to dismissOnce the skill is installed (make install-skill), share a tab and give Claude Code its tab handle along with what you want. Examples:
"Summarize the page I'm looking at"
"Extract all the links from this page"
"Check the console for errors"
"Monitor network requests while I click around"
"Take a screenshot and describe the layout"
"Click the login button and fill in the form"
"What API calls is this page making?"
Claude Code will use the handle with the CLI to interact with the tab. If you forget the handle, it will ask for it.
When your agent runs on another machine (over ssh, or a remote terminal session) but Chrome runs on your laptop, link the two:
user@host, or a ~/.ssh/config alias) and click Link. The row turns linked within a few seconds.The remote agent uses the handle exactly as it would locally (tc info <tab>). Each host's row in the popup shows its state (connecting, linked, retrying with the reason) and its latest attaches and refusals, so you can see what reached your tabs. Unlink cuts the host off. Hosts you linked before stay listed as not linked, for one click to relink. Nothing links on its own, not even when Chrome starts.
A host can reach your tabs only while it is linked, a tab only with Remote ticked, and only with that tab's handle. Ticking Remote opens a tab to every linked host, but only a host whose agent has the handle can attach to it.
Links from the popup are per Chrome profile. Each profile runs its own copy of the extension and its own tab proxy, so a link started in one profile's popup reaches only that profile's tabs and is listed only in that popup. To use tabs from two profiles on the same host, link it from both. Links from the popup run inside the tab proxy, so they end when Chrome quits.
To link from a terminal instead, run tc remote devbox on the Chrome machine. A terminal link reaches the tabs of every profile (still only those with Remote ticked, and only by handle). It prints each attach and refusal, and Ctrl-C unlinks. Terminal links are not listed in any popup.
How it works: a link keeps an ssh -R session open that forwards ~/.chrome-tab-control/sock/remote-<laptop>.sock on the remote host to a local bridge. The bridge marks each attach as remote and hands it to the local tab proxy, which admits it only for tabs with Remote ticked. Turning Remote off disconnects remote agents from that tab at once and keeps local ones. If the SSH connection drops, the link reconnects with backoff. Restarting Chrome does not break it.
Requirements:
BatchMode=yes, so it cannot answer password prompts.# Alias for convenience
alias tc='node skills/chrome-tab-control/scripts/tab-control-cli.mjs'
# <tab> is the tab handle (copied to your clipboard when you share), e.g. K7QM-4XRT-AB12
# Show the tab's title and URL
tc info <tab>
# Take a screenshot
tc shot <tab> # saves to /tmp/screenshot.png
tc shot <tab> --highlight "button.primary" # highlight matching elements
# Accessibility tree snapshot (best for AI understanding)
tc snap <tab>
# Evaluate JavaScript
tc eval <tab> "document.title"
# Navigate (same-host only by default)
tc nav <tab> https://example.com
tc nav <tab> https://example.com --watch # monitor console+network during load
tc nav <tab> https://example.com --watch 5 # monitor for 5s after load
tc nav <tab> https://other.com --cross-origin # opt in to cross-host navigation
# Reload the current page (no host change, no URL needed)
tc refresh <tab>
tc refresh <tab> --hard # bypass cache
# Click an element
tc click <tab> "button.submit"
# Click at coordinates (CSS pixels)
tc clickxy <tab> 200 300
# Type text at current focus
tc type <tab> "Hello world"
# Get page HTML
tc html <tab> # full page
tc html <tab> ".sidebar" # specific element
# Monitor console output (real-time)
tc console <tab> 30 # watch for 30 seconds
tc console <tab> --history # show all console output since tab was shared
tc console <tab> --clear # clear console history buffer
# Monitor network requests (real-time)
tc requests <tab> 30
# Monitor both console + network together
tc watch <tab> 60 # great for debugging interactions
# List user-drawn annotations with underlying page elements
tc annotations <tab>
# List user-selected (pinned) elements
tc pins <tab>
# Focus / fill inputs (prefer fill over click+type for text entry)
tc focus <tab> "textarea.prompt"
tc fill <tab> "textarea.prompt" "some text" # focus + select-all + insert + verify
# Press a key (type cannot: it inserts text without key events)
tc key <tab> Enter
tc key <tab> Shift+Enter # newline instead of submit
tc key <tab> Down 3 # repeat a key
# Wait for a page condition (not bound by the 15s per-command timeout)
tc waitfor <tab> "document.querySelector('.results')"
tc waitfor <tab> "window.ready === true" --timeout 120 --interval 500
# Bring tab to foreground (required for gesture-gated actions)
tc front <tab>
# Link this Chrome to a remote host (run on the Chrome machine; Ctrl-C to unlink)
tc remote devbox [--name <label>]
# Send raw CDP command
tc evalraw <tab> "DOM.getDocument" "{}"
# Run many steps in one call: a JS script with `tab` (the commands above) and `sleep`
tc run <tab> script.js [--timeout 300]
tc run <tab> <<'JS'
await tab.fill('input[name=q]', 'hello');
await tab.key('Enter');
await tab.waitfor(() => document.querySelector('.results'));
return await tab.eval(() => document.querySelectorAll('.result').length);
JS
The most expensive gotcha in this tool. Chrome grants transient user activation to CDP-dispatched input only while the tab is in the foreground. Any site action gated behind a user gesture — sending a message, uploading, opening a file picker, clipboard access — is accepted and silently ignored in a background tab. No error, no console message, no network request. It looks exactly like a hang, and is easy to misdiagnose as rate limiting or an expired session.
tc eval <tab> "navigator.userActivation.isActive" # false right after a click => this bug
tc front <tab> # bring it forward, then retry
tc front switches the user's visible tab, so reach for it only when the action
genuinely needs a gesture. It exits non-zero if the tab still is not visible
afterwards (a minimised window, or another desktop) rather than reporting a
success the page will not honour.
innerText reports only rendered text. Shared tabs are usually backgrounded,
where Chrome throttles animations, so on any site that animates content in,
innerText lags or stays empty while textContent is unaffected. Measured on one
chat UI: textContent held the complete 242-character answer at 3s, where
innerText showed 67 characters and then froze at 86 once the tab went hidden.
The failure is quiet — you get a truncated answer rather than an error. Extract
with textContent and rebuild line breaks from block elements if structure matters.
chrome-tab-control/
├── README.md
├── LICENSE
├── Makefile
├── extension/ # Chrome extension (user installs into Chrome)
│ ├── manifest.json
│ ├── background.js
│ ├── popup.html / popup.css / popup.js
│ ├── offscreen.html / offscreen.js # Hidden page that copies the tab handle to the clipboard
│ ├── annotate.js # Annotation overlay (injected on demand)
│ ├── screenshot-select.js # Screenshot area selector (injected on demand)
│ └── icons/
├── scripts/ # Setup & dev tools
│ ├── install-tab-proxy.sh # Tab proxy setup
│ ├── install-skill.sh # Install as Claude Code skill
│ ├── check.sh # Verify dependencies
│ └── gen-icons.mjs # Icon generator
├── tests/ # Unit tests (node --test, no deps)
│ ├── cli.test.mjs
│ ├── proxy.test.mjs # Proxy access control (spawns the real proxy)
│ ├── remote.test.mjs # Remote link: bridge, ssh supervisor (fake ssh)
│ ├── links.test.mjs # Links managed from the popup, via the proxy
│ ├── helpers.mjs # Shared harness: fake extension, socket client
│ └── fixtures/fake-ssh.mjs # ssh stand-in with a local "remote host"
└── skills/chrome-tab-control/ # Skill directory (what AI agents use)
├── SKILL.md # Skill manifest
└── scripts/
├── tab-control-cli.mjs # CLI tool (all commands)
├── tab-proxy.mjs # Tab proxy (native messaging bridge)
├── remote-link.mjs # Remote link: bridge + ssh -R supervisor (`remote` command)
├── link-manager.mjs # Links the tab proxy runs for the popup
├── protocol.mjs # Paths and auth constants shared by CLI and proxy
└── tab-proxy-wrapper.sh # Node.js path resolver for nvm/volta/fnm
CLI → Agent: Unix socket (NDJSON) → tab-proxy.mjs → stdin/stdout (native messaging) → extension → chrome.debugger.sendCommand() → Chrome
Remote CLI → Chrome: remote-.sock on the remote host → ssh -R → bridge-.sock (remote-link.mjs; one per host and name) → proxy-.sock → … as above
Chrome → CLI (events): Chrome → chrome.debugger.onEvent → extension → native messaging → tab-proxy.mjs → sends to the sockets attached to that tab
--remote-debugging-port — the extension uses chrome.debugger API, which is scoped to individual tabs~/.chrome-tab-control/sock/proxy-<pid>.sock (one proxy per Chrome profile). A client attaches to a tab by sending its handle, and the CLI tries each proxy socket until one accepts it. Nothing lists the shared tabsssh -R session that you started. The forwarded socket is named remote-*.sock so a tab proxy on the remote host never mistakes it for one of its own stale proxy-<pid>.sock files. sshd does not delete a forwarded socket when the link closes, so the link removes it before reconnectingScreenshots are captured at native device resolution:
CSS pixels = screenshot pixels / DPR
The shot command prints the DPR and conversion formula. For Retina displays (DPR=2), divide screenshot coordinates by 2 for clickxy.
chmod 600 inside ~/.chrome-tab-control/sock/, which is 0700 and must be a real directory owned by you (the proxy refuses to start otherwise), so other accounts cannot reach or plant socketstc remote link that you started. The bridge, not the remote client, marks a request as remote, so a remote agent cannot pass itself off as local. The link carries only attach requests to the tab proxy, which checks the handle exactly as for a local clientMIT