Part 1: Forking Simon’s Tool Because My Sessions Folder Was On Fire

Back in December, Simon Willison wrote about a small CLI he built called claude-code-transcripts. The pitch is simple: Claude Code stores every session as a JSONL file under ~/.claude/projects/, and Simon wanted a clean way to convert one into a sharable HTML page. Pick a session, get a paginated transcript with all the prompts, tool calls, and code edits laid out in a way you can actually scroll through. Optionally publish it as a Gist with one flag.

I started using it heavily. Like, embarrassingly heavily. I have a habit of rambling out loud to Claude Code about half-formed ideas, and the transcripts turned out to be a much better record of “what was I doing on Tuesday” than my actual notes. Tuesday Me ships features. Wednesday Me reads what Tuesday Me did. The tool was perfect for that.

Until it wasn’t.

The picker that hated me

Run claude-code-transcripts (or just claude-code-transcripts local) and you get an interactive picker:

2026-05-09 11:18    18 KB  Fix the gist preview links
2026-05-09 09:42   384 KB  Read the Claude Code session transc...
2026-05-08 22:01    42 KB  Add folder grouping to local picker
...

Date, size, summary. The summary is the first prompt of the session, truncated to 50 chars. The folder the session belongs to does not appear anywhere.

This is fine when you have, say, four projects. I have forty. Tab-completing my way through “wait, was that the parser refactor or the templating thing?” by squinting at truncated first prompts is not a winning strategy. You quickly learn that “Add folder grouping to…” could be any of six different repos, and the only way to know is to render the HTML and look.

So I forked it. The first change was small enough to feel almost embarrassing: make the picker two-step. Pick a project folder first, then pick a session within it.

? Select a project:
  > claude-code-transcripts   2026-05-10 14:02   12 sessions
    datasette                 2026-05-09 18:30   34 sessions
    llm                       2026-05-08 09:11   8 sessions
    Global Sessions           2026-05-07 21:54   39 sessions
    ...

The trick is that the first picker is cheap. It only reads filesystem metadata — directory entries, mtimes, file counts. No JSONL summaries are parsed until you actually pick a project. So even with hundreds of sessions on disk, the project list pops up instantly. Summaries get read only for the project you opened.

I also turned on questionary’s built-in use_search_filter so you can type to narrow the list. (You have to give up j/k vim navigation to do this — questionary refuses the combo because typed letters would be ambiguous. Arrow keys still work, fight me.)

Global Sessions, or: where my one-off questions go to die

There’s a folder called ~/.claude/projects/-Users-ekinertac/. There’s another called ~/.claude/projects/-Users-ekinertac-Code/. These correspond to running Claude Code from my home directory and from ~/Code/ respectively — the catch-all places where I run commands like “summarize this PDF” or “why is my docker container being weird”. Not project work. Just digital scratch paper.

In the picker, these showed up as two separate entries called ekinertac and Code (because that’s what the existing display-name helper does to those paths). Two top entries that aren’t really projects, both polluting my recently-active list.

I taught the tool to merge them into a single virtual project called “Global Sessions”. The detection generalizes: it computes the encoded forms of Path.home() and Path.home()/Code and treats those as the merge set. Anywhere else’s home directory will work the same way out of the box.

It’s the kind of tweak you’d never bother contributing upstream. Nobody else’s quick-question folders are at ~/ and ~/Code/. But on my machine it removed two entries’ worth of noise from every picker invocation.

The day I found 1,667 sessions

After I shipped the Global Sessions merge, I ran the new picker and noticed something delightful:

Global Sessions    2026-05-07 21:54    1667 sessions

One thousand six hundred and sixty-seven sessions in two folders. I have used Claude Code for maybe eight months. That’s an average of seven scratch sessions per day, every day, no exceptions. I am very stupid but not that stupid.

I scrolled into the session picker and saw this:

2026-05-07 21:52     32 KB  Read the Claude Code session transcript at: /Us...
2026-05-07 21:52     32 KB  Read the Claude Code session transcript at: /Us...
2026-05-07 21:52     32 KB  Read the Claude Code session transcript at: /Us...
2026-05-07 21:52     32 KB  Read the Claude Code session transcript at: /Us...
... (repeat ~1600 times)

Oh. Oh no.

I had, at some point, spun up a Claude Code session whose entire job was “look at this transcript, then summarize it” — and accidentally pointed it at a directory of transcripts in a way that turned into a self-perpetuating loop. Each iteration spawned a new Claude session, which spawned another, which spawned another. 1,629 sessions in 48 minutes on the night of May 7. I never noticed because the cost was probably absorbed by my subscription and the runaway eventually hit some context limit and stopped.

