# Reading the record of what your agent did

> Source: https://codelearningdojo.com/agent-observability/
> Part of Code Learning Dojo, free to read.

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.

:::note Verified, but check your own paths
The layouts below were confirmed on a real installation running Claude Code 2.1.x and the Codex CLI in September 2026. Formats here are internal and do change between releases — the queries are written to degrade into empty output rather than wrong output, and there is a discovery command in each section so you can confirm what your version writes.
:::

## Claude Code

### Where it lives

```bash
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.

```bash
# 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.

```bash
# 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.

```bash
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.**

```bash
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:

```bash
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.

```bash
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:

```bash
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:

```bash
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

```bash
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.

```bash
# 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:

| Type | What it is |
|---|---|
| `session_meta` | one per file: version, cwd, originator, instructions |
| `turn_context` | model, approval policy, sandbox settings in force for the turn |
| `event_msg` | UI-level events — the things the harness told you |
| `response_item` | the model's output items, including reasoning and tool calls |
| `world_state` | the harness's view of the environment |

```bash
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.

```bash
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:

```bash
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](/harness-hooks/) — it is four lines and it does not care what the harness does internally.

```bash .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
```

```json .claude/settings.json
{ "hooks": { "PreToolUse": [ { "matcher": ".*",
  "hooks": [{ "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/audit.sh" }] } ] } }
```

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

```bash
# 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](https://learn-python.com/ai/context/). 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.
