Releases: hamzahamidi/claude-in-chrome-cli
Release list
v0.8.0
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
v0.7.0
Highlights
cic with-tab <url> <tool> [args]makes a tab, navigates it, calls one tool against it and closes it. That is the shape most one-off scripts were assembling by hand out oftabs_create_mcp, a copied id and atabs_close_mcpthey had to remember. It fills intabIdand nothing else, so passing your own is a usage error rather than an argument that would be silently overwritten.- After an unknown outcome the tab is deliberately left open, and its id is printed. The request reached the browser, nobody knows whether it acted, and closing could discard a half-finished action along with the only evidence of it. An ordinary tool error is the opposite case: the browser answered, so the tab is closed as usual. A cleanup that fails is reported either way, beside a success or appended to a failure.
--output <path>writes the image in a result to a file, oncic callandcic with-tab. Until now a non-text part printed as[image]and the bytes were unreachable from a shell, which made a screenshot the one thing the CLI could ask for and not deliver. It looks for image content rather than for a tool name, socic callstill owns no schema and this works for anything returning a picture.cic tabsanswers what is open by reading Chrome and Chromium session files from disk: no bridge, no handshake, no extension, no tab group. Redaction applies to--jsonas well as to the text, since choosing machine-readable output is not a request to turn the safe default off.- 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.
A file that exists holds a whole image
Refusing a bad result is the point of --output, and the first implementation of it did not do what it claimed. Valid base64 carrying the right magic bytes says nothing about where a file ends: base64 cut at a length divisible by four is still valid base64, and a truncated PNG still starts with the PNG signature, so a 30-byte fragment of a 70-byte image passed every check and was written over the destination.
Review caught it, and the fixtures were what had hidden it. Three of the four formats were a magic header followed by filler, which is exactly what an incomplete transfer produces, so the suite was asserting that a fragment is a valid image.
Every format is now asked where it ends: PNG chunk lengths walked to IEND, the JPEG end-of-image marker, the GIF trailer, and a WebP whose declared RIFF size matches the bytes present. The write still goes to a temporary file in the destination's directory and is renamed into place, so an existing file survives every refusal.
Refusals exit 64. The browser did what it was asked and the file is what could not be produced, which is neither a tool error nor anything retryable, and the alternative was a sixth code in a contract frozen since 0.4.0.
Two ways to get an exit code wrong
A reply is a JSON-RPC error or a result, and a result may carry isError. The lifecycle read .result without checking .error first, so a create or navigate answering with a JSON-RPC error threw a TypeError that surfaced as exit 3. That is the worst answer available: exit 3 promises the browser cannot have acted and is the only class --retries repeats, while a failed navigate happens after a tab already exists. Both shapes now go through one reader and both are exit 1, carrying the bridge's own message.
Separately, when the bridge creates a tab and its reply does not name the id, a real tab exists that nothing can address. That is exit 2 for the same reason: exit 3 would deny the tab exists and invite a retry that makes a second orphan.
Layering
withTab lives in lib/tab-lifecycle.js, not on BridgeSession. That class was extracted in 0.5.0 as the generic protocol layer, and hanging the helper off it taught it the names tabs_create_mcp, navigate and tabs_close_mcp. It ends this release byte-identical to how it started. The SNSS reader likewise moved to lib/session-tabs.js, so the CLI no longer loads a file that also has a main().
Coverage that does not depend on whose machine it is
collect() reads the real home directory, so its discovery failure paths, a corrupt session file, an encrypted-only profile, an empty Sessions directory, were reached only on a machine with Chrome installed and more than one profile. Coverage was therefore a property of whose laptop ran the suite, and the first floors set here came from a laptop and failed all four POSIX legs. Those paths now run against a synthetic profile tree in a child process.
Verified
437 checks across seven 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.31% of statements and 89.37% of branches with 81 of 81 functions, measured with no browser profile present because that is the lower of the two environments and what CI has; Windows enforces its own 97 and 89 against a measured 97.71 and 89.93. Each review finding was reproduced before it was fixed, and each fix fails its own tests when reverted. The published tarball is 9 files and the plugin cache 11. Installed from the registry, cic --version reports 0.7.0, cic tabs reads 40 tabs, with-tab --output wrote a real screenshot through the live bridge, and the three fixed cases exit 1, 1 and 64 rather than 3, 3 and 0. Provenance attached and verified with npm audit signatures.
Full changelog: v0.6.0...v0.7.0
v0.6.0
Highlights
cic session --jsonlholds one connection open and answers one JSON object per line each way. AtabIdreturned by one call is still valid in the next, which a one-shotcic callcould not offer: it pays for a process, a handshake and an empty tab group every time.- The response record is the frozen 0.4.0 envelope with an id added, rather than a second vocabulary. Success is
{id, exit: 0, result}, failure{id, error: true, kind, exit, message}, so a caller that already parsescic call --jsonneeds no new branches. The id is yours and is echoed back untouched. - Because a persistent process has one exit status and many calls, each record carries its own outcome while the process code describes only the session: 0 clean shutdown, 2 an unknown outcome ended it, 3 it never started.
- An unknown outcome is fatal to the session, deliberately, with no flag to continue past one. The request was sent, nobody knows whether the browser acted, and letting a later call race it is the one thing the exit-code split exists to prevent.
cic shellis the same connection with a prompt, and is deliberately dumb: no variables, captures, aliases, implicit current tab or control flow, because the program driving the JSONL interface already has those.- The one-shot contract is unchanged.
cic listandcic callkeep their stdout, stderr, exit codes and--jsonshape, proven by the 0.5.0 suites passing untouched.
Install
npm install -g claude-in-chrome-cliPlugin users need nothing new: the CLI ships bundled at node ${CLAUDE_PLUGIN_ROOT}/bin/cic.js.
What it looks like
A real session, recorded through a pty. Each reply's tab context is elided, because the bridge appends the title and URL of every open tab to every reply. Worth knowing if you log session output.
Two bugs from building it
Both appear only with piped input, which is how a program will actually drive this. Readline emits every piped line and fires close before the first call has finished, so the shell's trailing prompt() hit a closed interface, threw ERR_USE_AFTER_CLOSE, and that rejection poisoned the queue: every line after the first was silently skipped while the process still exited 0. Separately, .exit did not stop work already queued behind it, for the same reason. Stopping now means stopping, including what is already queued.
Four more from review
A fatal session emitted its record and then did not exit: readline was closed, but stdin had been paused by hand and stayed referenced, so the process waited for whoever was writing to it. Eight seconds against a writer holding the pipe, now one. Every earlier test missed it structurally, because spawnSync closes stdin immediately and the condition never arose.
Backpressure was half-implemented. Reads were paced by the bridge, but records went out through unchecked write() calls, so with stdout unread forty one-megabyte replies were accepted and buffered into 142 MB of resident memory. Each record is now handed over before the next request is taken, and the test asserts that memory does not grow with the queue: twenty requests and two hundred both peak around 64 MB.
Windows gained a coverage floor of its own rather than losing enforcement, and flags a command cannot act on became usage errors. cic list --jsonl, cic session --jsonl --retries 3 and cic shell --json previously exited 0 while quietly doing something other than what was asked; each command now declares what it understands and anything else exits 64.
Stated precisely
Every platform is gated on its own floor. Windows skips the EPIPE pipeline, the detached-descendant test and the resident-memory probe by design, so it enforces 95 statements and 87 branches against a measured 95.75% and 87.24%, where macOS and Linux enforce 96 and 86 against 96.55% and 86.54%. Its branch figure is the higher of the two because the skipped tests take branches out of the denominator, which is the argument for comparing a platform against itself. The memory probe needs ps, so it prints a visible skip on Windows rather than a pass it did not earn.
Verified
281 checks across five 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 96.55% of statements, 86.54% of branches and 63 of 63 functions. The published tarball is 6 files and the plugin cache 8. Installed from the registry, cic --version reports 0.6.0 and cic session --retries 3 exits 64 with the reason. Provenance attached and verified here with npm audit signatures: one verified registry signature, one verified attestation.
Full changelog: v0.5.0...v0.6.0
v0.5.0
Highlights
- The MCP protocol moves out of the command line into one internal
BridgeSession: spawn the child, negotiate initialization in specification order, allocate request ids, validate replies before anyone reads them, then terminate and reap.cic listandcic callare now create-session, one call, close. - The one-shot contract is unchanged. The proof a refactor changed nothing is the old tests passing untouched, so that came first: all 120 checks from 0.4.1 ran green against the new core before a single new test was written. Same stdout, stderr, exit codes and frozen
--jsonshape. - Adds
--retries <n>, restricted to failures that never reached the browser. Exit 3 is the only retryable class by definition; an exit-2 unknown outcome is never retried whatever the flag says, because repeating a click is a second action rather than a second look at the first. - CI runs on Ubuntu, macOS and Windows across Node 22 and 24, six legs, none cancelling the others.
Install
npm install -g claude-in-chrome-cliPlugin users need nothing new: the CLI ships bundled at node ${CLAUDE_PLUGIN_ROOT}/bin/cic.js.
Windows, stated precisely
Both Windows legs pass, and the assertions say what they actually prove. A child there cannot refuse a signal, because Node maps kill() to TerminateProcess, so the test that demonstrates SIGTERM escalation on POSIX demonstrates only that the client asked on Windows, and its label says so. Two tests stay POSIX-only and print a visible skip with the reason rather than being weakened into assertions that pass everywhere: the EPIPE pipeline needs a POSIX shell, and the detached-descendant shutdown test needs POSIX fd inheritance. The README does not yet claim Windows support, because the matrix proves the contract and parser suites there, not the process-lifecycle guarantees.
Three bugs worth naming
Review of this release found two defects in the new core, both invisible to one-shot use and both fatal to the reuse it exists for. The dispatched flag was set after the request write resolved, leaving a window where the child had already received the request and replied badly or died while the session still reported nothing dispatched: the failure was then classified exit 3, and --retries would have repeated an action that had already run. The flag now leads the write. Separately, four listeners per request were never detached, so twelve sequential calls on one session reached thirteen data listeners and Node began warning about a leak.
A third came from writing --retries itself, and it is the same family as the 0.4.0 output truncation: the backoff timer was unref'd, so it did not hold the event loop open. By the time a retry waits, the child is dead and its pipes destroyed, so nothing else held it either, and Node exited cleanly with code 0 mid-backoff, reporting success for a call that never happened.
Known limit, carried to 0.6.0
Each request owns its own reader and buffer, so bytes arriving between calls are dropped. Serialized one-shot use never sees this; a long-lived streaming session needs one persistent reader with a per-id waiter map instead.
Verified
142 checks across four offline suites, no Chrome and no extension required. Coverage 95.98% of statements, 85.33% of branches, 51 of 51 functions. The published tarball is 5 files. Installed from the registry, cic --version reports 0.5.0, cic list returns 22 live tools through a real bridge, and a missing binary still exits 3. Provenance attached and verifiable with npm audit signatures.
Full changelog: v0.4.1...v0.5.0
v0.4.1
Highlights
- Closes the validation gaps v0.4.0 shipped as known limits. The initialize response now gets the same result-or-error validator as a tool reply, and a malformed handshake exits 3 rather than 2, because nothing was dispatched and the browser cannot have acted.
- Validates members, not just their containers: a
contententry can no longer be a bare string, a text part can no longer omit itstext, and a tool can no longer omit itsname. Error codes must be integers, andisErrora boolean rather than anything truthy. - Adds coverage measurement and a non-regression gate. Codecov receives
lcov.infoover GitHub OIDC with no long-lived token, from a job that holdsid-token: writeand runs nothing else, because minting a token that asserts this repository's identity does not belong beside steps that install third-party code. - First release published by the trusted-publishing workflow, so the first to carry provenance. A
v*tag runs the suites andnpm pack --dry-run, then waits for a human to approve thenpmenvironment before anything reaches the registry. Verify it withnpm audit signatures.
Install
npm install -g claude-in-chrome-cliPlugin users need nothing new: the CLI ships bundled at node ${CLAUDE_PLUGIN_ROOT}/bin/cic.js.
Behaviour changes
Only malformed replies are affected, and only toward honesty. A reply that previously reported success, or the wrong code, now reports exit 2, outcome unknown. Well-formed bridge replies behave exactly as in 0.4.0.
One correction to an earlier report: plain and --json never actually disagreed on exit codes. What differed was their output, and the real defect was that both agreed on success for malformed members. Both now exit 2, and a test table enforces the agreement rather than leaving it incidental.
Verified
195 offline checks pass with no Chrome and no extension. Coverage is 95.31% of statements and 84.71% of branches, up from 94.95% and 82.55%, against a floor set from the earlier baseline so new code had to be covered rather than merely added. The published tarball is 4 files. Installed from the registry, cic list returns 22 live tools through a real bridge, and list_open_tabs read 49 tabs across 2 windows from a real profile.
Full changelog: v0.4.0...v0.4.1
v0.4.0
Highlights
- Replaces
cic.shwith a zero-dependency Node.js 22+ CLI and publishes it asclaude-in-chrome-cli, withcicas the installed command. - Fixes the MCP lifecycle: initialization is negotiated in order, protocol versions are checked before dispatch, and replies are read as they arrive instead of after a fixed sleep.
- Freezes meaningful exit codes: 0 success, 1 tool error, 2 outcome unknown after dispatch, 3 transport failure before dispatch, and 64 usage error.
--jsongives machine callers a stable one-line error envelope. - Drains large output before exit, handles closed pipelines without an EPIPE stack trace, rejects malformed top-level replies, and terminates and reaps misbehaving bridge children without hanging on inherited pipes.
- Bundles the Node CLI inside the Claude plugin while also supporting global npm installation; both paths are exercised by install-level tests.
Install
npm install -g claude-in-chrome-cliThe Claude plugin continues to bundle its own entry point at node ${CLAUDE_PLUGIN_ROOT}/bin/cic.js; plugin users do not need the global npm install.
Breaking change
cic.sh is removed. Existing copies remain usable and the script stays available from the v0.2.x and v0.3.x tags, but upgrades should move to cic list and cic call <tool> [json-args]. See the README migration table for every previously documented invocation.
Known limits
- Initialize replies do not yet use the same exact result-or-error validator as tool replies.
- Tool result arrays are required, but their members and optional
isErrortype are not fully validated; JSON-RPC error codes are checked as numbers rather than integers.
These affect hostile or broken server replies, not well-formed results, and are planned as post-0.4.0 hardening.
Validation
139 offline checks pass: 53 parser, 13 chrome-tabs handshake, 64 cic contract, and 9 installation checks. The npm tarball contains four files and no bundled dependencies.
Full changelog: v0.3.1...v0.4.0
v0.3.1
Highlights
- Moves the plugin's runtime files into
plugins/claude-in-chrome/and points the marketplace'ssourcethere instead of the repository root, so installingclaude-in-chromeno longer copiestest/,docs/,ROADMAP.mdand the GitHub workflow files into Claude Code's plugin cache. - Verified against the real
claudeplugin CLI, in an isolatedHOME, that a marketplacesourcepointing at a subdirectory produces an installed cache containing only that subdirectory. - Adds
test/test_plugin_install.sh: installs the plugin from this repo's own marketplace with throwawayHOME/CLAUDE_CONFIG_DIRvalues and asserts the installed cache holds exactly the runtime allowlist, and thatchrome-tabsboots from its real installed${CLAUDE_PLUGIN_ROOT}path. - Adds
test/test_cic_sync.sh, failing the build if the rootcic.sh(for the standalone curl download) and the plugin's own copy ever diverge. - Bumps
plugin.jsonandtabs_mcp.js's version together to 0.3.1, since version is part of Claude Code's plugin cache key.
Known limits
Packaging only: no change to parser behavior, MCP tools, or cic.sh semantics. The live claude --claude-in-chrome-mcp handshake isn't exercised end to end in CI, since no CI runner has a connected Chrome extension; the install test instead confirms the installed manifest still declares that command.
Full changelog: v0.3.0...v0.3.1
v0.3.0
Highlights
- Adds
list_open_tabs, a read-only MCP tool that lists tabs recoverable from every readable Chrome and Chromium profile, across all windows, without a debugging port, extension connection or tab group. - Ships the new
chrome-tabsMCP server as a zero-dependency Node.js 22+ program bundled with the plugin. - Redacts URLs by default: credentials, query strings and fragments are stripped, and wrapper schemes such as
blob:,filesystem:andview-source:cannot expose an embedded credentialed URL. Passfull_urls: trueonly when the raw value is needed. - Reports the page a tab is actually showing after Back/Forward navigation by honoring Chromium's selected navigation index.
- Fails closed on unsupported, corrupt or incomplete initial session snapshots, falls back to an older trustworthy file when possible, and distinguishes encrypted profiles from empty ones. A valid snapshot truncated during later incremental updates is returned with an incomplete warning.
- Adds offline parser and MCP regression suites plus GitHub Actions CI.
Known limits
list_open_tabs reads URLs and titles; it cannot drive pages or identify the focused tab. Chromium's session format is undocumented, encrypted session data cannot be decrypted by this release, and uncommon history-pruning events can leave a reported page stale. cic.sh and the extension-backed browser-driving tools are unchanged in this release.
Full changelog: v0.2.3...v0.3.0
v0.2.3
Documentation correction: the browser-routing guidance now matches how the tools actually behave.
- The Claude in Chrome bridge sees only tabs inside the extension's native Chrome tab group. Windows are irrelevant, and no bridge tool adopts a tab from outside the group. Re-open the URL in a group tab, or use Chrome's tab context menu when the original tab itself matters.
- Read-only questions about what is open can be answered from Chrome's on-disk session file without remote debugging.
chrome-devtoolsis not sessionless by default: it launches a persistent profile whose cookies survive between runs.--isolatedis the throwaway mode, and two servers launching against the default profile cannot run concurrently.- Remote debugging with
--autoConnectis reserved for reaching tabs already open in the user's daily Chrome. A staleDevToolsActivePortfile is not proof that debugging is enabled; check for a live listener instead.
Documentation only; no runtime behavior or MCP tools changed.
v0.2.2
Documentation fix. The shell examples in the README could not work as written.
- Every tool that acts on a page needs a
tabId. Eachcic.shcall is its own MCP session, so the session's tab group starts empty and the tool answersNo tab available. The examples now create a tab withtabs_create_mcp, reuse its id, and close it at the end. - The
javascript_toolexample was wrong twice: the requiredactionfield was missing, and the code goes intext, notcode. - The bundled skill carries the same note, so an agent driving
cic.shgets it right.
Every corrected example was run against a live extension. No change to the plugin's MCP tools: inside a Claude Code session the tools share one tab group, so none of this applies there.
