# JSONL Spike Report — Claude Code Log Schema

> **Date**: 2026-02-24
> **Source**: `~/.claude/projects/*/` (461 files, 91,306 entries, 0 parse errors)
> **Purpose**: Comprehensive field inventory for MyBurn parser rewrite

---

## 1. Entry Types

| Type | Count | Description |
|------|-------|-------------|
| `progress` | 33,124 | Tool execution progress events (bash, agents, hooks) |
| `assistant` | 31,397 | Claude's responses (streamed in chunks) |
| `user` | 21,190 | Human messages + tool results |
| `file-history-snapshot` | 2,797 | File backup snapshots for undo |
| `queue-operation` | 1,787 | Message queue operations |
| `system` | 1,091 | System events (errors, compaction, commands) |

### System Subtypes

| `type:subtype` | Description |
|-----------------|-------------|
| `system:api_error` | API connection/response errors with retry info |
| `system:compact_boundary` | Marks where conversation was compacted |
| `system:local_command` | Local CLI command execution (e.g., `/clear`) |
| `system:turn_duration` | Total duration of an assistant turn in ms |

---

## 2. Top-Level Entry Fields (all types)

### Universal Fields (present on most entries)
```
type              string    — Entry type (see table above)
uuid              string    — Unique ID for this entry
timestamp         string    — ISO 8601 timestamp (e.g., "2026-02-18T02:41:20.435Z")
parentUuid        string|null — UUID of the previous entry in the chain (null = first in chain)
sessionId         string    — Session UUID (matches the .jsonl filename)
cwd               string    — Working directory at time of entry
version           string    — Claude Code CLI version (e.g., "2.1.50")
gitBranch         string    — Git branch name (e.g., "master", "HEAD")
isSidechain       boolean   — true for subagent/branched conversations
userType          string    — Always "external" in observed data
```

### Conditional Fields
```
message           object    — The actual message content (user/assistant entries)
requestId         string    — API request ID (assistant entries only, e.g., "req_011CYEmBhBkSqBr6xTK649Nj")
slug              string    — Human-readable session name (e.g., "peppy-seeking-brooks")
agentId           string    — Subagent identifier (e.g., "a4682d9", "acompact-7025b8")

# User-entry specific
thinkingMetadata  object    — {maxThinkingTokens: int} — present on first user message
todos             array     — Task list state (observed as empty [])
permissionMode    string    — "default" | "acceptEdits" | "bypassPermissions"
sourceToolAssistantUUID  string — Links tool_result back to the assistant UUID that issued tool_use
toolUseResult     varies    — Structured result data (see Section 7)
sourceToolUseID   string    — Rare; links to originating tool_use ID

# System-entry specific
subtype           string    — "api_error" | "compact_boundary" | "local_command" | "turn_duration"
level             string    — "error" | "info"
content           string    — Human-readable system message
isMeta            boolean   — Metadata-only entry (not shown in transcript)
durationMs        integer   — Turn duration in ms (turn_duration subtype)
error             object    — Error details (api_error subtype)
cause             object    — Error cause with {code, path, errno}
retryInMs         float     — Retry delay in ms
retryAttempt      integer   — Current retry number
maxRetries        integer   — Maximum retries allowed
compactMetadata   object    — {trigger: "auto", preTokens: int} (compact_boundary)
logicalParentUuid string    — Points to last real message before compaction (when parentUuid is null)

# Progress-entry specific
data              object    — Progress payload (see Section 8)
toolUseID         string    — ID of the tool being executed
parentToolUseID   string    — ID of the parent tool (for nested tool calls)

# Queue-operation specific
operation         string    — "enqueue" | "dequeue" | "remove" | "popAll"
content           string    — Queued message content

# File-history-snapshot specific
messageId         string    — Associated message UUID
snapshot          object    — {messageId, trackedFileBackups: {}, timestamp}
isSnapshotUpdate  boolean   — false for initial, true for updates with actual file backups

# Error-related
isApiErrorMessage boolean   — Entry represents an API error message
apiError          string    — Error description string

# Visibility
isCompactSummary  boolean   — Entry is a compacted conversation summary
isVisibleInTranscriptOnly  boolean — Visible only in transcript view (70 entries observed)
planContent       string    — Plan mode content
```

