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:
type | what it is |
|---|---|
mode, permission-mode | the current mode / permission posture at that point |
file-history-snapshot | a checkpoint of file state (this is what powers /rewind) |
system, queue-operation, … | internal bookkeeping |
user | a user turn — or a tool result coming back |
assistant | a 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 aname(Bash,Edit,Read,Write,Agent…) and aninput. This is where you read off what the agent did: theBashcommands, thefile_pathof everyEdit/Write/Read.text— what the model said.thinking— extended-thinking reasoning. Note thethinkingfield above is empty while thesignatureis 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:
- 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 statustells you the result;session-statstells you the process — how many commands, which files, how much thinking. - Debugging. "Why is this session huge / slow / stuck?" The line-type census and the health flag answer it in one screen. (A session where
Bashdwarfs everything andfiles touchedis tiny is usually thrashing.) - 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.
Found this useful?
- You're Running Five Agents on One Repo. Who Stops Them Overwriting Each Other?
- Your Agent Did the Work, Then It Vanished. Here's How to Get It Back.
- Your Claude Code Session Is Permanently Stuck on a 400. Don't /clear — Do This.
- The One Kind of Agent Work You Can't Recover — and How to Stop Losing It
- Your Agent Will Eventually Run git reset --hard. Stop It at the Door.
- Your Agent Reads Text You Can't See. Frisk It First.
- You're Running a Dozen Agents. Which One Is Stuck?
- Your Agent Fleet's Worktrees Are a Minefield. Here's How to See It.
- The Agent Wrote the Code in Minutes. Reviewing It Takes Hours. Here's Why.
- Your Claude Agent Re-Reads Its Whole Prompt Every Turn. Here's the 10× You're Leaving On the Table.
- Your Agent Can Read Everything Your Shell Can. Have You Looked?
- The Loosest Line in Your Repo Is in .claude/settings.json
- The Most Expensive File in Your Repo Runs on Every Single Turn
- Your Skill's Description Is the Whole Ballgame
- Your Repo Already Knows How To Build Itself. Put That In the CLAUDE.md.
Related posts
The boring part: governance that enforces, not displays
Most "AI governance" is a dashboard. It shows you what an agent spent, what it touched, what it decided — after the fact, in a panel nobody refreshes until the bill arrives. A display is not a control. A control is the t…
Your Launch Deserves a Real Demo Video, Not a Screen Recording With Captions
Most product launches skip the demo video entirely, and the ones that don't ship one usually reach for the fastest option: a raw screen recording, maybe some auto-generated captions, posted as-is. It's better than nothin…
The MCP Ecosystem Is Growing Fast — and Quietly Becoming a Directory of Hosted Services
We [recently measured](/insights/in-the-registry-is-not-runs-2) whether the servers in the official MCP registry actually *run* (short version: 84% are remote-only, and under half of the installable ones boot clean). Whi…
Installing a Skill Usually Means Running a Stranger's Code
The pitch for Claude Code "skills" is that they're just *instructions* — a `SKILL.md` file with some markdown telling the agent how to do a task. That framing makes them feel safe to install. A prompt can't hurt you; wor…