Back to Insights
21 Jun 20265 min readAI & LLMs

What's Actually In Your Claude Code Session File (and a Tool to Read It)

Every Claude Code session you've ever run is sitting on your disk as a plain text file. Not a database, not an opaque blob — newline-delimited JSON you can cat, grep, and parse. Once you know the shape, you can answer questions the UI never shows you: how many tool calls did that 3-hour agent run make? Which files did it touch? Is this stuck session actually broken, or just slow? We pull this apart constantly while building agent tooling, so here's the map, and a small reader that turns a transcript into a one-screen summary.

Where it lives

~/.claude/projects/<slug>/<session-id>.jsonl

<slug> is your project's working directory with the slashes turned into dashes — /Users/me/app becomes -Users-me-app. Each session is one .jsonl file; the newest-by-mtime in that directory is your current run.

The line types

Every line is one JSON object with a type. Most are bookkeeping; two carry the conversation:

typewhat it is
mode, permission-modethe current mode / permission posture at that point
file-history-snapshota checkpoint of file state (this is what powers /rewind)
system, queue-operation, …internal bookkeeping
usera user turn — or a tool result coming back
assistanta model turn

That's the first surprise: a user line isn't only you typing. When a tool finishes, its result is appended as a user line carrying a tool_result block. So "user turns" in a transcript counts both your prompts and every tool result the model received.

Inside an assistant turn

The substance is in assistant lines, where message.content is a list of blocks:

{ "type": "assistant", "message": { "role": "assistant", "content": [
  { "type": "thinking", "thinking": "", "signature": "…" },
  { "type": "text", "text": "Reading the parser…" },
  { "type": "tool_use", "name": "Bash", "input": { "command": "ls" } }
] } }

Three block types matter:

  • tool_use — a tool call, with a name (Bash, Edit, Read, Write, Agent…) and an input. This is where you read off what the agent did: the Bash commands, the file_path of every Edit/Write/Read.
  • text — what the model said.
  • thinking — extended-thinking reasoning. Note the thinking field above is empty while the signature is kept. That's not a bug: on Opus 4.7/4.8 the thinking text is omitted from the transcript by default, but the signed block is retained. (It's also the root of the thinking-block 400 wedge — when an empty-but-signed block is left as the last assistant turn, it's resent and rejected forever.)

A 120-line reader

Knowing the shape, a useful summary is a short loop. session-stats counts the turns, builds a tool histogram, collects every file touched, counts thinking blocks, and flags whether the session ends on the wedge signature. Run it on a real session — here it is on the very session that built this series of posts:

$ session-stats "$(session-stats find)"
session : 6b59e853-…
size    : 7,455,965 bytes  (~1,863,991 tokens, rough)  ·  1,209 lines

turns   : 227 user  ·  470 assistant
thinking: 123 blocks  (123 empty-text)
tools   : 133× Bash, 41× Read, 24× Write, 10× Edit, 4× WebSearch, 1× ToolSearch, 1× Skill, 1× WebFetch
          (215 tool calls total)
files   : 55 touched
health  : ✓ clean (does not end on an orphaned thinking turn)

That's a portrait of an agent's work you can't get from the UI: 215 tool calls, Bash-heavy, 55 files touched, all 123 thinking blocks empty-text (the Opus-4.8 default), and a clean health check. The find subcommand resolves the slug for you so you never compute it by hand.

The health line is the genuinely useful safety check: it reuses the wedge test — does the last assistant turn end in a thinking block? — so a single command tells you whether a stuck session is the recoverable 400 wedge or something else.

Why read the transcript at all

Three reasons we reach for this constantly:

  1. Auditing agent runs. When a fleet agent churns for an hour, the transcript is the ground truth of what it touched. A diff of git status tells you the result; session-stats tells you the process — how many commands, which files, how much thinking.
  2. Debugging. "Why is this session huge / slow / stuck?" The line-type census and the health flag answer it in one screen. (A session where Bash dwarfs everything and files touched is tiny is usually thrashing.)
  3. Building on top. Every tool we've shipped for agent ops — recovery, un-wedging, guarding — starts by reading this file. The format is stable enough to build on, and it's yours: local, plaintext, greppable.

The honest caveats

The ~tokens figure is bytes / 4 — a rough proxy, not the API's count (which you'd need a token-counting call for); it's there for an at-a-glance sense of scale, and the transcript bytes include tool output and inlined images, so it over-counts "conversation." And the schema is undocumented, so a future Claude Code version can rename a field — the reader is deliberately defensive (unknown line types are just counted, never fatal). Treat it as a high-leverage convenience, not a contract.

The real lesson

The thing worth internalizing isn't the tool — it's that your agent's entire session is local, plain-text data you own. Not a SaaS dashboard you rent, not a black box. Once you read one transcript by hand, every "what did the agent actually do?" question becomes a grep, and every agent-ops tool you've wanted becomes a short script over a file that's already on your disk.

We build and run autonomous agent fleets on Claude Code in production. If you're instrumenting or debugging a multi-agent setup, talk to us.


Get the tool → session-stats in agent-ops — open source (MIT), a single dependency-free script, part of our toolkit for running coding agents in production.

AI agentsobservabilityClaude Codetranscripts

Found this useful?