Part 2: From cct to cad — when a personal fork stops being a fork

TL;DR — My fork of Simon Willison’s transcript renderer ate enough features that it stopped being a transcript renderer. It now manages sessions across claude, codex, pi, opencode, and forge from one picker, resumes them in the right agent CLI, and does the full project-folder migration (mv + claude state dirs + JSONL cwd rewrite + backups) when you rename a project on disk. New name: cad (Coding Agent Driver). Repo: github.com/ekinertac/cad.

Last time I wrote about forking Simon Willison’s claude-code-transcripts to fix some personal pain points — folder-grouped picker, Global Sessions, the night I deleted 1,628 runaway loop sessions, an iMessage-style dark theme. I closed with “there’s a particular kind of joy in not asking permission” and figured I’d be done.

I was not done.

The thing that broke the “personal fork” framing for me was a slow realization: the workflow I was fixing wasn’t claude-specific. I use Claude Code 90% of the time, but the rest of my AI coding life happens in codex, pi, opencode, and forge. Each of those CLIs stores sessions somewhere. Each has its own resume command. Each one is a tiny half-tool with a session list inside it. There’s no overlap between any of them.

If I started a session in ~/Code/foo with claude, then later wanted to revisit it via codex or opencode, I had to remember which agent I used, switch CLIs, navigate their resume picker. The transcript renderer I’d forked was solving a different problem; the actual pain was that my own session history was balkanized across five tools.

So the fork started growing in a direction the original wasn’t built for.

Five agents, one picker

Each of the five agents stores sessions completely differently:

AgentStorageResume
claude~/.claude/projects/<encoded-cwd>/<uuid>.jsonlclaude --resume <uuid>
codex~/.codex/sessions/YYYY/MM/DD/rollout-<ts>-<uuid>.jsonlcodex resume <uuid>
pi~/.pi/agent/sessions/<encoded>/<ts>_<uuid>.jsonlpi --session <uuid>
opencode~/.local/share/opencode/opencode.db (SQLite)opencode --session <id>
forge~/forge/.forge.db (SQLite)forge --conversation-id <id>

Two JSONL providers with path-encoded folders. One JSONL provider with date organization. Two SQLite databases. Different encoding schemes. Different filter rules in their pickers. All of them store a cwd somewhere, though, even when their folder structure doesn’t.

Grouping by cwd instead of folder name turned out to be the right key. A claude session and a codex session both started in ~/Code/foo now show up in one project entry. The badge tells you the mix: arcade (6c+5o+1f) means six claude sessions, five opencode, one forge — all in the same directory.

self-healing-crawler    2026-05-10 13:34   15 sessions  (10c+1p+1o+3f)
arcade                  2026-05-10 21:32    9 sessions  (3c+5o+1f)
Global Sessions         2026-05-10 23:40   95 sessions  (34c+3x+3p+50o+5f)

That second-to-last column was my “huh, neat” moment. I’d been working across all four providers on self-healing-crawler without remembering which I’d used when. Now I can scroll the list and see whichever session I want, regardless of which agent CLI I happened to launch that day.

The session manager that used to be a transcript viewer

Once the picker spans five agents, the question changes. Rendering HTML transcripts is still occasionally useful, but the thing you actually want to do 95% of the time is resume the session in its agent. So Enter on a session row stopped meaning “render to HTML” and started meaning “chdir to the recorded cwd, then exec the right CLI with the right resume command”:

  • Press Enter on a claude session → claude --dangerously-skip-permissions --resume <uuid>
  • Press Enter on a codex session → codex resume <uuid>
  • Press Enter on a pi session → pi --session <uuid>

Same row, same key, totally different agent. The “transcript viewer” hat is now a h keypress (claude only — the other providers’ schemas need separate renderers).

Enter resuming means the parent shell needs to follow the session into its project directory after the agent exits. Child processes can’t cd their parent, so this needs a shell wrapper:

1eval "$(cad shell-init zsh)"

That installs a wrapper function that passes a temp-file path via env var, and the binary writes the project cwd to that file before exec’ing the agent. On exit, the wrapper reads it and cds. Standard zoxide-style trick, transparent in practice.

Other one-letter keys piled on naturally:

  • r to rename — write a custom title to ~/.cad/titles.json. Wins over the provider’s own summary in the picker display.
  • s to summarize — pipes an excerpt of the session to an LLM and saves the returned 3-7 word title.
  • m to move — point a session at a different project’s cwd.
  • p to peek — open the user-prompt/assistant-reply pairs in $PAGER (less, by default). Tool calls are filtered out. Quick Look for chat sessions.
  • n to start a fresh session in the current project.
  • / to enter modal search (fzf-style — couldn’t have plain typing both filter and trigger h, so fzf’s pattern won).
  • Esc/Backspace to back out one level, q to quit.

There’s a small narrative arc inside s: I originally implemented it against the Anthropic SDK directly. It worked once and then immediately failed with “credit balance too low.” I switched to shelling out to claude -p thinking the subscription auth would kick in — but claude -p honors ANTHROPIC_API_KEY env var first, so it routed through the depleted API anyway. The fix was switching to codex exec --ephemeral, which uses my ChatGPT account auth and bypasses the whole API-credits dimension. Also --ephemeral keeps the summarize call from creating its own codex session that would then show up in the picker. (Yes, I had to clean those up too.)

The rename rabbit hole

This is the section that took the most iterations and was the most interesting to debug.

The pain: I’d start a session at ~/Code (one of my Global Sessions catch-all folders). Eventually the conversation became a real project and I’d mkdir ~/Code/something and cd into it. But the session’s recorded cwd was still ~/Code. So in my project list, all my “became a project” sessions were stuck under Global Sessions. And if I renamed a project directory later — mv ~/Code/foo ~/Code/bar — the session’s cwd pointed at a path that didn’t exist anymore. claude --resume from the new directory found nothing.

