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.