View as MarkdownUpdated 2026-09

Reading the record of what your agent did

Every session you run is already recorded in full. Almost nobody reads it — and the record is the fastest way to find out why a run went sideways.

When an agent session goes wrong, the usual reaction is to scroll back through the terminal. That is the worst available view: it is truncated, it is interleaved with your own typing, and the moment things actually went wrong is usually several turns before where you noticed.

Both major harnesses write a complete, structured record of every session to disk. It is machine-readable, it is on your own machine, and it answers questions the scrollback cannot.

Claude Code#

Where it lives#

shell
ls ~/.claude/                  # projects/ sessions/ history.jsonl settings.json telemetry/ debug/
ls ~/.claude/projects/         # one directory per working directory, path-slugified

A project directory is your cwd with slashes replaced by dashes — /Users/you/myrepo becomes -Users-you-myrepo. Inside it, one JSONL file per session, named by session UUID.

shell
# find the transcript for the repo you are in
proj=~/.claude/projects/$(pwd | tr '/' '-')
ls -lt "$proj" | head

What is in it#

One JSON object per line. The types you care about are user, assistant and system; the rest are housekeeping (queue operations, attachments, titles, mode changes).

Records carry type, sessionId, timestamp, uuid, parentUuid, cwd, version, gitBranch and message. parentUuid is what makes this a tree rather than a list — following the chain reconstructs the real order, and isSidechain marks work done by a subagent.