---

## 3. Message Object Structure (`message` field)

### Common Message Fields
```json
{
  "role": "user" | "assistant",
  "content": string | array,           // string for simple user messages, array for structured
  "type": "message",                    // always "message" in observed data
  "model": string,                      // assistant only — see Models section
  "id": string,                         // assistant only — API message ID (e.g., "msg_01J31r7...")
  "stop_reason": string | null,         // assistant only — see Stop Reasons
  "stop_sequence": string | null,       // assistant only — always null in observed data
  "usage": object,                      // assistant only — see Usage section
  "container": object,                  // rare — container context
  "context_management": object          // rare — {applied_edits: []}
}
```

### Models Observed
| Model ID | Notes |
|----------|-------|
| `claude-opus-4-6` | Primary model |
| `claude-sonnet-4-5-20250929` | Sonnet variant |
| `claude-opus-45` | Older Opus |
| `claude-haiku-4-5-20251001` | Haiku (fast/cheap) |
| `<synthetic>` | Synthetic/system-generated messages (e.g., compact summaries) |

### Stop Reasons
| Value | Description |
|-------|-------------|
| `tool_use` | Model wants to call a tool |
| `stop_sequence` | Model finished its response / hit stop sequence |
| `null` | Streaming entry (not the final chunk) |

**Important**: `end_turn` was NOT observed in any of 91K entries. Only `tool_use` and `stop_sequence`.

---

## 4. Content Block Types (`message.content[]`)

### For `type: "assistant"` entries

#### `text`
```json
{
  "type": "text",
  "text": "string"           // The actual text content
}
```

#### `thinking`
```json
{
  "type": "thinking",
  "thinking": "string",      // The thinking/reasoning content
  "signature": "string"      // Cryptographic signature (long base64 string)
}
```

#### `tool_use`
```json
{
  "type": "tool_use",
  "id": "string",            // Tool use ID (e.g., "toolu_01VFGT5myMdMVuhwLaBZ5Jak")
  "name": "string",          // Tool name (see Tool Names section)
  "input": {},               // Tool input parameters (DO NOT extract - contains code/content)
  "caller": {                // Optional
    "type": "direct"         // Only value observed
  }
}
```

### For `type: "user"` entries

User message `content` can be:

1. **Plain string** — Simple text message from human
2. **Array of content blocks**:

#### `tool_result`
```json
{
  "type": "tool_result",
  "tool_use_id": "string",   // References the tool_use.id it responds to
  "content": "string" | [blocks] | null,  // See below
  "is_error": boolean        // true when tool execution failed
}
```

**tool_result content variations:**
- `string` — Plain text output
- `[{"type": "text", ...}]` — Structured text
- `[{"type": "image", "source": {"type": "base64", "data": "...", "media_type": "string"}}]` — Screenshot/image
- `null` — No output

#### `text` (in user messages)
```json
{
  "type": "text",
  "text": "[Request interrupted by user]"   // User interruption signal
}
```

---

## 5. Tool Names Observed

| Tool Name | Category |
|-----------|----------|
| `Bash` | Execution |
| `Read` | File I/O |
| `Write` | File I/O |
| `Edit` | File I/O |
| `Glob` | Search |
| `Grep` | Search |
| `WebSearch` | Web |
| `WebFetch` | Web |
| `TaskCreate` | Task Management |
| `TaskUpdate` | Task Management |
| `TaskList` | Task Management |
| `TaskOutput` | Task Management |
| `TaskStop` | Task Management |
| `Task` | Task Management (legacy?) |
| `TodoWrite` | Task Management (legacy?) |
| `Skill` | Skill invocation |
| `EnterPlanMode` | Plan Mode |
| `ExitPlanMode` | Plan Mode |
| `AskUserQuestion` | UI |

