📁 Where the logs live
It all starts in one place. Claude Code records every conversation in
~/.claude/projects/<projeto>/<sessão>.jsonl:
one file per session, organized in a folder for each project. Add that up over months of use,
and you have thousands of files — an entire gold mine of behavior, waiting to be mined.
🗺️ Main concept
There's no database or server: just the file system. That's good news — you can scan everything with common tools.
- •Root folder:
~/.claude/projects/ - •One subfolder per project (sanitized path).
- •One file
.jsonlper session.
~/.claude/projects/ ├── -home-voce-projetos-fablelite/ │ ├── 2f3a…-9c.jsonl # sessão 1 │ └── 7b1c…-4e.jsonl # sessão 2 └── -home-voce-projetos-portal/ └── a01d…-22.jsonl
~/.claude/projects
1 file = 1 session
thousands of files
filesystem only
📜 JSONL = one JSON event per line
Watch out for the trap: the file no is one huge JSON object. JSONL means JSON Lines — each line is a complete, independent JSON object, appended as the conversation progresses. It’s a format for streaming, append-only.
{"type":"user","message":{...},"timestamp":"2026-06-10T14:00:01Z"}
{"type":"assistant","message":{"model":"claude-fable-5",...}}
{"type":"assistant","message":{"model":"claude-fable-5",...}}
{"type":"system","subtype":"hook",...}
✓ What to DO
- ✓Read line by line (
for line in f). - ✓Do
json.loadsper line. - ✓Tolerate a broken line (skip it, don’t abort).
✗ What NOT to do
- ✗
json.load(arquivo_inteiro)— breaks. - ✗Assume the last line is complete.
- ✗Load everything into memory at once.
💡 Practical tip
Since it’s append-only, an interrupted session is still readable up to the last complete line. Always process it defensively: try/except around the json.loads of each line.
JSON Lines
append-only
line by line
tolerates interruption
🧬 Anatomy of an event
Each line carries a handful of predictable fields. The main ones: type (user/assistant/system/summary),
message (the actual content), timestamp, uuid,
cwd e gitBranch. Knowing these fields is what unlocks filtering, grouping, and measuring.
type
Classifies the event: user (you), assistant (model), system (harness/hooks), summary (context summary).
message
The payload: role, model (assistant-only) and content (the list of blocks).
timestamp · uuid · cwd · gitBranch
Context metadata: chronological order, event identity, working directory, and git branch. They show the "where" and "when".
🔎 Why this matters
O timestamp orders the turns; the type separates who spoke; and the cwd/gitBranch help reconstruct what was happening. Without these fields, the log would be unstructured text.
who spoke
the content
the order
the context
🧱 The assistant’s content blocks
The assistant doesn't speak in continuous prose — it speaks in blocks:
text (what you read), thinking (reasoning),
tool_use (tool call) and tool_result (the tool’s response).
And here’s the crucial detail: Claude Code records EACH block on a SEPARATE LINE.
🧩 The four blocks
- •
text— the model’s visible output. - •
thinking— the reasoning (encrypted in the logs — we'll return to this in 1.2). - •
tool_use— the model decides to call a tool. - •
tool_result— the tool output comes back (overhead, in 1.2).
⚠️ The pitfall that dilutes the signal
Because each block becomes a line, counting “per line” artificially inflates the numbers. A single assistant turn can become 5 lines (1 thinking + 1 text + 3 tool_use). If you measure by line, you lose track of “how much the model did in response to a prompt.” The fix comes in topic 6: group into logical turns.
visible speech
reasoning
decision to act
= 1 line
🏅 message.model — the GOLD field
Of all the fields, one is at the heart of the course: message.model. It says exactly
which model wrote each turn — claude-fable-5,
claude-opus-4-8, claude-haiku-4-5... That’s what lets you filter the log by model and separate each model’s corpus.
{"type":"assistant","message":{
"role":"assistant",
"model":"claude-fable-5",
"content":[ {"type":"thinking",...}, {"type":"text",...} ]
}}
📊 What it unlocks
- Filter by model: isolate everything Fable-5 did from everything Opus-4.8 did.
- Compare: measure each corpus separately and calculate the delta.
- Assign: each turn has an owner — there is no ambiguity.
💡 Practical tip
Only assistant have model. User and system events do not count. When grouping into logical turns, the turn’s model is the model of the assistant event(s) within it.
message.model
assistant only
filter by model
corpus per model
🔗 Physical turn (line) vs. logical turn
The last piece before measuring anything: the right unit. One physical turn is one line in the file. A logical turn is everything between one human prompt and the next — the thinking, the text, and all the tools the model used to answer that prompt.
✓ Measure by logical turn
- ✓“Did the model think before acting this turn?”
- ✓“How many tools did you use to answer?”
- ✓Reflects the pace of real work.
✗ Measure by physical line
- ✗Inflates the count (5 lines = 1 response).
- ✗Dilutes the presence of reasoning.
- ✗Mixes blocks from different turns.
1 line
prompt → prompt
human prompt
the pace
🪙 Module Summary
~/.claude/projects — one .jsonl file per session.message.model is the gold field — separates Fable from Opus.Next Module:
1.2 — Fat vs. Gold, and the Myth of Mineable Reasoning