Parsing Claude Code Session Transcripts in Python: Turns, Tool Calls, and Results
Every time I run Claude Code, it quietly writes the entire session to disk. No flag, no setup, no instrumentation. The file is already there when I want it. That single fact is the reason I have been able to build small analysis tools on top of my own agent runs without ever touching the agent itself. In this tutorial I want to show you how to read those files and pull real structure out of them:…
Every time I run Claude Code, it silently saves the entire conversation session to a text file without requiring any additional configuration. This single characteristic has enabled me to develop lightweight analysis tools directly on top of my own agent runs, without needing to directly interact with the agent itself. In this guide, I will demonstrate how to parse those files and extract meaningful information: the turns, tool calls, and the results produced by those calls.
Claude Code stores one file per session in the following location: ~/.claude/projects/project-slug/session-id.jsonl. The slug represents your project's path, with slashes replaced by dashes. Each file is in JSONL format, meaning each line is a separate JSON object that gets appended as the session progresses. This append-only nature is crucial later on.
To locate these files, you can use the following Python function:
```python
from pathlib import Path
def find_sessions(root: Path | None = None) -> list[Path]:
root = root or (Path.home() / ".claude" / "projects")
if not root.exists():
return []
return sorted(root.glob("**/*.jsonl"))
```
When reading these transcripts, it's important to handle the possibility of a torn final line, which can occur if Claude Code is still writing while you're reading. To avoid crashing your analysis, simply skip any incomplete lines.
Each line in the transcript is not always a conversation turn. The 'type' field can contain values such as 'user', 'assistant', 'queue-operation', 'attachment', 'file-history-snapshot', 'mode', and other miscellaneous types. The two types you primarily care about for turns are 'user' and 'assistant'. Each of these types carries a 'message' object, which in turn contains a 'content' field that is a list of typed blocks. These blocks are where the interesting information resides.
A 'text' block has the following structure: { "type": "text", "text": "..."} A 'tool_use' block represents a tool call, and its 'tool_result' block represents the result produced by that tool call. Thus, you can think of the data as follows: lines give you turns, content blocks within a turn provide text, tool calls, and results.
To extract the tool calls and results, you can use two separate functions. The first function, `tool_calls(path)`, collects all tool calls, while the second function, `tool_results(path)`, gathers all tool results. These results are then joined together. The pairing of a tool call with its corresponding result is achieved by matching the 'id' field in a 'tool_use' block with an 'id' field in a subsequent 'tool_result' block. This is the key to understanding the structure of the data.
Written by urgent.news from Dev.to's reporting — not their text. Machine-written — may contain errors; check the original before relying on it.