---

## 6. Usage Object Structure

```json
{
  "input_tokens": 3,                              // Input tokens (often low due to caching)
  "output_tokens": 12,                             // Output tokens for this chunk
  "cache_creation_input_tokens": 1740,             // Tokens written to cache
  "cache_read_input_tokens": 18209,                // Tokens read from cache
  "cache_creation": {
    "ephemeral_5m_input_tokens": 0,                // 5-minute ephemeral cache
    "ephemeral_1h_input_tokens": 1740              // 1-hour ephemeral cache
  },
  "service_tier": "standard",                      // Always "standard" observed
  "inference_geo": "not_available" | "",            // Inference geography
  "server_tool_use": {
    "web_search_requests": 0,                      // Server-side web searches
    "web_fetch_requests": 0                        // Server-side web fetches
  },
  "speed": "standard",                             // Always "standard" observed
  "iterations": []                                 // Always empty array observed
}
```

**Key insight for cost calculation:**
- `totalTokens` = `input_tokens` + `cache_creation_input_tokens` + `cache_read_input_tokens` + `output_tokens`
- Cache tokens have different pricing tiers (creation vs read, ephemeral duration)
- `input_tokens` is very small when cache is used heavily

---

## 7. Top-Level `toolUseResult` Structure

The `toolUseResult` field on `type: "user"` entries contains structured result data from tool execution. It varies by tool:

### Bash Results
```
{stdout, stderr, interrupted, isImage, noOutputExpected}
{stdout, stderr, interrupted, isImage, returnCodeInterpretation}
{stdout, stderr, interrupted, isImage}
```
- `returnCodeInterpretation` values: `"No matches found"`, `"Some directories were inaccessible"`
- `interrupted` always `false` in observed data
- `isImage` always `false` in observed data

### File I/O Results
```
# Read
{content, filenames, mode, numFiles, numLines}
{filenames, mode, numFiles}           # file list mode

# Write
{file, type}
{durationMs, filenames, numFiles, truncated}

# Edit
{content, filePath, originalFile, structuredPatch, type}
{filePath, newString, oldString, originalFile, structuredPatch}

# Glob
{appliedLimit, filenames, mode, numFiles}

# Grep
{content, filenames, mode, numFiles, numMatches}
```

### Task Results
```
{task}                                 # TaskCreate result
{tasks}                                # TaskList result
{success, taskId, updatedFields}       # TaskUpdate result
{statusChange, success, taskId, updatedFields}  # TaskUpdate with status change
{retrieval_status, task}               # Task retrieval
{error, success, taskId, updatedFields}  # TaskUpdate error
{newTodos, oldTodos}                   # TodoWrite result
```

### Agent/Skill Results
```
{agentId, description, isAsync, prompt, status}           # Task (agent) start
{agentId, content, prompt, status, totalDurationMs}       # Task (agent) completion
{commandName, success}                                    # Skill result
```

### Search Results
```
{durationSeconds, query, results}      # WebSearch result
```

### AskUserQuestion Results
```
{questions, answers}
{questions, answers, annotations}      # With user annotations/notes
```

### Plan Mode Results
```
{filePath, isAgent, plan}              # EnterPlanMode
{command, message, task_id, task_type} # Task management
```

### Simple Results
```
string                                 # Plain text result
{message}                             # Simple message wrapper
```

---

## 8. Progress Entry Data Types

| `data.type` | Description |
|-------------|-------------|
| `bash_progress` | Bash command execution updates |
| `agent_progress` | Subagent execution updates |
| `hook_progress` | Hook (pre-commit, etc.) execution |
| `waiting_for_task` | Waiting for async task completion |

Progress entries also carry: `data.hookEvent`, `data.hookName`, `data.command`

---

## 9. Streaming Pattern

**Critical finding**: Assistant responses are streamed as multiple JSONL entries sharing the same `message.id` but with different `uuid` values.