My first instinct was to assume they’d be exact duplicates and write a find -exec rm one-liner. They weren’t. The summaries looked identical because the picker truncates to 50 chars, but each iteration was reading a different transcript path, so the prompts were all slightly different. Strict (size, mtime, summary) equality found zero duplicates. The 50-char display lied to me.

What worked was clustering by summary prefix + time window: take the first 30 characters of the summary, group sessions whose prefixes match and whose mtimes fall within an hour of each other, demand at least 5 in a cluster. That heuristic caught the loop cleanly:

Found 1 loop cluster(s):

  Folder: -Users-ekinertac-Code
  Prefix: 'Read the Claude Code session t'
  Count:  1629  (2026-05-07 21:06 → 2026-05-07 21:54)
  Total:  51317 KB
  Keep:   20018773-e9fa-444c-a206-6e19912c791e.jsonl  (oldest)
  Delete: 1628 session(s)

One cluster, no false positives. I kept the oldest session in the cluster (the originating one — the user-typed prompt that started the loop) and deleted the other 1,628. The script lives in git history at one specific commit and was deleted in the next. Recoverable, but not cluttering the working tree.

Global Sessions went from 1,667 to 39.

iMessage, but for AI conversations

The default HTML output is a clean, minimal, full-width, light-theme transcript. It’s perfectly fine. It’s also, for someone reading conversations all day, a wall of identically-styled cards where user prompts and assistant replies blend together visually:

I wanted chat bubbles. iMessage style. User on the right in filled-blue, assistant on the left in dark grey, asymmetric border-radius for the chat-tail look, dark theme, the whole thing. Not because the existing layout was broken, but because reading 100+ message transcripts is a different task than reading a single session, and the visual rhythm of bubbles is genuinely faster to scan when you only care about “who said what.”

The markup didn’t change at all. Every message is still <div class="message user"> / <div class="message assistant">. The CSS rewrite swaps the message into a flex column whose children are reordered: the bubble (.message-content) sits on top, and the role-label/timestamp strip becomes a tiny caption underneath. align-items: flex-end for user, flex-start for assistant. Asymmetric border-bottom-right-radius: 6px on user bubbles and border-bottom-left-radius: 6px on assistant bubbles for the chat tail. Tool calls, edit diffs, code blocks, and todo lists all picked up dark-friendly translucent accent colors.

Six hundred lines of CSS rewritten in a single sitting. The 18 HTML snapshot tests had to be re-recorded, of course. The fact that the markup was already well-structured made this a CSS-only diff.

The boring polish

A few smaller things while I was in there:

  • Temp output pruning. The tool writes generated HTML to $TMPDIR/claude-session-<id>/ by default. macOS does eventually clean $TMPDIR, but only after several days of inactivity per file. I’d accumulated dozens of stale transcript directories. Now everything goes under a single $TMPDIR/claude-code-transcripts/ parent and gets pruned to the 20 most-recent on each render. Best-effort — a permission error never blocks the user’s render.

  • A short alias. claude-code-transcripts is 24 characters. My right hand was developing a grudge. Added cct as a second entry-point. Tab-completes uniquely against claude (Claude Code itself), three keystrokes from anywhere.

  • A search filter on the picker. Mentioned this above but it deserves its own callout: type any substring — project name, date, words from the session summary — and the list narrows live. Once you have it you wonder how you lived without it.

Why I’m keeping this fork to myself

When I was about to draft the PR back to upstream, I looked at my commit list and realized maybe half of it was straight-up personal preference. The Global Sessions detection is hardcoded to ~/ and ~/Code/. The dark theme is opinionated. The dedup script existed for one specific incident on one specific night. The cct alias would make sense to nobody else.

The folder-grouped picker and the search filter would probably be welcomed upstream. The rest is private polish — the kind of thing forking exists for. There’s a particular kind of joy in not asking permission. You don’t have to argue for the dark theme. You don’t have to write tests for the dedup script. You don’t have to justify why your home folder is special. You just… change it.

This is the second time this year I’ve forked an open-source tool I use daily and just kept the fork. I’m starting to suspect this is the natural endpoint of using AI to write code: the marginal cost of adapting a tool to your exact workflow drops to roughly one afternoon and a clear head, and the threshold for “should I make this PR-worthy” gets a lot higher than the threshold for “should I just fix it for myself.”

Simon’s tool is great. The thing I’m running on my machine is Simon’s tool plus the parts of me that bleed into everything I touch. That’s a fork.

The whole thing is at github.com/ekinertac/claude-code-transcripts if you want to steal pieces. The folder-grouped picker and the search filter are probably the bits worth lifting.