bough
What is in a Claude Code transcript All documents

What is actually in a Claude Code transcript

Claude Code writes a JSONL file for every session, under ~/.claude/projects, and nobody at Anthropic has documented what is in it. These are notes from reading 275 MB of my own: 28 transcripts across 6 project directories, 76,801 lines, nothing unparseable.

I wrote them down because I needed them to build bough, which draws what you built from those files, and because getting them wrong is easy in ways that do not announce themselves. A parser that misreads this format does not crash. It returns a number that is three times too high and looks entirely plausible.

The files are append-only, and they replay

This is the one to know first, because it is the one that quietly ruins counts.

A transcript is not a log of what happened once. When a session resumes, Claude Code writes the earlier records again, under the ids they already had. The same record can appear several times in one file, and later copies carry more fields than earlier ones.

Count lines and you are counting some conversations several times over. In one project 1,533 of 2,236 replies were written three times, each copy carrying the same 252 output tokens. That is not a rounding error, it is a different answer.

Deduplicating by uuid is not enough

The obvious fix is to key on uuid and skip repeats. That gets you closer and it is still wrong for tokens.

One assistant reply is written as several records that share a message.id but each carry their own uuid. Dedup by uuid and you keep all of them, and every copy repeats the usage for the whole reply, so the same tokens are counted once per record. On one project:

dedup key output tokens
by uuid 12,402,105
by message.id 6,823,315

Records are one thing and replies are another. Use uuid for records, and message.id for anything you are counting as spend.

Tool results are filed as though you typed them

A record with "type": "user" is not necessarily something a person wrote. When a tool returns, its output is written back as a user record, because from the model's side that is what it is: input arriving from outside.

So counting user records counts your prompts plus every file read, every grep, every test run. The field that separates them is isMeta, set on the records nobody typed.

This one cost me real data. bough used to require a promptId to recognise a prompt, on the reasoning that a real prompt would have one. That field only appeared in Claude Code around version 2.1.8, in March 2026. Anyone with history older than that had it silently dropped: one person's 238-session project displayed 20 prompts where the file held 3,282.

The schema is loosely typed

Several fields arrive as either a string or a structure, depending on the version that wrote them and sometimes on the record. content is the usual offender. Code that assumes either shape works on your machine and fails on somebody else's, which is the worst kind of failing.

Read defensively. Accept both, and treat anything unexpected as absent rather than as a reason to stop: a transcript with one strange record in it is still worth reading.

Where the files live

~/.claude/projects/<mangled-working-directory>/<sessionId>.jsonl

The directory name is the project's working directory with the separators replaced by dashes, which cannot be reversed: you cannot tell whether a dash was a dash or a slash. The real path is written inside the records, on cwd. Read it from there rather than trying to unmangle the name.

Nothing in my corpus suggested transcripts are ever pruned. The oldest was four months old and complete. I would not rely on that.

Retention, and what this is good for

The files are already on your disk, they go back as far as you have been using the tool, and they are a remarkably detailed record of how a piece of work actually went. That is the reason to read them carefully rather than approximately.

There is a fuller version of these notes, kept next to the code that depends on them, at the Claude Code transcript format. The Codex rollout format is written up separately, and has a replay problem of its own that spans files rather than sitting inside one.