Example: A single API response (`msg_01J31r7...`) produced 4 entries:
```
Entry 0: content=[text(len=2)]          stop_reason=null
Entry 1: content=[thinking(len=311)]    stop_reason=null
Entry 2: content=[text(len=54)]         stop_reason=null
Entry 3: content=[tool_use(AskUser)]    stop_reason=null
```

**Pattern details:**
- 9,067 messages had >1 entry (out of ~31K assistant entries)
- Maximum observed: 20 entries for a single message
- Each entry has the **same** `message.id` and `requestId`
- Each entry has a **different** `uuid`
- Each entry has **incremental** content (NOT cumulative)
- `usage` values are **duplicated** across all streaming entries of same message
- `stop_reason` is `null` on ALL streaming entries (even the last one)
- Content blocks appear one at a time: first text, then thinking, then more text, then tool_use
- `parentUuid` chains: each entry points to the previous entry's UUID

**Deduplication strategy**: Group by `message.id` (or `requestId`), concatenate content blocks, take usage from any entry (they're identical).

---

## 10. Subagent File Structure

### Directory Layout
```
~/.claude/projects/<project-dir>/
  <session-uuid>.jsonl                    # Main session file
  <session-uuid>/
    subagents/
      agent-a<hash>.jsonl                 # Regular subagent (270 files observed)
      agent-acompact-<hash>.jsonl         # Compacted subagent (86 files observed)
```

### Key Differences: Main vs Subagent

| Field | Main Session | Subagent |
|-------|-------------|----------|
| First entry type | `file-history-snapshot` | `user` (tool_result or compact prompt) |
| `agentId` | absent | present (e.g., "a4682d9") |
| `slug` | present after first few entries | always present |
| `isSidechain` | `false` | `true` |
| `sessionId` | matches filename | matches **parent** session UUID |
| File naming | `<uuid>.jsonl` | `agent-a<hash>.jsonl` |

### Compact Subagent Files
- Filename pattern: `agent-acompact-<hash>.jsonl`
- **Start with a tool_result** from the parent, followed by the compact prompt
- The compact prompt is a large `type: "user"` entry with `role: "user"` containing detailed instructions for summarization
- Then contains the summary response

### Compact Prompt Structure
The compact/summary prompt injected by Claude Code asks the agent to produce a `<summary>` with:
1. Primary Request and Intent
2. Key Technical Concepts
3. Files and Code Sections
4. Errors and fixes
5. Problem Solving
6. All user messages
7. Pending Tasks
8. Current Work
9. Optional Next Step

---

## 11. Compact/Summary System

### Compact Boundary (`system:compact_boundary`)
```json
{
  "type": "system",
  "subtype": "compact_boundary",
  "content": "Conversation compacted",
  "level": "info",
  "isMeta": false,
  "parentUuid": null,
  "logicalParentUuid": "<uuid>",        // Points to last real message before compaction
  "compactMetadata": {
    "trigger": "auto",                   // Compaction was triggered automatically
    "preTokens": 167088                  // Token count before compaction
  }
}
```

### Compact Summary Entries
Entries with `isCompactSummary: true`:
- `type: "user"` or `type: "assistant"`
- Contain the compacted conversation summary
- Have `isVisibleInTranscriptOnly: true` (70 entries total observed)
- Use `model: "<synthetic>"` for assistant summaries
- `stop_reason: "stop_sequence"` for synthetic entries

---

## 12. Edge Cases & Anomalies

### User Interruption
```json
{
  "type": "user",
  "message": {
    "role": "user",
    "content": [{"type": "text", "text": "[Request interrupted by user]"}]
  }
}
```

### API Errors with Retry
```json
{
  "type": "system",
  "subtype": "api_error",
  "level": "error",
  "error": {"cause": {"code": "ConnectionRefused", "path": "http://...", "errno": 0}},
  "retryInMs": 507.11,
  "retryAttempt": 1,
  "maxRetries": 10
}
```
- Exponential backoff visible: 507ms -> 1224ms -> 2216ms -> 4461ms -> 8307ms -> 16637ms

### API Error Messages on Entries
Some entries have both `isApiErrorMessage: true` and `apiError: string` — these are assistant-type entries where the API returned an error.

### Tool Use Errors in Content
```json
{
  "type": "tool_result",
  "content": "<tool_use_error>InputValidationError: [...]</tool_use_error>",
  "is_error": true,
  "tool_use_id": "toolu_..."
}
```

### File History Snapshots with Actual Backups
When `isSnapshotUpdate: true`:
```json
{
  "snapshot": {
    "messageId": "...",
    "trackedFileBackups": {"site/index.html": "..."},   // filename -> content
    "timestamp": "..."
  }
}
```

### Empty/Minimal Entries
- Smallest file observed: 236 bytes (single `file-history-snapshot` entry)
- Sessions that crashed immediately: only snapshot + user message + api_error entries

### Zero JSON Decode Errors
All 91,306 entries across 461 files parsed successfully. No truncated or corrupted entries observed.

---

## 13. Queue Operations

| Operation | Description |
|-----------|-------------|
| `enqueue` | Add message to processing queue |
| `dequeue` | Remove message from queue (processing started) |
| `remove` | Remove specific message from queue |
| `popAll` | Clear entire queue |

---

## 14. Metrics Extractable for MyBurn

### Per-Session Metrics
- Session duration: `first_timestamp` to `last_timestamp`
- Turn count: count of `type: "user"` entries (excluding tool_results)
- Turn duration: `system:turn_duration` entries give exact ms per turn
- Token totals: sum `usage` across deduplicated messages
- Tool usage: count tool_use content blocks by `name`
- Error count: count `system:api_error` entries
- Compaction events: count `system:compact_boundary` entries
- Pre-compaction token count: `compactMetadata.preTokens`

### Per-Message Metrics
- Model used
- Input/output/cache tokens (from `usage`)
- Thinking presence (content blocks with `type: "thinking"`)
- Tools called (from `tool_use` blocks)
- Streaming chunk count (entries sharing same `message.id`)

### Subagent Metrics
- Subagent count per session: count files in `subagents/` dir
- Subagent compactions: count `agent-acompact-*` files
- Subagent total tokens: parse subagent JSONL files

### New Metrics (not in ccusage)
- **YOLO mode detection**: `permissionMode: "bypassPermissions"`
- **Tool breakdown**: exact tool name counts (Bash, Read, Edit, Grep, etc.)
- **Web search/fetch**: `server_tool_use.web_search_requests` / `web_fetch_requests`
- **Thinking tokens**: presence of `thinking` content blocks (token count unavailable directly)
- **Compaction frequency**: how often conversations get compacted
- **Error rate**: API errors per session
- **Retry patterns**: exponential backoff tracking
- **Git branch**: what branch work happened on
- **CLI version**: track adoption of new Claude Code versions
- **Plan mode usage**: `EnterPlanMode`/`ExitPlanMode` tool calls
- **Subagent depth**: parent-child tool chain analysis
- **Image tool results**: screenshot usage via `isImage` in Bash or image blocks in tool_results

---

## 15. Deduplication Strategy

**Primary dedup key**: `message.id` + `requestId` (for assistant entries)

Each API response produces N streaming entries with the same `message.id`. To get the full response:
1. Group all entries by `message.id`
2. Collect all content blocks from all entries (they are incremental, not cumulative)
3. Take `usage` from any entry (identical across all)
4. Take `stop_reason` from the last entry (though often null — may need to check if `tool_use` block is present)
5. Use the **first** entry's `uuid` as the canonical ID, or the **last** for the final state

**For user entries**: No dedup needed — each user message is a single entry.

**For `requestId`**: All streaming entries of the same response share the same `requestId`. This is the safest dedup key when `message.id` is not available.

---

## 16. File Path Convention

```
~/.claude/projects/-home-gekko-projects-ccusage-dashboard/
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
                    Project working directory with / replaced by -
```

The project directory name is the absolute path to the project with all `/` replaced by `-` and the leading `/` becoming `-`.
