Skip to content

Changelog

What's new in cctabs. Source of truth lives in CHANGELOG.md on GitHub.

Install or update:

sh
# Claude Code plugin (recommended)
/plugin install cctabs@generativereality

# Or as a standalone CLI
npm i -g @generativereality/cctabs

0.5.1 — 2026-08-12

  • restore called a live session's tab dead, and closed it. detectSessionStatus read the Tabby plugin's captured output and treated an empty result as "no session here" — reported as unknown, 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/tabs already reports each tab's pid, so a tab that can't be read but is running is reported and left alone — restore can neither type claude --resume into 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. unknown is renamed unreadable throughout, because the word was the bug.
  • active meant "a Claude session exists", not "it's working". Every marker was matched into active — 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 58 active and 0 idle, 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 missed Sautéed for and Brewed for on 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.flags produced, so a tab deliberately left in plan mode came back able to bypass permissions. cctabs sessions --json now records permission_mode per tab and restore hands it back with claude --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's permission-mode entries 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 (default occurs 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 --manifest only: 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.ts and src/core/wave-db.ts are 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, so cctabs restore reopens them by name once Tabby and its companion plugin are installed. cctabs doctor still runs under Wave, so a stranded user can see what was detected and what to do about it.
  • cctabs doctor --fix and --yes are gone. Their only action was repairing Wave's orphan-tabid SQLite bug, which left with the Wave backend. doctor is now diagnosis-only: detected terminal (and how — env, CCTABS_TERMINAL override, or the SSH plugin probe), whether a login+interactive shell can find node, and the Tabby plugin's health endpoint. The manual SQL for the Wave DB repair is preserved in notes/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 names src/core/tabby.ts as 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 customTitle of its own — and Claude Code prefixes that title with a spinner glyph while it is busy — so the tab's identity flickered between career-strategy and ✳ career-strategy depending on when you looked. Every name-based lookup missed it in the busy state: cctabs sessions reported no session id, cctabs restore said no session found, skipping and left the tab dead, and cctabs sort ranked 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 by cctabs new were never affected — they carry a customTitle — so this only ever hit tabs opened by hand.
  • cctabs sort accepts --dry, matching restore and resume. --dry-run still works.
  • cctabs sort is 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/fork resolved -b independently, so a tab spawned from inside a session already running under a backend silently fell back to the plain anthropic preset. 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 -b on a spawned sub-task tab meant it quietly ran on the wrong account. Each launched tab's claude process now carries CCTABS_ACTIVE_BACKEND=<name>, which its child cctabs invocations inherit and default to. Explicit -b still wins, and -b anthropic forces the default back. fork had no backend support at all before this; it now matches new/resume.
  • Sessions belonging to a second Claude account are no longer invisible. A backend preset can point CLAUDE_CONFIG_DIR at its own directory (env_CLAUDE_CONFIG_DIR in ~/.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 as no session named "…" found in any project by restore, and cctabs sessions --json emitted it with a null session_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 whatever CLAUDE_CONFIG_DIR the current process is running under), across restore, resume, sessions, rename, sort, export and session-id expansion. Same-name and same-id collisions across config dirs resolve newest-first, consistent with the existing multi-project rule, and restore names 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. restore and resume carry that through to the launch, setting the preset's env (or a bare CLAUDE_CONFIG_DIR when 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 --json now emits backend / config_dir, restore --manifest reads them, and restore infers both from the session id alone when a manifest predates the fields. So cctabs sessions --json | cctabs restore --manifest - --create-missing restores a second-account tab correctly with no manual flags.
  • cctabs resume picks the backend from the session it found. Previously the only default was CCTABS_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 -b and -s spelled out by hand.
  • restore is now one implementation. cctabs restore [dir] and cctabs restore --manifest had 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 — so cctabs 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: --dry no 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 --dry stopping 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/resume no longer mistake a longer-named tab for the session's own. Tab resolution falls back to prefix matching, so resuming gapminder matched a live gapminder-login tab 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 both restore and resume; hand-typed lookups (send, close, rename, fork, scrollback, export) keep the prefix convenience.
  • Faster restore on 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, and AppService emits 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 the spawn-waits-for-pty capability 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 rebuilt tabby-cctabs plugin — without it nothing breaks, restore is just as slow as before.

