Highlights
.adopt, incic shell: use a Chrome tab you already have open. The bridge sees only tabs inside its own tab group and has no tool that moves one in, so you move it through Chrome's own tab context menu and.adoptmakes that safe to detect. It holds a group open, waits as long as you need, and identifies your tab as one new live id confirmed across two consecutive polls, because a single reading of a set difference can catch the browser mid-move.- Detection is not adoption. A candidate has to answer a read-only call before it counts. A blank tab appearing in the group is a perfectly stable single addition and cannot be driven at all, which is exactly the false positive the first version of this reported.
- Identity is the live id and nothing else. Titles and URLs are shown to you and never used to decide which tab was meant, and the URL is shown as origin and path only.
- Move two tabs in and it says so and waits for you to take the extras out. A stray blank tab beside the real one is ignored rather than treated as ambiguity. Ctrl-C cancels and leaves the shell running; closing input cancels too, since nobody is left to move a tab.
- The one-shot contract is unchanged.
cic listandcic callkeep their stdout, stderr, exit codes and--jsonshape.
Install
npm install -g claude-in-chrome-cliPlugin users need nothing new: the CLI ships bundled at node ${CLAUDE_PLUGIN_ROOT}/bin/cic.js.
Groups are reused now, not recreated
A tab group dies with its last tab, so closing the tab cic opened destroyed the group and the next run opened a fresh one. Six create-and-close cycles left six distinct group tokens in Chrome's session file, which is why identically named Claude (MCP) pills accumulated in the tab strip.
Worse, an emptied group cannot be found again by anything. Chromium writes no group-lifecycle command: group existence is only ever implied by a live tab's membership record, established by diffing Sessions/Session_* around a known create and close, which produced no group command at all. So the leftover is invisible to the bridge and to disk alike, and only a human can remove it.
A group cic opened is therefore kept alive with one tab and reused. Measured after the change: three runs, one group, one new token, and the same tabGroupId across three separate processes. The cost is stated rather than hidden, because it is a genuine trade: one blank tab and one pill persist for good, since emptying them is the act that strands them.
Two bugs in 0.7.0, fixed
cic with-tab did not work when no tab group existed, which is the ordinary case from a clean browser: tabs_create_mcp refuses until a group exists and says to use tabs_context_mcp, and withTab called create first. Every live check in 0.7.0 passed because a group happened to exist from earlier work, and the offline stub answered create unconditionally, so five hundred checks blessed a path that could not run.
Cleanup could also orphan tabs. Closing a group's first tab makes the bridge lose the whole group, and everything else in it becomes unreachable from any session, so cleanup now reads the group before closing and refuses when its tab is the first with others behind it. An unreadable group refuses as well: the first attempt at that guard collapsed "cannot read" into "empty group" and so failed open, which is precisely the wrong direction.
Ctrl-C worked in tests and not at a keyboard
Reviewing this before merge caught the headline behaviour holding in the harness and failing for a human. On a terminal, readline intercepts Ctrl-C and emits the signal event on the interface; no process-level signal ever fires. Only a process handler was registered, so every piped test passed, because kill() delivers a real signal, and a person got nothing. Both paths are handled now, and the keypress path is tested under a real pty, because a piped test structurally cannot reach it.
What did not ship
The unattended fallback that opens the same page in a managed tab. The roadmap section promised it in its title and only the adoption half happened, so the title was corrected rather than the record of it. On naming, open-copy beat reopen-tab and clone-tab: "clone" promises DOM state, form contents, scroll position and live connections that a fresh navigation does not preserve.
Upstream, and a new repository
Tab groups turned out to be extension-only surface, verified by closing every alternative: page JavaScript sees a chrome object holding only loadTimes, csi, app; CDP has no tab-group surface across its 51 domains; Chrome's AppleScript dictionary has no group vocabulary. The measurements are attached to anthropics/claude-code#75901, which had reported the accumulation without the mechanism.
A helper extension now lives in its own repository, because something asking for tabs should be auditable on its own terms and publishes on the Chrome Web Store's clock rather than this project's. Nothing in cic depends on it.
Verified
518 checks across eight offline suites, no Chrome and no extension required, and all eight matrix jobs green on Ubuntu, macOS and Windows for Node 22 and 24. Coverage 98.4% of statements, 89.78% of branches and 100% of 97 functions, measured with no browser profile present because that is the lower of the two environments and what CI has. The premise was established against a live bridge before any of it was written: a manually moved tab is seen on the same session and can be driven, removal is seen with the anchor surviving and still drivable, and the first-tab behaviour was characterised in both directions. Installed from the registry, cic --version reports 0.8.0, .adopt adopts a real tab and hides its query string, and the anchor rule fires correctly in both of its branches. Provenance attached and verified with npm audit signatures.
Full changelog: v0.7.0...v0.8.0