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