Easy v1: cad’s m and r actions write a sidecar at ~/.cad/cwd-overrides.json. Discovery applies overrides before grouping. Agent files untouched, fully reversible. Sidecar overrides only matter to cad. Pressing r rebranded a project in my picker; claude --resume was still looking at the original folder. It worked in cad and broke in claude.

What followed was an experimental rabbit hole I’m still slightly embarrassed about. I copied JSONLs to a new folder, ran claude -r, saw two phantom sessions appear. Copied more, rewrote the cwd field inside the JSONL, still nothing. Spent a confused half-hour assuming claude had a recency filter or a session-type filter I wasn’t seeing.

The actual answer came from Vincent Schmalbach’s blog post on migrating sessions across computers: claude’s session state lives in four parallel directories under ~/.claude/, all keyed by the same path encoding:

~/.claude/projects/<encoded-cwd>/
~/.claude/file-history/<encoded-cwd>/
~/.claude/todos/<encoded-cwd>/
~/.claude/shell-snapshots/<encoded-cwd>/

And the encoding I’d been using was wrong: /Users/x/Code/humbl.ai doesn’t encode to -Users-x-Code-humbl.ai, it encodes to -Users-x-Code-humbl-ai. Both / AND . become -. I’d been doing only /. Confirmed against my own filesystem and updated.

The “real” project rename in cad now does six things in order, with a confirmation that lists each step:

  1. Backup all four state dirs to ~/.cad/agent-backups/<ts>/.
  2. Move every state dir that exists for this project (most projects only have projects/; aux dirs only appear when claude actually used them).
  3. Rewrite the cwd field in every JSONL line under the new projects/<new-enc>/. Preserves mtime so the picker sort order stays stable across the migration.
  4. mv the user’s actual project directory (the part the user would have done manually before).
  5. For non-claude providers (codex/pi/opencode/forge), fall back to the sidecar override — their storage isn’t path-encoded so there’s nothing on disk to move.
  6. Clear the claude sidecar overrides for the migrated sessions. The JSONL is now the source of truth — keeping the sidecar would create double bookkeeping.

End-to-end test on a real project (after a few failed attempts that taught me to test on another project, not the live one): cd ~/Code/<renamed-dir> && claude -r finds the migrated sessions. Verified bit by bit: encoded folder gone from old name, present at new, cwd inside JSONL rewritten, sidecar entries cleared, backup intact.

The destructive testing did unearth a separate bug: cad was showing phantom sessions I’d never created. Turned out my own SessionEnd hook was firing claude -p to write digests to a vault, and each claude -p invocation produced its own JSONL marked with queue-operation events. Claude’s own --resume picker filters those out (they’re not interactively resumable conversations). cad was listing them anyway. Quick fix: skip any JSONL whose first 50 lines contain a queue-operation event, matching claude’s behavior. Three sessions in my test project dropped to one — exactly what claude -r showed.

When the fork stopped being a fork

After all of this — five providers, custom picker, title management, real folder migration with backups — I looked at the project and noticed how little of Simon’s original transcript-rendering code I was actually exercising day-to-day. cad json, cad all, cad web are all still there, unchanged from upstream, but they’re the third-most-used commands behind the picker.

The metadata still said the tool was “for converting Claude Code session files to HTML.” It was, but it had also quietly become the default way I navigate any agent session. Naming the binary cct after claude-code-transcripts stopped describing what it did.

So I renamed it. The new name is cadCoding Agent Driver. Three letters, tab-completes uniquely against claude, free both on the user’s PATH and on the GitHub namespace (PyPI’s cad is taken, but I’m not publishing there). Detached the project metadata from the fork — updated URLs in pyproject.toml, rewrote the README around the session-manager framing, kept Simon’s authorship credit on the HTML-rendering code. LICENSE unchanged. The git history is preserved at the new repo.

The original fork — ekinertac/claude-code-transcripts — is still on GitHub as a fork in the technical-relationship sense. The tool inside it is cad. I’ll detach the GitHub fork-network label when I get around to it. Both repos point at the same code; the new one’s just at a name that matches what it does.

What I’m taking from this

A few things have crystalized for me across writing both posts:

The cost of adapting tools to your exact workflow is now an afternoon. I wrote in Part 1 that this was the natural endpoint of using AI to code. After another month of working on this, I think it goes further: the limiting factor isn’t writing the code, it’s having a clear enough picture of the workflow you want. The cad design changed three times in the past month, and each rewrite was a few hours’ worth of code. The actual hard part was each “wait, what am I really trying to do?”

Sidecar overrides are a great starting point and an insufficient ending point. The cwd-override sidecar I built first was a perfectly correct solution to half the problem. When I needed the other half — claude --resume actually finding the sessions — there was no shortcut around touching agent files. With backups, atomic writes, and a confirmation prompt, the destructive path was fine. But I had to walk through the chain of “but does X work for it?” four times before getting there.

A fork can stop being a fork without you noticing. I’d been adding features for weeks before I clocked that I was building a different tool with Simon’s original tool nested inside it. The rename felt almost ceremonial when it finally happened. The code wasn’t really changed; just the framing.

The whole thing is at github.com/ekinertac/cad now. Same caveat as last time: there are bits worth lifting and bits that only make sense on my machine. The multi-agent grouping by cwd is probably the most generally-useful idea in here. The project rename with the four-dir migration is the most useful destructive operation I’ve ever built — but read the blog post about hooks before running it on a project whose claude session is live, because moving the JSONL claude is actively writing to is its own kind of cursed.

Two months ago this was a CSS rewrite and a session deduper. Now it’s a five-agent session manager I run more often than claude itself. Different tool. Same starting point.