txcript 0.7.0

Convert coding-agent session transcripts between harness formats.
Documentation
# Claude Code

Claude Code is Anthropic's terminal coding agent. The CLI is closed source. Anthropic
officially documents *where* sessions are stored, but explicitly declares the record
format "internal to Claude Code" and subject to change between releases — so this
document is **reverse-engineered** from observed local sessions, with
`src/harness/claude_code.rs` as the authoritative mapping. The format is the least
exotic of the harnesses txcript supports: each conversational line wraps a payload
already in the Anthropic Messages shape, which is why the Common model (and every
other harness's codec) is normalized toward it.

```
~/.claude/projects/                          ($CLAUDE_CONFIG_DIR/projects overrides)
└── <munged-cwd>/                            one dir per project cwd; / . \ : → -
    ├── <uuid>.jsonl                         one session = one JSONL file
    ├── <uuid>/                              per-session side-files (not sessions):
    │   ├── subagents/  tool-results/  ...
    └── <uuid>.jsonl

one session file, line by line:
    {"type":"summary","summary":…,"leafUuid":…}      title for --resume
    {"type":"user",     …envelope…, "message":{…}}   ┐ conversation, linked by
    {"type":"assistant",…envelope…, "message":{…}}   ┘ parentUuid → uuid
    {"type":"system","subtype":"local_command",…}    slash-command output (newer CLIs)
    {"type":"file-history-snapshot"|"attachment"|…}  bookkeeping
```

## On disk

Sessions live under `~/.claude/projects/`, or `$CLAUDE_CONFIG_DIR/projects` when that
variable is set and non-empty. Each project working directory gets one subdirectory
whose name is the cwd with `/`, `.`, `\`, and `:` each replaced by `-`
(`/Users/alice/src/app` → `-Users-alice-src-app`); the official docs describe this
more broadly as "non-alphanumeric characters replaced by `-`". Inside, each session
is a single JSONL file named `<session-uuid>.jsonl`. A session may also own a sibling
directory of the same uuid holding side-files (`subagents/`, `tool-results/`,
`workflows/`); these are not sessions.

txcript's `ClaudeStore::discover` walks the root recursively collecting `*.jsonl`
files, skipping `subagents` and `tool-results` directories and symlinked directories
(symlinked files still list). Metadata comes from a shallow scan that never builds
message payloads; unreadable files are skipped, a missing root yields no sessions,
and an empty `sessionId` falls back to the filename stem.

## Dissection of a transcript

Each line is one JSON object tagged by `type`. Known types parse into typed records;
everything else is preserved verbatim so a native load/save round-trips byte-losslessly.

| Their name | What it is | Maps to |
|---|---|---|
| `user` / `assistant` line | Envelope (`uuid`, `parentUuid`, `timestamp`, `sessionId`, `cwd`, `gitBranch`, `version`) wrapping a `message` | `Message` with `Role::User` / `Role::Assistant` |
| `message` | An Anthropic Messages API message; `content` is a string or block array | `Vec<Block>` on the `Message` |
| `text` block | Plain prose | `Block::Text` |
| `thinking` block | Reasoning with opaque `signature` | `Block::Thinking` |
| `tool_use` block | Tool call: `id`, `name`, `input` | `Block::ToolUse` (typed `Tool`, else `Tool::Raw`) |
| `tool_result` block | Output paired by `tool_use_id`, rides on a user line | `Block::ToolResult` |
| `image` block | Base64 inline image | `Block::Image` |
| `stop_reason`, `usage` | Anthropic stop reason and token accounting | `StopReason`, `Usage` |
| `<command-name>` markup | Slash command the user ran, as XML-ish tags filling a body | `Block::ToolUse` with `Tool::Command` |
| `<local-command-stdout>` markup | What the command printed | `Block::ToolResult` paired via `parentUuid` |
| `<local-command-caveat>` markup | Fixed boilerplate, regenerated by the CLI | dropped |
| `summary` line | Session title with a `leafUuid` anchor | `Meta.title` (lowest precedence) |
| `custom-title` / `agent-name` line | User-set or agent-derived title | `Meta.title` (custom-title wins) |
| `system` line | Bookkeeping (`turn_duration`, `away_summary`, hook notices…) — except `subtype: "local_command"`, which carries command envelopes on newer CLIs | envelopes → command blocks; rest → `Record::Other` |
| `attachment`, `file-history-snapshot`, `queue-operation`, `mode`, `last-prompt`, `ai-title`, … | Other bookkeeping line types | `Record::Other` (round-trip only, no Common turn) |

Threading is a `parentUuid` → `uuid` chain rather than file order; txcript reads in
file order and uses the chain only to pair local-command output to its command.
Slash-command markup has appeared on `user` lines (older CLIs) and on
`system`/`local_command` lines (newer), and only a body that is *entirely* envelope
markup counts — a message that merely quotes the tags stays plain text. Session
metadata (`Meta`) is extracted from the first line that carries each field: `sessionId`,
`cwd`, `gitBranch`, `version` from user lines, `model` from assistant lines, the
earliest `timestamp` as session start.

A synthetic assistant line (real files add more envelope keys — `isSidechain`,
`requestId`, `userType`…, all preserved in `extra`):

```json
{"type":"assistant","uuid":"b7e2…","parentUuid":"a1f4…","sessionId":"3f2a…",
 "timestamp":"2026-08-10T12:00:05.123Z","cwd":"/Users/alice/src/app",
 "gitBranch":"main","version":"2.1.230",
 "message":{"role":"assistant","model":"claude-fable-5",
   "content":[{"type":"text","text":"Reading the parser now."},
              {"type":"tool_use","id":"toolu_01","name":"Read",
               "input":{"file_path":"/Users/alice/src/app/main.rs"}}],
   "stop_reason":"tool_use",
   "usage":{"input_tokens":1200,"output_tokens":45,"cache_read_input_tokens":900}}}
```

## Caveats

- **The format is officially unstable.** Anthropic says so outright; observed drift
  includes local-command envelopes migrating from `user` to `system` lines, tag order
  and `<command-args>` presence varying, and new line types appearing per release
  (`ai-title`, `file-history-delta`, `pr-link` are recent). Unknown lines survive as
  `Record::Other`, so native round-trips stay lossless; Common conversion drops them.
- **Hostile envelope payloads.** The markup has no native quoting, so a payload
  containing `</local-command-stdout>` could truncate or forge an envelope on re-read.
  txcript writes with a bijective backslash escape (`</tag>``<\/tag>`) and unescapes
  on parse; ANSI escapes are stripped from command stdout.
- **Round-trip lossiness.** Common → native regenerates lines with deterministic
  UUIDv5 entry ids: bookkeeping lines, envelope `extra` keys, and unmodeled block
  types are gone after a cross-model trip (same-harness native trips keep everything).
  Rewritten `tool_result.content` must be a string or block array — anything else
  fails a `claude --resume` load — so bare JSON is flattened to its compact text.
- **Resume anchoring.** A leading `summary` line's `leafUuid` must name a real
  user/assistant line in the file, or Claude Code reports the whole session missing;
  txcript anchors generated summaries to the last real turn.
- **Malformed input.** Invalid JSON lines are skipped; a known-type line whose body
  fails its schema degrades to `Record::Other` rather than failing the file. Title
  precedence is `custom-title` > `agent-name` > first `summary`; the newer `ai-title`
  line is passed through but not read.

## References

- Anthropic, "Manage sessions", https://code.claude.com/docs/en/sessions (accessed
  2026-08-10) — documents the storage path, `CLAUDE_CONFIG_DIR`, retention
  (`cleanupPeriodDays`), and states the entry format is internal and version-unstable.
- No official specification of the record format exists, and Claude Code is closed
  source — no upstream permalink is possible. This document is reverse-engineered
  from real local sessions (CLI versions 0.144 through 2.1.227) and from txcript's parser.
- Authoritative mapping: `src/harness/claude_code.rs`, exercised by
  `tests/integration/claude_code.rs`.
- Last verified: 2026-08-10, against src/harness/claude_code.rs and real local sessions.