0.4.9 — 2026-07-22

  • Fix: restore --manifest now 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; a restore --manifest --create-missing spawned 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 calls reorderTabs as a separate final step after all spawns complete — the parallel spawn is left untouched. Skipped under --dry. (Internal: the no-manifest path's runLegacyMode is renamed runNameScanMode — it was never deprecated, just older than manifest mode.)
  • More robust restore/resume picker handling, plus the follow-up mobile-app overlay. Under heavy load (e.g. right after a large restore) 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. confirmResumePicker now (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 rename now persists the new name to disk so resume can find it. Previously cctabs rename (and Claude's in-session /rename) only relabelled the live tab / remote-control session, never the customTitle recorded in the session's .jsonl — which is what cctabs resume <name> / restore search by. A session renamed that way became unfindable by its new name. cctabs rename now also appends a custom-title entry (the same line shape Claude writes at launch) to the resolved session's transcript, so cctabs resume <newName> works afterwards. Claude's own /rename still doesn't touch disk — that remains a documented limitation (see the "Two names" section of the skill).
  • Fix: restore could send a session into a directory with no transcript at all, if the agent had ever cd'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 ran cd <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 send restore/resume into the drifted directory, failing with "No conversation found with session ID: ...". Both resolveTabSession and findSessionsByNameGlobally now 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: restore could 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 --worktree tab's worktree gets deleted, and the session is later manually resumed from the repo root instead. A subsequent cctabs restore (plain or --manifest) would then try to cd back into the deleted worktree path, ignoring the relocation. Both resolveTabSession and findSessionsByNameGlobally now 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 on 127.0.0.1:3300. It now (a) auto-falls back to probing that plugin when environment detection comes up empty — so a bare ssh host 'cctabs new foo "~"' just works when the remote plugin is up — and (b) honours an explicit CCTABS_TERMINAL=tabby (alias CCTABS_BACKEND) override to force the backend regardless of TERM_PROGRAM. cctabs doctor reports 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.prefix config setting (empty by default) is prepended to both the Tabby/Wave tab title and the claude --name for every name minted by new, resume, and fork. 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 --worktree no longer spawns at the wrong commit. Previously cctabs delegated worktree creation to claude --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 runs git worktree add itself, explicitly anchored to the target dir's current HEAD, then launches plain claude inside the worktree. The success line prints the base SHA so you can verify it's what you expect. If a branch named worktree-<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 / resume previously 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-cctabs plugin 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 --resume reattaches a large or old session it shows a blocking three-way picker; previously a restore left every such tab stuck on it (the auto-confirm logic only ran when seeding an initial prompt, which restore doesn't). restore now 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 --all for 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 that claude --resumes each session. The target's Claude project slug is recomputed from the resolved target cwd, so cross-machine $HOME differences 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> on import remaps a single-tab archive to a different directory. --dry-run previews everything without copying files or spawning tabs. --force overwrites a session jsonl that's already present locally.
  • Uses the system tar binary — 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: cctabs with no arguments crashed. The default command (which dispatches to sessions) was passing a fake context shaped like { args: {} } to sessionsCommand.run, but sessions --json (added in 0.3.1) reads ctx.values.json, so the missing values key threw Cannot 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 / unknown because 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/, so cctabs restore can reopen every tab and resume every Claude session after a reboot — no need to remember names.
  • cctabs send --wait-for-prompt waits until the tab is at a Claude prompt before delivering input, instead of racing the previous turn.
  • cctabs sessions --json for 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-cctabs on npm; available from Tabby → Settings → Plugins) and run the CLI normally — the backend is auto-detected.
  • cctabs doctor prints 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 restore searches 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.
  • --session accepts 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 api from accidentally hitting api-v2.
  • Recreate dead tabs on resume + handle spaces in project paths.
  • Bump wsh blocks list timeout so cctabs new no longer flakes on large Wave sessions.
  • Skill: parallel-work guidance + worktree decision guide.

0.1.1 — 2026-04-10

  • Skill renamed from herd to cctabs to match the package name everywhere.

0.1.0 — 2026-04-10

  • Initial public release: cctabs new, fork, close, send, sessions, scrollback, restore for Wave Terminal.
  • Claude Code skill (cctabs) lets Claude orchestrate its own sibling sessions.

Released under the MIT License.