shell
# what record types does YOUR version write?
cat "$proj"/*.jsonl | jq -r '.type' | sort | uniq -c | sort -rn

The queries worth having#

Every shell command run in this project. The one people want first.

shell
cat "$proj"/*.jsonl | jq -r '
  select(.type=="assistant")
  | .message.content[]?
  | select(.type=="tool_use" and .name=="Bash")
  | .input.command'

Every file touched, most-edited first.

shell
cat "$proj"/*.jsonl | jq -r '
  select(.type=="assistant")
  | .message.content[]?
  | select(.type=="tool_use" and (.name=="Edit" or .name=="Write"))
  | .input.file_path' | sort | uniq -c | sort -rn

A file near the top that you did not expect to be touched at all is the single most useful signal in this whole page.

Just your side of the conversation — the fastest way to remember what you actually asked for:

shell
cat "$proj"/<session-uuid>.jsonl | jq -r '
  select(.type=="user" and (.message.content|type=="string"))
  | "\(.timestamp[11:19])  \(.message.content)"' | head -40

Tool usage profile for a session. A run dominated by Read was exploring; one dominated by Edit with no Bash never checked its work.

shell
jq -r 'select(.type=="assistant") | .message.content[]?
       | select(.type=="tool_use") | .name' "$proj"/<uuid>.jsonl \
  | sort | uniq -c | sort -rn

Token cost per session, where the assistant records usage:

shell
jq -r 'select(.type=="assistant") | .message.usage // empty
       | [.input_tokens, .cache_read_input_tokens, .output_tokens] | @tsv' \
  "$proj"/<uuid>.jsonl \
  | awk '{i+=$1; c+=$2; o+=$3} END {printf "in %d  cached %d  out %d\n", i, c, o}'

Interactively, /cost and /context give you the same information without leaving the session, and /context in particular is how you find out that an MCP server you installed months ago is eating a fifth of your window.

Telemetry, for teams#

Claude Code can export OpenTelemetry metrics and logs:

shell
export CLAUDE_CODE_ENABLE_TELEMETRY=1
export OTEL_METRICS_EXPORTER=otlp
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317

Worth it when several people are using it and you want aggregate cost and usage in the same place as your other dashboards. Overkill for one person — the JSONL files answer more, more cheaply.

Codex#

Where it lives#

shell
ls ~/.codex/                              # sessions/ config.toml session_index.jsonl logs/ rules/
ls ~/.codex/sessions/2026/09/04/          # rollout-<timestamp>-<uuid>.jsonl

Sessions are bucketed by date rather than by project, which is the main practical difference — finding "that session about the parser" means searching by content or by time rather than by directory.

shell
# most recent sessions, newest first
find ~/.codex/sessions -name 'rollout-*.jsonl' | sort | tail -5

What is in it#

Different vocabulary to Claude Code. The record types are:

TypeWhat it is
session_metaone per file: version, cwd, originator, instructions
turn_contextmodel, approval policy, sandbox settings in force for the turn
event_msgUI-level events — the things the harness told you
response_itemthe model's output items, including reasoning and tool calls
world_statethe harness's view of the environment
shell
f=$(find ~/.codex/sessions -name 'rollout-*.jsonl' | sort | tail -1)
jq -r '.type' "$f" | sort | uniq -c
jq -r 'select(.type=="response_item") | .payload.type' "$f" | sort | uniq -c

turn_context is the one with no Claude Code equivalent, and it is genuinely useful: it records the approval policy and sandbox mode that were actually in force, per turn. When you are reconstructing how something was allowed to happen, that is the record that answers it.

shell
jq -r 'select(.type=="turn_context")
       | [.payload.approval_policy, .payload.sandbox_policy.mode] | @tsv' "$f"

There is also ~/.codex/session_index.jsonl for lookup, SQLite state alongside it, and a TUI log you can raise the verbosity of:

shell
RUST_LOG=debug codex          # then read ~/.codex/logs/

The log worth building yourself#

Both formats are internal and both will change. If you want something stable, greppable and per-repo, write it yourself with a PreToolUse hook — it is four lines and it does not care what the harness does internally.

.claude/hooks/audit.sh
#!/usr/bin/env bash
set -Eeuo pipefail
jq -c '{ts: now|todate, session: .session_id, tool: .tool_name,
        arg: (.tool_input.command // .tool_input.file_path // .tool_input.pattern // null),
        cwd: .cwd}' >> "${CLAUDE_PROJECT_DIR:-.}/.agent-audit.jsonl"
exit 0
.claude/settings.json
{ "hooks": { "PreToolUse": [ { "matcher": ".*",
  "hooks": [{ "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/audit.sh" }] } ] } }

Add .agent-audit.jsonl to .gitignore. Then:

shell
# what happened in the last hour, in order
jq -r '"\(.ts[11:19])  \(.tool)  \(.arg // "")"' .agent-audit.jsonl | tail -40

# every command that touched git
jq -r 'select(.arg != null and (.arg | test("^git "))) | .arg' .agent-audit.jsonl

# files written, ranked
jq -r 'select(.tool=="Write" or .tool=="Edit") | .arg' .agent-audit.jsonl | sort | uniq -c | sort -rn

Because it is your format, it survives harness upgrades, works identically across projects, and you can add whatever fields you want — the current git SHA, the ticket you are working on, whether the tests were green at the time.

What to actually look for#

Reading a transcript is only useful if you know what you are looking for. Four patterns, in rough order of how often they explain the problem:

The turn where it started guessing. Scan the tool sequence. A long run of Read and Grep calls that never converges means the model could not find something — usually because your repo structure did not make it findable. That is a fixable property of your codebase, not of the model.

Edits to files you did not expect. The most reliable signal that a change grew beyond its brief. The ranked file list above finds it in one command.

Tests that stopped being run. If Bash calls running your suite appear early and then stop, the suite is too slow and the loop broke. That is the highest-value thing this exercise can tell you, because the fix — split the suite — improves every future session.

The same failure three times. Repeated identical failures mean the context now contains several wrong approaches, which makes each subsequent attempt worse. The right move at that point is a fresh session with a summary, not another lap.

Privacy, briefly#

These files contain everything: your prompts, your source code, output from commands, and anything that was in the environment when a command ran. They sit unencrypted in your home directory.

Two consequences worth acting on. First, they are covered by whatever your disk encryption is, and nothing more — treat them the way you treat your shell history, only more so. Second, if you are ever asked to share a transcript to reproduce a bug, read it first: jq -r 'select(.type=="user") | .message.content' will remind you what is actually in there.

Common questions#

Is there a UI for any of this?#

Interactively, /cost and /context in Claude Code cover the common questions, and the harnesses' own scrollback covers the rest. For anything historical or aggregate, the JSONL files plus jq are what exist today, and they are more flexible than a dashboard would be.

How much disk does this use?#

Less than you would expect — they are text, and text compresses. If you want to prune, they are safe to delete: they are a record, not state, and removing an old session file loses only the ability to look back at it.

Can I resume from a transcript?#

Both harnesses have their own resume mechanism, and that is what to use — the files are a record of what happened, not a restorable session. What the transcript is genuinely good for is writing the summary you paste into a fresh session after a run went wrong, which is usually the better move anyway.

Should I commit the audit log?#

No. Gitignore it. It contains command output and file paths, it is per-developer, and it would produce a merge conflict on every commit. If you want team-level visibility, export OpenTelemetry to somewhere central instead.

Get the updates

Agent tooling moves monthly. We send a short note when something on these sites changes materially — a new harness feature, a failure mode worth knowing, a config that stopped being right. Nothing else.

Unsubscribe in one click. We never sell the list.