Changelog
What's new in cctabs. Source of truth lives in CHANGELOG.md on GitHub.
Install or update:
# Claude Code plugin (recommended)
/plugin install cctabs@generativereality
# Or as a standalone CLI
npm i -g @generativereality/cctabsUnreleased
0.5.8 — 2026-09-23
- Suspended tabs.
cctabs suspend <tab>stops the Claude in a tab and leaves a small placeholder that keeps the tab's name, place and session:⏸ cctabs suspended — <name> · <size> · <dir> / press Enter to resume.cctabs restore --manifest … -c --suspendedbrings a whole fleet back this way in about a second per tab with no Claude started (a 66-session restore took minutes, hit the plugin's spawn timeouts, and pushed live sessions out of claude.ai's remote-control top 20);cctabs new <name> -r <id> --suspendedopens one directly. A suspended tab wakes on Enter, oncctabs wake/resume, or — the point of it — whencctabs sendtargets it: send wakes the tab, waits until Claude is at a ready prompt, then delivers and checks the delivery against the transcript, so the sender never has to know.sessions --jsonreportsstatus: "suspended"with the session id and dir;manifestkeeps suspended tabs (suspended: true) so they restore asleep;restoreandrestartleave them asleep. The state lives in a registry (~/.config/cctabs/suspended/) and in the placeholder's own argv, never only on screen — Tabby captures nothing for a background tab. Trade-off: a suspended tab is not on Remote Control until woken. Needs bash or zsh as the tab shell. - Fix: cctabs answered the folder-trust dialog "No, exit". Current Claude Code lists
No, exitfirst and highlighted, with no option numbers, and cctabs's dialog handling pressed Enter on the default — quitting the session it was restoring, and onnew --promptletting the brief fall through to the shell. It now finds "Yes, I trust this folder", moves the cursor there, confirms the move landed and only then presses Enter; it presses nothing if it can't read where the cursor is. Where it answers is unchanged: an explicitcctabs newdirectory, and restores/wakes of directories that already have sessions. - Fix:
restorecould type a resume into a tab whose process was known to be dead when its last output still read like a shell. A tab whose process is known gone is now recreated, whatever its screen shows. - Fix:
cctabs resume <name>andcctabs new <name>without a directory failed withPositional argument 'dir' is required, despite the documented cwd default.
0.5.7 — 2026-09-23
- The skill is split:
SKILL.mdis under 500 lines, the rest is inskills/cctabs/references/. It had grown to ~1,200 lines, all loaded on every invocation, and marketplace skill lints reject a SKILL.md over 500. What every invocation needs stays inSKILL.md: install and version checks, the right-tool test, worktrees, the trust gate, the quick reference, the core workflows and the hard rules. Restore and restart, backends and accounts, routing, the fullsendreference, tab names and colours, Remote Control, worktree recovery, export/import and timeout troubleshooting moved to eleven reference files, each linked fromSKILL.mdwith a line saying when to read it. Moved, not cut: every line of the old file is in one of the new ones. sync-plugin.shsyncs the whole skill directory. It copied and checkedSKILL.mdalone, which would have published a marketplace skill whosereferences/links led nowhere. It now replacesskills/cctabs/wholesale and--checkcompares the directory recursively.- The skill-only install fetches the whole skill directory. The README, the getting-started guide and
install.shused tocurlonlySKILL.md; they now extractskills/cctabs/from the repo tarball. LICENSEfile added. The package and plugin already declared MIT; the licence text itself was missing from the repo.- Fix:
restorereported three tabs "✔ verified running" that had exited. The check sawclaude --resume <id>in the process table and stopped looking; each of those Claudes printedNo conversation found with session IDand exited a second later. A process appearing proves the launch, not the resume. A Claude launched on the requested id now has to stay up for 6s to count, a transcript on disk no longer passes a tab while its process is unproven, and one that starts and then exits fails immediately, quoting "No conversation found" when the screen shows it. The screen text never decides on its own, because a tab resumed in place keeps the old error in its scrollback above a healthy Claude. - Fix: a metadata-only trailer was taken for the session it was left behind by. When a session moves to another Claude config dir, its closing process writes a ~1 KB trailer (title, mode, no messages) back to the old path. The by-title lookup, the id lookup and the transcript locator all accepted it, so
cctabs manifestrecorded three sessions under an account that held no conversation for them, and restore resumed all three into nothing. Every lookup now skips a transcript without a single message. Andrestoreno longer trusts a manifest's recorded account blindly: if that account holds only a trailer and another account on this machine holds the conversation, the entry is re-homed with a warning (a claim that can't be checked, such as an imported manifest whose transcript isn't here yet, still wins). manifest/restartno longer drop a tab whose worktree was deleted. Claude Code removes a worktree on exit, but the session stays resumable:claude --resume <id>finds a session by id from any directory — measured on Claude Code 2.1.280 from the repo root, a parent dir, an unrelated dir and~, with every resumed turn appended to the original transcript. Four live tabs were being refused asmissing-dirfor exactly this. An entry whose.claude/worktrees/<name>is gone is now pointed at that worktree's own repo root, with a warning that it will be working in the main checkout. Unlike--repoint-missing-dirs, which takes one directory, this is right for a fleet spread over several repos. Any other missing directory is still an error.transcript-elsewhereis a warning, not an error. A session started in a subdirectory andcd-ed out of is filed under the subdirectory's slug while its tab reports the parent; the check assumed--resumefrom the parent could not find it. It can (same measurement), so the tab is kept and the warning says only an older Claude Code might miss it.
0.5.6 — 2026-09-23
cctabs restart— a fleet restart in one command. Putting 56 tabs on a new Claude Code took an hour of hand-written scripts: build a manifest, pull pids out ofps, kill everything except yourself, restore, then audit for tabs that came back empty.restartdoes all of it — snapshot, save the manifest to~/.config/cctabs/restarts/before stopping anything, SIGTERM each tab's Claude and wait for it to exit,restore --manifest … -c, then check that every restored session has a live Claude launched with--resume <its id>. Barecctabs restartonly prints the plan;--allor--only a,bacts. It refuses to run unless it can identify its own process, never signals anything in its own process tree, and only stops a Claude it can tie to a session (its argv resumes that id, or it sits under the tab's own shell — which ties it to the tab, whose session still comes from the title, so one whose argv resumes a different session is refused). A Claude matched only by name is listed "restart it by hand". The audit replaces the hand-rolled "no Claude without--resume" count, which reads non-zero the moment any tab has been opened fresh — including the one running it.cctabs manifest— a restore manifest you can act on. Every rule is a correction a hand-built fleet manifest needed. The calling session is left out, by session id and pid rather than by tab. Entries are keyed on session id: one session reached a hand-built manifest under two names — its tab's, and a stale spawn-time--nameread fromps— and restore spawned a second Claude onto a live transcript. So two tabs resolving to one id is an error, unless a live process proves which owns it, in which case the other is dropped with a warning. Every directory must exist (six entries pointed at deleted worktrees, where a restored Claude getsUnknown skilland git can't read its cwd;--repoint-missing-dirsredirects them), and every id must have a transcript in some Claude config dir. Two tabs sharing a name are an error too: restore resolves entries by name, so a restart would stop both and bring back neither. So is a session recovered from argv whose transcript isn't under the tab's directory, since--resumefrom there wouldn't find it. It writes nothing on an error unless--drop-invalid.sessions --jsonfinds sessions the title search misses. When no transcript is titled after a tab, it now asks the Claude running there:--resume <id>in its argv names the session exactly. That recovers two measured misses — a worktree renamed after its session started (the transcript sits under the old slug), and a transcript whose last title no longer matches its tab. Those rows saysession_source: "argv". Only the id comes from argv, never the name. Rows also gainclaude_pidandclaude_pid_via.- Fix:
restorereported a running tab as "did not come back". One look at 4s saw no process for a tab whose PTY had not attached yet — under load that takes longer than the plugin's own 20s wait — and a false failure on a fleet command invites a second restore over a healthy tab. Verification now re-checks every few seconds for up to 45s before calling anything failed, and a live Claude whose argv says--resume <the id asked for>counts as verified without waiting for a title to reach disk. A resume that came back as a different session still fails, whatever argv says. - Fix:
restorecould put two Claudes on one transcript. It de-duplicated by name only. It now also refuses a second entry on a session id it is already restoring (duplicate-session), and never spawns or types into an entry whose session a live Claude is already running (session-live). - Fix:
whoamiansweredunknownin a perfectly ordinary tab. Tabby records a tab's pid once, two seconds after spawn, by following single-child chains — which for a Claude tab lands on Claude's owncaffeinate -t 300helper, gone five minutes later. On a measured 57-tab fleet 52 tabs reported a pid that no longer existed, so the process match found nothing.whoaminow falls back to the--nameits own Claude was launched with, taken only when exactly one tab has that name and its directory agrees with the session's (via: "argv-name"). doctorchecks the two Windows things that aren't cctabs' fault.claude in the spawned shellprobes the shell a tab actually gets — on Windows the Claude Code installer doesn't put itself on PATH, so a tab would open oncommand not found. On Windows only,bash on PATH (Claude Code's Bash tool)warns when Git Bash exists but isn't on PATH: cctabs doesn't need it, but Claude Code then silently substitutes a PowerShell tool for its Bash tool, so a Bash allowlist stops covering what the agent runs. The hint prints the exact directory to add.doctoralso opens with a platform · node · tab-shell line, which answers the first question on every Windows report.- Fix:
doctorcould report a shell banner as a path. The spawned-shell probes took the first line of output, and a login shell can print one of its own first (macOS Terminal's "Restored session: …"). - tabby-cctabs 0.1.5: the
stable-pidcapability./api/tabsreports each tab's real shell pid (shellPid), and/api/tabs/identifymatches on it before the stale one. With it,whoami, restore's "is anything running here" check andrestart's process matching are exact rather than inferred. Older plugins keep working through the fallbacks above.
0.5.5 — 2026-09-22
- Fix: an option a command doesn't have was accepted and ignored.
cctabs new mmm-b2b "…" --path <file>printed✔ Tab "mmm-b2b" … → claudeand opened the tab, and the brief it was meant to deliver went nowhere —--pathis asendoption,newhad--prompt/--file, and nothing said so; the tab sat idle until a human noticed. gunshi parses an undeclared flag into neithervaluesnorpositionalsand runs the command as though it were never typed, so this was never specific tonew:cctabs sessions --bogus-flagexited 0 and printed the session list. Every command now refuses to run when given an option it doesn't declare, naming the option and exiting non-zero.--help/--version,--no-<flag>for a declared boolean, and anything after a--terminator are all still accepted. cctabs new --path <file>hands the new session a file the waysend --pathdoes. The skill calls--paththe robust way to deliver anything large — only the path crosses the prompt line, so there is no truncation surface — and a session that has read that will reach for it when spawning a tab too. It is the same handoff, now shared from one module rather than reimplemented: the file is checked for existence before the tab is opened, because a tab pointed at a file that isn't there is worse than no tab. It takes no short flag:-pis--promptonnewwhile it is--pathonsend, and quietly resolving that collision either way would be worse than spelling the option out.
0.5.4 — 2026-09-17
Windows:
cctabs newreported a tab it had not started. The spawn handed a POSIX command line to whateverprocess.env.SHELLnamed, which on Windows is wrong in both directions — unset inside a Tabby tab (so cctabs asked Windows to run/bin/zsh), and set topowershell.exeover OpenSSH, which then failed onexecand-lwhile the create call had already returned exit 0.resolveTabShell()now picks a shell that can actually run the line:$SHELLwhen it is POSIX, else Git Bash located from whichevergitis onPATH.Windows: the pid walk and the plugin-install path both dead-ended. Measured on a Windows 11 Pro ARM64 guest (build 26100) running Tabby's native arm64 build — before and after, on the same machine. The Tabby side was already fine there: the plugin loads,
/api/healthanswers, tabs spawn, andpathToProjectSlugalready agreed with Claude Code's own slug. Every failure was CLI-side, and each one reported success while failing.The skill description was being silently truncated, and the part cut off was the rule that matters. Claude Code caps a skill's description at 1535 characters in the available-skills listing; ours ran 1760, so every session ever shown this skill saw it cut mid-sentence at
"do this in parallel without a new tab", or w…— losing the tail of the anti-substitution rule and the wholeNOT for:line. The description now runs 1432 characters, and the decisive sentence (a subagent is NOT a tab) has moved ahead of the TRIGGER list so it can no longer be the thing that gets dropped. Every trigger phrase is preserved verbatim. The opening clause now leads with the literal tokenscctab/cctabs/terminal tabs, because the first clause is what registers in a listing where this skill can sit 75% of the way down 82 entries.cctabis now a real command. The skill has always advertised "cctab" as a singular alias, but onlycctabswas ever installed — so a session that went looking for the CLI under the name the docs use gotcommand not foundand concluded the tool did not exist.cctabis now a second bin pointing at the same entry point. This matters precisely in the case the listing cannot help with: Claude Code injects the full skill listing once per session and does not re-inject it after a compaction, so in a long session probing the shell is the only route left back to cctabs.Skill: the trust dialog, which silently eats a spawned tab's
--prompt. Measured 2026-09-16: seven tabs spawned, five with--worktree --file <brief>; all five landed on Claude Code's "Is this a project you trust?" dialog and none received its brief, while one brief reached the shell instead and began npm-downloading packages before that session dropped to a bare prompt. Three facts the skill was missing: the selection marker defaults toNo, exit, so a bare Enter kills the session and the working keystroke is Down-then-Enter (printf '\033[B' | cctabs send <tab>— one call, sincesendappends the Enter itself); thecctabs newpoll for❯cannot see past the dialog, so the doc's "no race condition" promise held only for already-trusted directories; and the gate's real precondition is not "is it a worktree". Verified against Claude Code 2.1.273's own resolver: trust lives in${CLAUDE_CONFIG_DIR:-$HOME}/.claude.jsonunderprojects[path].hasTrustDialogAccepted, and the lookup walks up from the tab's directory but stops at the enclosing git repo root — which for a worktree resolves to the main repository. So a worktree of a trusted repo is trusted (measured: no dialog) while a never-visited repo is gated no matter how many trusted ancestors it has (measured:~/Devtrusted since 2026-08-28 did not trust a repo beneath it). The skill now carries a read-only pre-spawn check, the spawn-bare-then-unblock recipe, and a carve-out to the "rendered menu needs a human" rule — the trust dialog is two options with a known starting position, so a driver may clear it, unlike the resume picker.Skill: how to notice that the skill text itself is stale. The marketplace plugin and the npm CLI are separate channels, and the doc only warned about the CLI half. The driver above was reading the cached skill at
<config-dir>/plugins/cache/generativereality/cctabs/0.5.0/…— 691 lines, zero occurrences of "trust" — while the CLI on PATH was 0.5.3 and the source skill was 1030 lines and already covered the stuck-tab case. The cache path carries its version; the skill now says to compare it againstcctabs --versionand to treat anything absent as possibly-just-missing.Skill: guidance on message ROUTING — which tab gets a message, upstream of whether it arrives. Three gates from a day of driving a ~15-tab fleet, each measured: resolve the owning tab from its branch rather than its name (on a 92-tab fleet all three layers disagreed — the tab owning one topic's work was named after something else entirely, and no tab was named after the topic at all); count the phrases you are about to relay in the target's transcript before drafting, because
transcriptshows what a tab concluded and not what it has seen (one of six candidate tabs had nothing new and was dropped); and relay what was said rather than your own conclusions, since a quoted statement is checkable where a paraphrased instruction is not — with a contradiction of the tab's own conclusion being the highest-value relay there is. Also documents what the two new refusals mean:nothing from the text appeared in the tabidentifies a tab stuck on a rendered menu, which is unreachable and needs a human rather than a--pathretry, and the 1 KB busy-tab refusal means shorten the message rather than--forceit.Fix: a
sendwhose text quoted a flag name delivered NOTHING and reported success. The option parser silently drops any argv element containing--— measured on"mentions --verify here","--leading", and even"a--b"; a single dash survives. The text vanished from the positionals and the values,sendfell through to reading stdin, stdin was empty, and it printed✔ Sent to 5f3e853e: ⏎with an empty preview. A ~900-byte bug report was lost this way, and the input is not exotic: any message quoting a flag name hits it, which for a tool whose users are agents reporting tool bugs is the normal case.sendnow recovers its positionals fromprocess.argvdirectly (core/send-argv.ts) rather than from the parser that loses them, and--is supported as an explicit terminator for text that is entirely flag-shaped:cctabs send tab -- --verify is broken.Fix: an empty body is now a hard failure rather than a ✔. Reporting success for a delivery of nothing is the same defect class as the restore success line that could not fail. An empty
--file, or no text source at all with empty stdin, exits non-zero and says which it was; the no-source message names the--terminator, since a swallowed payload is the likeliest reason to land there. An explicit empty is still honoured —--submit, or a literal""— and now reports itself asSubmitted Enter only (no body), so an empty preview after a ✔ can never appear again. That preview was the operator's only tell.Fix:
--verifycompared against the target's NEWEST user message, which the target overwrites with its own work. Claude records a tool's output as arole: "user"message, so a--pathhandoff — which tells the tab to read a file — makes the newest user-role entry the file's contents. Verify then compared the handoff against the file and reported that the payload "matches neither end of what was sent", which reads exactly like--pathhaving pasted the contents. It now skips tool results (identified by the entry'stoolUseResult) and searches every message rather than only the last, so a payload stays findable after the session has moved on. Both ends arriving in different messages is still not a delivery.Fix:
--verifyfailed instantly against a freshly spawned tab. It resolved the session once, before its poll loop, so a tab whose transcript was a second from existing reported "no session resolved for tab" and failed. Session lookup now retries for the whole window, dropping the per-process title-index cache each round — the session being waited for is precisely the one that appears after that cache was built.send --path's help text no longer implies the receiving session's transcript stays clean: the session obeys the handoff by reading the file, so the contents land there as a tool result. That is the handoff working, and mistaking it for a paste is what the report above turned on.cctabs transcript <tab> [n]reads what a tab has SAID, not what it is painting.scrollbackreturns the last painted frame, so a tab mid-turn shows a spinner and nothing else — which meant sessions briefed each other from stale pictures, and one such brief told a tab to go measure two things it had already measured, missing a third it had found. Prints the last n assistant messages from the transcript (--jsonfor a driver;findingsas an alias). It searches every Claude config dir, because a tab on a backend preset writes under that preset's own root and looking in only~/.claude/projectsreports "no transcript" for a healthy session — which reads as dead. Tool-only and thinking-only messages are skipped. It exits non-zero and names the failure — no session titled after this tab (with a count of transcripts that do exist for its directory, which distinguishes a renamed tab from a dead one), no transcript on disk, or a lookup that threw — because "I couldn't read it" and "it hasn't said anything" are different answers.cctabs sort --first a,b,cpins a chosen set to the front of the bar. Activity order is close to the opposite of what a driver wants: a tab that just delivered sinks. The Tabby plugin'sPOST /api/tabs/reorderalready did exactly this — unlisted tabs keep their relative order and sort after the listed ones — with no CLI verb exposing it. Naming tabs skips the activity scan entirely (~7.7s of transcript reading on a 65-tab fleet). If any name doesn't resolve to exactly one tab, nothing moves and it exits non-zero: half a working set in reach, with no indication which half, is worse than an error.cctabs sendno longer reports success it hasn't established. A large paste collapses to a[Pasted text #N +M lines]chip, and the chip renders no matter how little arrived — so "a chip is on screen" was accepted as proof of delivery, which is how a 6,835-byte brief that landed as its last 756 bytes came to be reported as sent.sendnow distinguishes three claims that used to be one ✔ line: nothing arrived (a hard failure — and the body is not submitted, because pressing Enter on a fragment sends something that reads as a complete message; the text is left in the input box and the command exits non-zero), something arrived but completeness is unverified (a warning naming the two ways to establish it), and verified.cctabs send --verifychecks what the target session actually RECEIVED. After submitting, it reads the target's own transcript — which records the user message as received — and compares the payload's front and tail fingerprints against it, failing loudly on a mismatch and naming which end went missing. This is the only reliable completeness check available, and finding that out cost a measurement: the chip's+N linescount does not track the payload. A 6,892-byte, 76-line payload was measured delivering completely into an idle tab while its chip read+10 lines, so treating a shortfall as truncation fails healthy sends. The screen cannot answer this question; the transcript can.cctabs send --path <file>hands a tab a file path instead of pasting its contents. No truncation surface at all: what crosses the prompt line is a path, and the receiving session reads the payload from disk. This is the fallback that had to be used to deliver the brief describing the truncation, and it is now first-class rather than a discipline.cctabs sendrefuses payloads over 1 KB into a tab with a turn in flight, with--forceto override. Short replies into a busy tab —yesto a tool call,2to a picker — are exactly what sending into an active tab is for and still work; a multi-kilobyte brief pushed into a busy input handler is how text gets clipped.cctabs send --submitpresses Enter only, submitting a prompt already parked in a tab's input box. An empty body with no--submitnow says so instead of silently pressing Enter in someone's session.cctabs send --wait-for-promptno longer times out against tabs that are ready. It tested only the buffer's last non-empty line, and Claude renders notices below its input line — aRestart to updatebanner was enough to defeat it for the full 20s. It now reads the whole tail of the window.cctabs restorereports the count it actually achieved. It read "78 spawned, 0 failed" while one tab was absent entirely and another had come back with no session, having lost its context — a summary computed from "did the spawn call return?", which cannot report a failure that happens after it returns. Restore now re-reads the tab list after spawning, checks each tab has a process, resolves its session from disk, and reportsN verified, N unconfirmed, N failed, exiting non-zero when anything failed. A tab that came back as a different session than requested counts as failed:claude --resumeon an id it can't find opens a fresh conversation, so the tab looks perfect and the context is gone.unconfirmedis a first-class outcome rather than rounded either way.cctabs sessions --jsonsays why asession_idis missing. Every row carriessession_lookup:found,not-found(withsessions_in_dir, so a tab renamed out from under a live session is distinguishable from a directory nothing has ever run in),no-cwd, orlookup-failed(with the error — a lookup that threw is unknown, not absent, and was previously swallowed into the same null). Observed on a fleet as a tab whose session was perfectly readable but filed under a title that no longer matched.Internal:
send,scrollbackandtranscriptnow share one target resolver (core/tab-target.ts) instead of three verbatim copies of the tab-then-block lookup, andscrollback <tab> [n]accepts the bare line count the docs have always shown.
0.5.3 — 2026-09-07
cctabs whoamianswers "which tab am I running in?" Prints the tab name — so$(cctabs whoami)drops into a PR body or commit trailer — with--jsonaddingworktree,session_id,cwd,backend,config_dir,colorandvia. It exists because self-attribution had no answer: when every session's PRs carry the same GitHub author, "whose work is this, and is it in flight?" is unanswerable, and it gets asked exactly when someone else's files are being written right now.- It identifies the tab two ways, and neither is "the focused tab". Focus reads false for a background tab running the command, so matching on it silently attributes work to the wrong session. Instead: the terminal's process-tree match (
via: "pid", definitive when it answers), falling back to the session's own id —CLAUDE_CODE_SESSION_IDnames the project directory its transcript lives in, and a tab whose cwd maps there is us (via: "session-slug"). The fallback is accepted only when exactly one tab matches; two tabs in one directory are indistinguishable that way, and guessing is the failure being avoided. unknownis an answer, not an error. A session in a plain terminal, over SSH or in CI has no tab;whoamiprintsunknownand exits 0, so callers can say "unnamed session" rather than invent one.- It reads no transcripts. The hand-rolled equivalent pipes
cctabs sessions --jsoninto a matcher, which resolves every tab's session by reading the transcripts in each project dir — measured at 7.7s on a 65-tab fleet holding ~1.7GB of history, and minutes on a cold cache.whoamifetches the tab list and one directory listing: ~1s, mostly process startup. A check meant to run on every PR cannot cost the former. cctabs sendnow confirms the text actually landed, and re-sends it if not. Input sent into a not-yet-ready handler can be silently lost — sometimes wholly, sometimes only its front, which arrives looking like a message whose lead-in was trimmed rather than like a failure.sendnow fingerprints the front of the text (the part observed to go missing, so a clipped send is caught exactly like a dropped one), waits for it to appear in the scrollback, and re-sends — clearing the line first, so a retry can't stack a second copy. It warns rather than fails when it genuinely can't confirm. Text too short to fingerprint (under 4 non-whitespace characters, e.g. the bare2that answers a picker) is sent once as before, since matching on it would be guesswork. This is the same paste/confirm/retrycctabs new --prompthas always used, now shared rather than duplicated.
0.5.2 — 2026-08-26
Fix: the Tabby plugin announced the wrong version of itself.
tabby-cctabs0.1.4 shipped withPLUGIN_VERSION = '0.1.5', so/api/health— and thereforecctabs doctor— reported a version that was never published. Harmless in behaviour, because plugin features are feature-detected throughcapabilitiesrather than compared by version, which is precisely why this drift has now survived review twice (once a release behind, once, via a renumbered release, a release ahead). There is now a test assertingPLUGIN_VERSIONmatchestabby-plugin/package.json, so the next drift failsnpm test— and, since the plugin's release workflow never runs the test suite, that workflow now checks the same pair itself before publishing.Fix:
restorecoloured only the tabs it recreated, not the ones it attached to. Whether a restored tab is attached or recreated turns on whether its shell happens to be alive — which, after a terminal restart, comes down to which tabs got focused first, since Tabby spawns a recovered tab's shell only on focus. So a restored fleet came back half-coloured, in an order the user has no reason to predict. Both paths now apply the colour.Fix:
cctabs configdidn't listdefaults.color. It printedclaude.flags,defaults.workspaceanddefaults.prefixonly, so the one command whose job is "show me what my config is" silently omitted a setting that was being honoured — an easy way to conclude a colour hadn't been picked up when it had.Tabs can be coloured.
--coloronnew,resumeandforksets the Tabby tab colour, andcctabs color <tab> <colour>changes one that already exists — including a tabresumereused rather than created, which a create-time-only flag would have missed. Values are Tabby's own palette (blue,green,orange,purple,red,yellow),noneto clear, or any 3-, 6- or 8-digit hex. The motivation is fleets that span several Claude accounts:[backends.<name>] colorcolours one account's tabs, so which account a tab belongs to is visible in the tab bar rather than something you have to remember.[defaults] colorcolours everything; precedence is--color→ the preset'scolor→[defaults].Named colours resolve to Tabby's exact hex values, not merely similar ones. Tabby's right-click → Color menu renders its radio state by comparing
tab.coloragainst the literals in its ownTAB_COLORS, so mapping "blue" to any other blue would colour the tab correctly and still leave that menu showing no selection — a cctabs-set colour that looks subtly unlike a hand-set one. The plugin setsBaseTabComponent.colordirectly, which is exactly what the menu's own click handler does, so the two are indistinguishable and Tabby's tab recovery persists both alike.An unrecognised colour fails instead of doing nothing. Tabby binds the value straight into a CSS
background-color, so a colour it doesn't understand simply fails to render — silently, and identically to a colour that never arrived. Validation happens before the terminal is touched, so a typo costs nothing: no tab, and (onnew --worktree) no worktree left behind.Colour support is a plugin capability, probed rather than assumed. A user's installed plugin is routinely older than the CLI talking to it, and an older
tabby-cctabsdrops an unknowncolorfield onPOST /api/tabs/newwithout complaint — which is indistinguishable from a colour that was accepted and didn't render.new/resume/forktherefore ask first and, when the answer is no, warn once and open the tab uncoloured: losing the spawn and whatever was going to run in it would be much the worse trade for a cosmetic field.cctabs colorexits non-zero instead, because there colouring was the whole request.A tab's colour survives a reboot. Tabby does persist a tab colour across its own restart, but that isn't enough:
cctabs restorerecreates a dead tab — closes it and spawns a new one — and a fresh tab starts uncoloured, so a restored fleet came back colourless. Colour now travels the same routepermission_modealready does.cctabs sessions --jsonrecordscolor,restore --manifesthands it back, and a scan-mode restore reads the colour off the tab it is about to replace. A recordednullis honoured as "deliberately uncoloured" rather than treated as missing.…and a rule like "the enterprise account's tabs are blue" holds even with nothing recorded. When an entry carries no colour — a manifest predating the field, or a tab Tabby never recovered — the colour falls back to what the config implies for that entry's backend:
[backends.<name>] color, else[defaults] color. The backend is already inferred from the Claude config dir the session was found in, so the colour follows the account for free.Fix: a
[backends.<name>]section replaced a builtin preset instead of overlaying it. Pre-existing, but this release is what makes it easy to hit, because adding acolorto a preset is now an obvious thing to write:[backends.kimi]\ncolor = "red"silently discarded kimi'sANTHROPIC_BASE_URL, auth token and model, leaving a preset that looked configured and quietly talked to the default Anthropic API instead. Sections now merge over the builtin of the same name —envkey-by-key, and unsetmodel/description/colorfalling through — so one key can be overridden without restating the rest.Plugin:
tab-colorcapability. Tabs report theircoloronGET /api/tabs,POST /api/tabs/newaccepts an optionalcolor(applied as part of the create, so a tab never renders uncoloured first), andPUT /api/tabs/:uuid/colorsets or clears it on a live tab. An invalid colour is a 400 on thePUTbut only a logged warning on the create — failing a spawn over a cosmetic field is the worse outcome. Needs a rebuilttabby-cctabs; without it the CLI degrades as above.cctabs profile-copymoves a session between Claude accounts.CLAUDE_CONFIG_DIRisolates each profile's transcripts, so a session started under one account is invisible to anything running under another — and moving one across by hand means knowing five separate ways to get it wrong.cctabs profile-copy <tab|session-id> --to <preset>copies the transcript and its sidecar into the target profile, names the copy distinctly, and opens it in a tab under that account.--tonames a backend preset rather than a bare config dir because on macOS the Keychain holds one Claude Code login per OS user and does not scope it byCLAUDE_CONFIG_DIR: running under another profile depends on that profile'sCLAUDE_CODE_OAUTH_TOKEN, which presets already carry. A bare path is accepted with a warning.The sidecar travels with the session — and
export/importwere silently dropping it. A session's subagent transcripts and tool results live in a<session-id>/directory beside the.jsonl; one real session had 357 files in it.cctabs exportcopied only the transcript, so an imported session resumed having forgotten every subagent, with nothing anywhere saying so. Sidecars are now bundled astabs/<name>/sidecar/and restored on import. Archives written before this simply have none, and import handles both, so no format bump.exportcouldn't see a second Claude account at all. It resolved sessions across every config dir but then built the transcript path as~/.claude/projects/<slug>/…unconditionally, so exporting anything belonging to a preset-owned profile failed with "session file missing". The transcript is now read from the config dir it was actually found in, and the owning preset is recorded in the manifest asbackendsoimportputs the session back under the same account. Importing into the wrong config dir doesn't fail loudly —claude --resumejust can't find the id and opens a fresh conversation — so this was a data-loss-shaped bug rather than an error message.A move refuses while the source is still running. A
mvwithin one filesystem is a rename: the inode is unchanged, so a liveclaude's open descriptor follows the file, and the old tab and the new one then append to the same transcript, interleaving two conversations into one unusable file. The default is therefore a copy, which diverges cleanly the way--fork-sessiondoes.--movechecks the source tab's pid and refuses, pointing at--close-source.Closing a tab is not the same event as its process exiting, and the gap loses data. A closing Claude Code writes a metadata trailer —
custom-title,agent-name,mode,permission-mode,pr-link, and no conversation content — back to its transcript path aftercctabs closehas already reported success. Move the transcript in that window and the trailer recreates the file at the old path, with a fresh mtime and acustomTitle. Since name resolution matches on customTitle and prefers the newest mtime, that stub outranks the relocated original and quietly shadows it on the nextrestore.--close-sourcenow waits for the pid to actually disappear (not just the tab), then re-checks the old path and sweeps the trailer — and only ever removes a file it can prove is metadata-only and belongs to that session id.A session whose directory no longer exists is filed under the repo root instead.
claude --resume <id>fails withNo conversation found with session IDwhen the transcript sits under the project slug of a deleted directory — a removed worktree, usually — even though the file is right there. The target slug is chosen as the last recorded cwd that still exists, scanning the transcript backwards, falling back to the repo root. Last, not first: a session can start in one worktree,cdelsewhere, and end in another, and the transcript is filed under the one it started in. Picking the transcript's own slug would reproduce the dead-path failure inside the target profile.After a relocating move, the stale project dir is archived out of
projects/entirely. A leftover worktree-named project directory is itself read as evidence that the worktree still exists, which sends a laterrestoreinto a deleted path. Renaming it in place does not help — matching is on thecustomTitleinside the transcripts, not on the directory name — so it is moved to<config-dir>/cctabs-archived-projects/<slug>-<timestamp>instead. Only ever when the dir has no transcripts left and the directory its slug names is genuinely gone.cctabs fork's private worktree-path handling is now the sharedrepoRootOf()insrc/core/worktree.ts, whichprofile-copyneeds for the same reason: it has to keep working after the worktree directory has been deleted, which is exactly when it's called.restorereported success for tabs that never opened a session. Claude has two startup dialogs that block a tab until answered — the folder-trust check ("Is this a project you created or one you trust?") and, new, "Set up auto mode for your environment?". Both render after the process starts and before the conversation loads, so a tab sitting on one is indistinguishable from a healthy launch: the process is alive,restoreprints✔ spawned, and nothing ever resumes. On a real 65-tab restore this stranded 10 tabs — 8 on trust, 2 on auto-mode — and the only symptom that surfaced anywhere was an unreadable permission mode, because a blocked tab never paints a footer. Trust was handled, but only on thecctabs newpath; restore and resume went throughconfirmResumePicker, which knew about the resume picker and the mobile-app overlay and neither dialog. Both are now cleared on every path, by one sharedclearStartupDialogs.The two dialogs need opposite answers, and the difference matters. Enter on the trust dialog takes its default, "Yes, I trust this folder". Enter on the auto-mode dialog takes its default — "Set it up" — which starts an interactive pass over your repo and recent sessions. That is not something a restore should trigger unattended across a fleet, so the auto-mode dialog is answered with ↓ once then Enter, selecting "Not now". Option 3 ("Don't show again") is deliberately never reachable: permanently suppressing a prompt is the user's call, the same reasoning that already governs the resume picker's option 3.
Trust is auto-confirmed only where the user already trusted the folder.
restoreandresumeconfirm the dialog when the directory already holds transcripts under some config dir — evidence that Claude has worked there before, so the decision was the user's, earlier, and we are re-affirming rather than making it. A directory with no prior session is genuinely new, the dialog is doing its job, and cctabs leaves it alone with a warning naming the path.cctabs newstill trusts unconditionally: it opens the directory you explicitly named, and requiring a prior session would hang the case that needs it most — starting a session in a repo you just created.Dismissal is confirmed by a forward signal (the live input footer), never by the dialog text disappearing. The captured buffer is append-only, so the prose stays on screen forever and "is it still there?" always answers yes — a trap that made cleared dialogs read as still-stuck during the debugging of this very bug.
0.5.1 — 2026-08-12
restorecalled a live session's tab dead, and closed it.detectSessionStatusread the Tabby plugin's captured output and treated an empty result as "no session here" — reported asunknown, rendered as "dead tab", and acted on by closing the tab and spawning a replacement. But the plugin fills that buffer by subscribing to each tab's output, and a tab whose session attaches after the subscriber gives up is never captured at all: it reads empty forever while Claude runs happily inside it. The plugin's own log showed 192 tabs wired against 147 subscribed in one boot — 45 live tabs any restore would have declared dead. One of them was a 4,377-turn session whose last turn had finished two minutes earlier. Liveness now comes from the process, not the scrollback:/api/tabsalready reports each tab's pid, so a tab that can't be read but is running is reported and left alone — restore can neither typeclaude --resumeinto it nor close it, so it does neither. Only a tab with no captured output and no process is rebuilt. Backends that don't report pids fall back to the previous heuristic, so an older plugin still works.unknownis renamedunreadablethroughout, because the word was the bug.activemeant "a Claude session exists", not "it's working". Every marker was matched intoactive— including permanent chrome (Claude Code,⏵⏵ auto,new task?) and completion notices (✻ Baked for 47s, which means a turn just ended). On a 61-tab fleet that produced 58activeand 0idle, so the question the README asks — "Did it finish? Is it waiting for input?" — had no answer. Presence and in-flight are now separate: chrome proves Claude is there (idle, waiting for input), and only an unfinished spinner in the recent tail proves it is working (active). The same fleet now reads 2 active / 56 idle / 1 shell. Completion notices are matched by shape rather than by verb, since the vocabulary is open-ended — an enumerated list missedSautéed forandBrewed foron the first real fleet it met.- A tab's permission mode survives a restore. Every restored tab used to come back in whatever the global
claude.flagsproduced, so a tab deliberately left in plan mode came back able to bypass permissions.cctabs sessions --jsonnow recordspermission_modeper tab andrestorehands it back withclaude --permission-mode <mode>, appended after the configured flags so it wins. It composes with--allow-dangerously-skip-permissions, which only makes bypass available rather than selecting it. The mode is read from Claude's own footer rather than from the transcript, because the transcript'spermission-modeentries are written at turn boundaries, not when the mode changes — cycling shift+tab through manual → plan → bypass leaves the recorded value untouched until the next prompt is submitted. Values are validated against what the flag actually accepts (defaultoccurs in real transcripts and is rejected by the flag), and entries with no recorded mode fall back to the configured flags with a count reported rather than silently. Modes round-trip through--manifestonly: a scan-mode restore rebuilds tabs whose sessions are already gone, and a tab with no session has no footer to read. - Plugin: the output-buffer subscriber no longer gives up. It polled for a tab's session for ~5 minutes and then stopped, but Tabby attaches a restored tab's session only once that tab has been focused — which can be hours later or never. Any tab not visited inside that window was left permanently uncaptured, which is the root of the "dead tab" report above. It now polls eagerly for the first minute, then every 5s for as long as the tab lives, stops on destroy, and attaches immediately on focus. Needs a rebuilt
tabby-cctabs; without it the CLI-side pid check already prevents the damage.
0.5.0 — 2026-08-03
- Wave Terminal support is withdrawn. Tabby is the supported terminal. Wave was a working backend through 0.4.x, but it had degraded to the point where a tab would open and the Claude session inside it often never started — a failure that looks exactly like a cctabs bug, costs real time to diagnose, and was only ever reachable on the Wave path. Rather than leave a trap in the install funnel, the adapter is removed:
src/core/wave.tsandsrc/core/wave-db.tsare gone, and running any cctabs command under Wave now exits non-zero with a message pointing at Tabby. Wave is still detected — deliberately — so that message can say "support was withdrawn" instead of "an unrecognised terminal", which would read as a bug. Nothing is lost in the move: Claude conversations live in~/.claude/projects, not in the terminal, socctabs restorereopens them by name once Tabby and its companion plugin are installed.cctabs doctorstill runs under Wave, so a stranded user can see what was detected and what to do about it. cctabs doctor --fixand--yesare gone. Their only action was repairing Wave's orphan-tabid SQLite bug, which left with the Wave backend.doctoris now diagnosis-only: detected terminal (and how — env,CCTABS_TERMINALoverride, or the SSH plugin probe), whether a login+interactive shell can findnode, and the Tabby plugin's health endpoint. The manual SQL for the Wave DB repair is preserved innotes/waveterm-blockslist-orphan-tabid.md.- Fix: the install instructions told you to install a package that doesn't exist. Both the README and the Getting Started guide said
npm install -g cctabs; the published package is@generativereality/cctabs, and the bare name 404s on the registry. Anyone following the npm path off the docs site hit a dead end before reaching a terminal check at all. - Fix: the CLI's own unsupported-terminal error recommended Wave. Landing in an unrecognised terminal printed "cctabs currently requires Wave Terminal" and
brew install --cask wave— funnelling new users straight at the backend that doesn't work. It now points at Tabby, and namessrc/core/tabby.tsas the reference implementation for anyone adding an adapter. - Docs, skill, and installer are consistent about the terminal requirement for the first time: the README,
install.sh, the CLI error text, and the docs site had four different answers between them. - A tab that Claude was actively working in could not be restored. Tabby shows a tab's live terminal title whenever it has no
customTitleof its own — and Claude Code prefixes that title with a spinner glyph while it is busy — so the tab's identity flickered betweencareer-strategyand✳ career-strategydepending on when you looked. Every name-based lookup missed it in the busy state:cctabs sessionsreported no session id,cctabs restoresaidno session found, skippingand left the tab dead, andcctabs sortranked the single most recently active tab last as(no session). Titles are now normalized where they enter cctabs, so a leading status glyph can't change what a tab is. Tabs created bycctabs newwere never affected — they carry acustomTitle— so this only ever hit tabs opened by hand. cctabs sortaccepts--dry, matchingrestoreandresume.--dry-runstill works.cctabs sortis documented — in the command reference, the README, and the skill. It had shipped undocumented everywhere except one changelog line.
0.4.10 — 2026-07-25
- Backend presets inherit into child tabs.
new/resume/forkresolved-bindependently, so a tab spawned from inside a session already running under a backend silently fell back to the plainanthropicpreset. That matters more now that a preset can represent a different Claude account (env_CLAUDE_CODE_OAUTH_TOKEN+env_CLAUDE_CONFIG_DIR) rather than just a different model provider — forgetting-bon a spawned sub-task tab meant it quietly ran on the wrong account. Each launched tab's claude process now carriesCCTABS_ACTIVE_BACKEND=<name>, which its childcctabsinvocations inherit and default to. Explicit-bstill wins, and-b anthropicforces the default back.forkhad no backend support at all before this; it now matchesnew/resume. - Sessions belonging to a second Claude account are no longer invisible. A backend preset can point
CLAUDE_CONFIG_DIRat its own directory (env_CLAUDE_CONFIG_DIRin~/.config/cctabs/config.toml), and every session launched under it lives there rather than in~/.claude/projects. Session discovery only ever looked in the default location, so such a tab was reported asno session named "…" found in any projectbyrestore, andcctabs sessions --jsonemitted it with a nullsession_id— making the manifest unusable for that tab and leaving it to be resumed by hand after every reboot. Discovery now searches the default config dir plus every config dir named by a backend preset (plus whateverCLAUDE_CONFIG_DIRthe current process is running under), acrossrestore,resume,sessions,rename,sort,exportand session-id expansion. Same-name and same-id collisions across config dirs resolve newest-first, consistent with the existing multi-project rule, andrestorenames the account it picked. - …and they relaunch under the right account, automatically. Each discovered session now reports which config dir it came from, which is exactly what identifies its backend — no new per-tab bookkeeping.
restoreandresumecarry that through to the launch, setting the preset's env (or a bareCLAUDE_CONFIG_DIRwhen no preset names the directory) for both the in-place resume and the spawn path. This matters more than a missing-session error would:claude --resume <id>in the wrong config dir doesn't fail, it just can't find the id and opens a fresh conversation — a silent wrong restore.cctabs sessions --jsonnow emitsbackend/config_dir,restore --manifestreads them, andrestoreinfers both from the session id alone when a manifest predates the fields. Socctabs sessions --json | cctabs restore --manifest - --create-missingrestores a second-account tab correctly with no manual flags. cctabs resumepicks the backend from the session it found. Previously the only default wasCCTABS_ACTIVE_BACKEND— the backend of whatever tab you happened to run the command from — which is the wrong answer whenever the session you're resuming belongs to a different account. Precedence is now explicit-b, then the session's own config dir, then the inherited one; the success line says which ([backend: gapminder (from session)]). Resuming a second-account session no longer needs-band-sspelled out by hand.restoreis now one implementation.cctabs restore [dir]andcctabs restore --manifesthad grown into two divergent code paths that made different decisions about the same tab. The bare scan now builds entries from the tabs it finds and runs them through the same planner as manifest mode, so resolve → attach → spawn → reorder → summary exists exactly once. Behaviour converges on the better half of each: manifest mode gains the scan's dead-tab handling (a tab whose terminal is confirmed gone is rebuilt around the resume instead of having a command typed into a shell that isn't there) and its duplicate-name dedup, while the scan gains manifest mode's worktree-aware, newest-wins session lookup — socctabs restore <dir>with two same-named sessions now resumes the newest instead of skipping the tab as ambiguous. A dead tab whose name is already live in another tab is no longer restored into a second copy of the same session.- Fix:
--dryno longer promises restores a real run wouldn't perform. In the scan path a dry run skipped both the empty-scrollback confirmation and the duplicate-dead-tab dedup, so it happily listed tabs that a real run would instead have closed as duplicates, and reported "would send" for tabs a real run would rebuild. Planning is now a single read-only pass shared by both modes, with--drystopping immediately after it — the dry output is the real run's decisions, and planning cannot mutate a tab by construction (there is a test that fails if it ever tries). - Fix:
restore/resumeno longer mistake a longer-named tab for the session's own. Tab resolution falls back to prefix matching, so resuminggapmindermatched a livegapminder-logintab and skipped the resume with "already running" for a session whose tab wasn't open at all. Deciding whether a session's tab already exists is now exact-name (or full-id) matching in bothrestoreandresume; hand-typed lookups (send,close,rename,fork,scrollback,export) keep the prefix convenience. - Faster
restoreon Tabby: missing tabs spawn in parallel again — safely this time. The two restore paths disagreed about whether parallel spawning was safe, and the pessimistic one was right: a Tabby tab only spawns its process once its terminal frontend attaches, which only happens once the tab has been focused, andAppServiceemits that focus event asynchronously against whichever tab is active by then. Fire several creates in one turn and the losers never start Claude at all. The plugin now serialises tab creation internally and doesn't answer until the new tab's process is actually running, advertising this as thespawn-waits-for-ptycapability on/api/health; restore probes for it and only then spawns in parallel, falling back to the old serial-with-settle pace against an older plugin. Needs the rebuilttabby-cctabsplugin — without it nothing breaks, restore is just as slow as before.
0.4.9 — 2026-07-22
- Fix:
restore --manifestnow rebuilds the tab bar in manifest order. Tab-order restoration (added in 0.4.5) only ever ran in the no-manifest name-scan path; arestore --manifest --create-missingspawned the missing tabs appended to the end of the bar (and, on Tabby, spawned in parallel so even their relative order was nondeterministic), never re-applying the order the manifest carried. Manifest mode now records each entry's final tab id (existing match, current tab, or freshly spawned) and callsreorderTabsas a separate final step after all spawns complete — the parallel spawn is left untouched. Skipped under--dry. (Internal: the no-manifest path'srunLegacyModeis renamedrunNameScanMode— it was never deprecated, just older than manifest mode.) - More robust
restore/resumepicker handling, plus the follow-up mobile-app overlay. Under heavy load (e.g. right after a largerestore) a tab could stay stuck on Claude's "Resume from summary / full session" picker, and cctabs knew nothing about the "Continue coding in the Claude mobile app … Enter/Esc to close" remote-control info overlay that can paint immediately after the session loads — leaving the tab on an overlay rather than a clean prompt.confirmResumePickernow (a) waits adaptively for the picker (patient under load, but early-exits the moment the session is demonstrably loaded without one, so the common no-picker resume no longer burns the whole window), (b) retries the confirm more times for a slow load, and (c) sweeps for the mobile-app overlay — on both the picker and direct-resume paths — and dismisses it with Esc. The send-↓-once / retry-Enter-only safety is preserved, so a retry can never land on option 3 ("Don't ask me again"). cctabs renamenow persists the new name to disk soresumecan find it. Previouslycctabs rename(and Claude's in-session/rename) only relabelled the live tab / remote-control session, never thecustomTitlerecorded in the session's.jsonl— which is whatcctabs resume <name>/restoresearch by. A session renamed that way became unfindable by its new name.cctabs renamenow also appends acustom-titleentry (the same line shape Claude writes at launch) to the resolved session's transcript, socctabs resume <newName>works afterwards. Claude's own/renamestill doesn't touch disk — that remains a documented limitation (see the "Two names" section of the skill).- Fix:
restorecould send a session into a directory with no transcript at all, if the agent had evercd'd into a subdirectory. Regression in 0.4.8's own fix (see below). A session's recorded cwd changes for two different reasons: (a) the session was genuinely relaunched from a new directory (0.4.8's case — the transcript moves with it), or (b) the agent rancd <subdir>via the Bash tool mid-session, which drifts the per-message cwd without the transcript file ever moving. 0.4.8 couldn't tell these apart and would sendrestore/resumeinto the drifted directory, failing with "No conversation found with session ID: ...". BothresolveTabSessionandfindSessionsByNameGloballynow only accept a recorded cwd whose own project slug matches the directory the transcript is actually stored under — a drifted cwd is skipped in favor of an earlier, matching one.
0.4.8 — 2026-07-18
- Fix:
restorecould try to relaunch a session into a directory that no longer exists. Session cwd was resolved from the first recorded location in a session's transcript, not the most recent one. That's wrong once a session's working directory has changed mid-life — most commonly: a--worktreetab's worktree gets deleted, and the session is later manually resumed from the repo root instead. A subsequentcctabs restore(plain or--manifest) would then try tocdback into the deleted worktree path, ignoring the relocation. BothresolveTabSessionandfindSessionsByNameGloballynow track the last recorded cwd, matching the existing last-wins handling for renamed sessions (customTitle). This fix had its own regression — see 0.4.9 above, fixed in the very next release.
0.4.7 — 2026-07-04
- Drive a remote Tabby over SSH. cctabs can now open / list / close / send tabs on another machine's Tabby over SSH. Over SSH the parent terminal never exports
TERM_PROGRAM, so cctabs previously refused with "unrecognised terminal" even though the target host's cctabs plugin was running and reachable on127.0.0.1:3300. It now (a) auto-falls back to probing that plugin when environment detection comes up empty — so a baressh host 'cctabs new foo "~"'just works when the remote plugin is up — and (b) honours an explicitCCTABS_TERMINAL=tabby(aliasCCTABS_BACKEND) override to force the backend regardless ofTERM_PROGRAM.cctabs doctorreports the resolved backend and how it got there (override vs plugin probe). The probe only runs when detection is otherwise unknown, so a recognised local terminal never pays the network round-trip. - Per-install name prefix for new sessions. A new
defaults.prefixconfig setting (empty by default) is prepended to both the Tabby/Wave tab title and theclaude --namefor every name minted bynew,resume, andfork. Set it to disambiguate a machine when several share one claude.ai remote-control session list, where unprefixed names otherwise collide.
0.4.6 — 2026-06-30
- Fix:
cctabs new --worktreeno longer spawns at the wrong commit. Previously cctabs delegated worktree creation toclaude --worktree <name>, which can branch from the upstream tracking ref (or another unexpected commit) when local commits aren't pushed — silently producing a worktree at a stale base. Now cctabs runsgit worktree additself, explicitly anchored to the target dir's current HEAD, then launches plainclaudeinside the worktree. The success line prints the base SHA so you can verify it's what you expect. If a branch namedworktree-<name>already exists, cctabs checks it out at its existing tip and warns when that differs from HEAD.
0.4.5 — 2026-06-09
- New tabs open right after the active tab (Tabby).
new/fork/resumepreviously dropped the new tab at the far end of the bar; it now lands immediately after the tab you created it from, browser-style. (Wave keeps append behaviour.) - Restore rebuilds the pre-reboot tab order (Tabby). Recreated dead tabs were appended, scrambling the original layout; restore now captures the pre-reboot order and reorders the bar to match once every tab is back. Both positioning changes need the rebuilt
tabby-cctabsplugin to take effect — with an older plugin the CLI degrades gracefully (tabs append as before). - Restore now auto-advances the "Resume from summary / full session" picker. When
claude --resumereattaches a large or old session it shows a blocking three-way picker; previously arestoreleft every such tab stuck on it (the auto-confirm logic only ran when seeding an initial prompt, which restore doesn't).restorenow detects the picker and selects option 2, "Resume full session as-is" — the whole point of restore is to bring the conversation back intact, not a lossy summary. It moves the cursor down exactly once (never risking option 3, "Don't ask me again", which would permanently silence the prompt) and retries only the confirm. - Restore no longer double-creates tabs that share a name. After a reboot it's possible to have two dead tabs with the same name; restore was recreating each, spawning duplicate live tabs that both resumed the same (newest) session. Restore now keeps the first tab per name and closes the extras.
0.4.0 — 2026-05-16
cctabs export+cctabs import— move tabs and their Claude sessions between machines.cctabs export <tab>(or--allfor the whole workspace) bundles each tab's Claude conversation jsonl plus a small manifest into a.tar.gz. On the other machine,cctabs import <archive>extracts the jsonls into the local~/.claude/projects/<target-slug>/and opens a tab thatclaude --resumes each session. The target's Claude project slug is recomputed from the resolved target cwd, so cross-machine$HOMEdifferences just work. Worktree-backed tabs (cctabs new --worktree) are handled correctly — export falls back to scanning<cwd>/.claude/worktrees/*when the direct slug lookup misses, and records the actual worktree path in the manifest so import recreates the right slug on the target.--cwd <path>onimportremaps a single-tab archive to a different directory.--dry-runpreviews everything without copying files or spawning tabs.--forceoverwrites a session jsonl that's already present locally.- Uses the system
tarbinary — no new npm deps. - Note: jsonl contents are not rewritten on import. Absolute paths from the source machine remain in the conversation history as historical references; Claude adapts to the actual current cwd on resume.
0.3.2 — 2026-05-13
- Fix:
cctabswith no arguments crashed. The default command (which dispatches tosessions) was passing a fake context shaped like{ args: {} }tosessionsCommand.run, butsessions --json(added in 0.3.1) readsctx.values.json, so the missingvalueskey threwCannot read properties of undefined (reading 'json'). Regression from 0.3.1.
0.3.1 — 2026-05-12
- Tabby: active-session detection now survives viewport padding and spinner redraws. Previously, Tabby tabs in the middle of a long Claude turn were misreported as
terminal/unknownbecause the scrollback window only sampled the last 10 rows — Claude's animated status line lives further up. The detector now scans a 200-line tail and matches against spinner labels (Thinking,Composing,Worked for…, etc.) and Claude's brand glyphs. cctabs new --resume <name>to open a tab and resume a named session in one step.- Manifest-driven restore. Each tab writes a manifest under
~/.cctabs/, socctabs restorecan reopen every tab and resume every Claude session after a reboot — no need to remember names. cctabs send --wait-for-promptwaits until the tab is at a Claude prompt before delivering input, instead of racing the previous turn.cctabs sessions --jsonfor scripting.- Skill: sharper triggers. The Claude Code skill now lists explicit trigger phrases ("open a new tab", "in another tab", "fork this tab"…) and disambiguates "tab" from the Agent tool, so Claude stops spawning background subagents when you asked for a real terminal tab.
0.3.0 — 2026-05-10 (not tagged; folded into 0.3.1)
- Tabby Terminal support. cctabs now works on Tabby in addition to Wave. Install the companion plugin (
tabby-cctabson npm; available from Tabby → Settings → Plugins) and run the CLI normally — the backend is auto-detected. cctabs doctorprints a diagnostic of the current backend, the running plugin, and known orphaned tab-ids — useful when Wave or Tabby state drifts out of sync.cctabs new --backend <name>for Ollama, Kimi, Qwen, and local-model presets. Spawns Claude Code wired to the chosen backend without per-tab env juggling.
0.1.3 — 2026-04-25
cctabs restoresearches every Claude project directory by default instead of only the current working directory. Restore now Just Works after a reboot regardless of which terminal/cwd you start it from.- Survive Wave restart with ephemeral workspace. Resume keeps working across Wave restarts; the workspace state no longer becomes stale.
--sessionaccepts prefixes, so you can resume by partial name.
0.1.2 — 2026-04-22
- Prefer exact tab-name match over prefix match in tab resolution. Stops
cctabs send apifrom accidentally hittingapi-v2. - Recreate dead tabs on resume + handle spaces in project paths.
- Bump wsh
blocks listtimeout socctabs newno longer flakes on large Wave sessions. - Skill: parallel-work guidance + worktree decision guide.
0.1.1 — 2026-04-10
- Skill renamed from
herdtocctabsto match the package name everywhere.
0.1.0 — 2026-04-10
- Initial public release:
cctabs new,fork,close,send,sessions,scrollback,restorefor Wave Terminal. - Claude Code skill (
cctabs) lets Claude orchestrate its own sibling sessions.