Claude Code Hook Payload Spec
Official documentation: https://code.claude.com/docs/en/hooks
This document organizes stdin payloads focusing on the Claude hook subset currently used by Agent Tracer. The [Observed] notation indicates parts where official spec and actual behavior differ.
Events currently in official docs but not covered in detail on this page: InstructionsLoaded, PermissionRequest, Notification, TaskCreated, TaskCompleted, StopFailure, ConfigChange, CwdChanged, FileChanged, WorktreeCreate, WorktreeRemove, Elicitation, ElicitationResult, TeammateIdle.
Common Fields
Fields included in all hook events per official spec:
| Field | Type | Description |
|---|---|---|
session_id | string | Current Claude Code session ID |
transcript_path | string | Path to conversation transcript JSONL file |
cwd | string | Current working directory |
hook_event_name | string | Event name |
permission_mode | string | "default" | "plan" | "acceptEdits" | "dontAsk" | "bypassPermissions" |
agent_id | string? | Included only inside subagent |
agent_type | string? | Included when --agent flag or subagent is used |
[Observed]
transcript_pathandpermission_modeare actually missing in some events. Missing events:SessionStart,SessionEnd,SubagentStart,PreCompact,PostCompactHook code should not depend on these fields.
Per-Event Payloads
SessionStart
Trigger: After Claude Code startup, resume, /clear, /compact
| Field | Type | Value |
|---|---|---|
hook_event_name | string | "SessionStart" |
source | string | "startup" | "resume" | "clear" | "compact" |
model | string | Model ID in use ([Observed] undocumented in spec) |
[Observed] No
transcript_path,permission_mode. [Observed]modelfield actually exists (e.g.,"claude-sonnet-4-6").
SessionEnd
Trigger: On session closure
| Field | Type | Value |
|---|---|---|
hook_event_name | string | "SessionEnd" |
reason | string | "clear" | "resume" | "logout" | "prompt_input_exit" | "bypass_permissions_disabled" | "other" |
[Observed] No
transcript_path,permission_mode.
UserPromptSubmit
Trigger: When user message is submitted
| Field | Type | Value |
|---|---|---|
hook_event_name | string | "UserPromptSubmit" |
prompt | string | Full user input text |
PreToolUse
Trigger: Just before tool execution
| Field | Type | Value |
|---|---|---|
hook_event_name | string | "PreToolUse" |
tool_name | string | Tool name (Bash, Edit, Write, Read, Glob, Grep, Agent, Skill, mcp__*, etc.) |
tool_input | object | Per-tool input (see below) |
tool_use_id | string | Unique tool call ID |
[Observed] When tool is called inside subagent,
agent_idandagent_typeare additionally included. This allows identifying which subagent made the call.
tool_input structure (per tool):
Bash: { command, description?, timeout?, run_in_background? }
Edit: { file_path, old_string, new_string, replace_all? }
Write: { file_path, content }
Read: { file_path, offset?, limit? }
Glob: { pattern, path? }
Grep: { pattern, path?, glob?, output_mode?, "-i"?, multiline? }
WebSearch: { query }
WebFetch: { url, prompt? }
Agent: { description?, prompt, subagent_type?, model?, run_in_background? }
Skill: { skill, args? }
mcp__*: Varies by MCP server/toolThe structure above summarizes fields currently used by Agent Tracer. For exhaustive schema, consult the official hooks reference first.
Agent Tracer Custom Metadata
Semantic Metadata Contract
Shared semantic metadata consumed by the UI:
// Defined in packages/domain/src/interop/event-semantic.ts
interface EventSemanticMetadata {
readonly subtypeKey: EventSubtypeKey; // "read_file", "run_test", "mcp_call", ...
readonly subtypeLabel?: string; // UI-friendly label
readonly subtypeGroup: EventSubtypeGroup; // "files", "execution", "coordination", ...
readonly toolFamily: EventToolFamily; // "explore", "file", "terminal", "coordination"
readonly operation: string; // "search", "modify", "execute", "delegate"
readonly entityType?: string; // "file", "directory", "command", ...
readonly entityName?: string; // Specific filename, command name, etc.
readonly sourceTool?: string; // Original tool name
readonly importance?: string; // "critical", "normal", "minor"
}This contract is derived server-side at ingestion inside @monitor/server (see the classification paths under packages/server/src/application/events/). The plugin sends raw payloads only. The derived fields are consumed by the web dashboard through packages/web/src/app/lib/timeline.ts.
Per-Tool Additional Metadata
explore.ts and tool_used.ts inject per-tool additional information into the metadata field:
| Tool | Additional Metadata | Description |
|---|---|---|
WebSearch | metadata.webUrls: string[] | Search query stored up to 300 chars. Displayed in Dashboard Exploration tab Web Lookups section |
WebFetch | metadata.webUrls: string[] | Fetched URL stored up to 300 chars |
| All explore tools | metadata.toolInput | Original tool_input stored as JSON string (for debugging) |
mcp__* | Per-tool custom fields | Unique metadata per MCP tool |
These fields are Agent Tracer's own extensions not present in the official Claude Code hook payload spec.
PostToolUse
Trigger: After successful tool execution
| Field | Type | Value |
|---|---|---|
hook_event_name | string | "PostToolUse" |
tool_name | string | Tool name |
tool_input | object | Same as PreToolUse |
tool_response | object | Tool execution result (varies per tool, can be large) |
tool_use_id | string | Unique tool call ID |
[Observed]
tool_responsecan be very large, including full file contents in Read/Edit, etc. Recommended to remove or truncate when logging.
PostToolUseFailure
Trigger: After tool execution failure
| Field | Type | Value |
|---|---|---|
hook_event_name | string | "PostToolUseFailure" |
tool_name | string | Tool name |
tool_input | object | Same as PreToolUse |
tool_use_id | string | Unique tool call ID |
error | string | Error message |
is_interrupt | boolean? | Whether interrupted by user |
SubagentStart
Trigger: When subagent starts
| Field | Type | Value |
|---|---|---|
hook_event_name | string | "SubagentStart" |
agent_id | string | Unique subagent ID |
agent_type | string | Subagent type name (e.g., "general-purpose") |
[Observed] No
transcript_path,permission_mode.
SubagentStop
Trigger: When subagent stops Fire order: SubagentStop → PostToolUse(Agent)
| Field | Type | Value |
|---|---|---|
hook_event_name | string | "SubagentStop" |
agent_id | string | Unique subagent ID |
agent_type | string | Subagent type name |
stop_hook_active | boolean | Whether stop hook is active |
agent_transcript_path | string | Subagent transcript path |
last_assistant_message | string | Full last response from subagent |
[Observed] When
/compactis performed, a compact-specific subagent runs internally. In this case,agent_typecomes as empty string""(regular subagents have a type name). Current code treats this with|| "unknown", but preserving""as-is is needed to identify compact agents.
PreCompact
Trigger: Just before context compression (/compact or automatic)
| Field | Type | Value |
|---|---|---|
hook_event_name | string | "PreCompact" |
trigger | string | "manual" | "auto" |
custom_instructions | string | User compact instructions (empty string "" if not provided) |
[Observed] No
transcript_path,permission_mode. [Observed]custom_instructionsis empty string""if not provided, notnull.
PostCompact
Trigger: After context compression completes
| Field | Type | Value |
|---|---|---|
hook_event_name | string | "PostCompact" |
trigger | string | "manual" | "auto" |
compact_summary | string | Full compression summary (<analysis>...</analysis><summary>...</summary> format, can be very long) |
[Observed] No
transcript_path,permission_mode. [Observed]compact_summaryis XML-formatted analysis + summary text, several KB in size.
StatusLine
Trigger: After every API request and on session start. Unlike the other hook events, statusLine is declared as a top-level key (not nested under hooks) in the plugin's hooks.json, so the plugin itself owns registration via ${CLAUDE_PLUGIN_ROOT} — no project .claude/settings.json entry is needed (see claude-setup.md).
Ref: https://code.claude.com/docs/en/statusline
| Field | Type | Value |
|---|---|---|
session_id | string | Current Claude Code session ID |
version | string | Claude Code client version |
model.id | string | Model ID (e.g., "claude-sonnet-4-6") |
model.display_name | string | Human-readable model name |
context_window.used_percentage | number | null | Context usage as % of the active model's window |
context_window.remaining_percentage | number | null | Remaining % of the active model's window |
context_window.total_input_tokens | number | Accumulated input tokens |
context_window.total_output_tokens | number | Accumulated output tokens |
context_window.context_window_size | number | Active model's total window size (varies by model) |
context_window.current_usage | object | null | Latest API call breakdown (input / output / cache-creation / cache-read) |
cost.total_cost_usd | number | Cumulative session cost in USD |
rate_limits.five_hour.used_percentage | number | 5-hour rate-limit usage (Pro/Max only) |
rate_limits.five_hour.resets_at | number | Unix seconds; reset timestamp for 5-hour window |
rate_limits.seven_day.used_percentage | number | 7-day rate-limit usage (Pro/Max only) |
rate_limits.seven_day.resets_at | number | Unix seconds; reset timestamp for 7-day window |
[Observed]
rate_limitsis only present for Pro/Max plans. [Observed]used_percentageis already normalized against the active model's window size, so UI does not need to re-scale when switching between Opus (1M) and Sonnet (200K).
Agent Tracer behavior:
- Posts a
context.snapshotevent (lanetelemetry) on every status refresh. - Metadata keys mirror the payload:
contextWindowUsedPct,contextWindowRemainingPct,contextWindowSize,contextWindowInputTokens,contextWindowOutputTokens,contextWindowCacheCreationTokens,contextWindowCacheReadTokens,rateLimitFiveHourUsedPct,rateLimitFiveHourResetsAt,rateLimitSevenDayUsedPct,rateLimitSevenDayResetsAt,costTotalUsd,modelId,sessionVersion. - Writes
[monitor] ctx N% · 5h N% · $X.XXXto stdout for Claude Code's status bar. Skips posting (but still writes stdout) when there is no meaningfulused_percentageorrate_limitspayload yet. - The Timeline's bottom context chart consumes
context.snapshotevents to render the context-usage curve, the per-model band, and model-change markers.
Event Fire Order
Session start
└─ SessionStart
User input
└─ UserPromptSubmit
Tool execution
├─ PreToolUse
├─ (tool execution)
└─ PostToolUse | PostToolUseFailure
Agent tool execution
├─ PreToolUse (tool_name: "Agent")
├─ SubagentStart
├─ (tools inside subagent: PreToolUse / PostToolUse repeating)
├─ SubagentStop
└─ PostToolUse (tool_name: "Agent") ← after SubagentStop
/compact execution
├─ PreCompact
├─ SubagentStart (agent_type: "") ← compact-specific internal agent
├─ SubagentStop (agent_type: "")
└─ PostCompact
Session end
└─ SessionEndSubagent Event Routing
The session_id problem
All hooks — including those that fire inside a subagent — receive the parent session's session_id. The session_id field does not change when Claude Code dispatches a subagent. This means a naive resolveSessionIds(session_id) call always returns the parent task's IDs, causing subagent tool events to be recorded under the parent instead of a separate child task.
agent_id is the only field that distinguishes subagent context from parent context.
Virtual session ID pattern (Agent Tracer solution)
Agent Tracer resolves this by mapping every agent_id to a virtual session ID:
virtualId = `sub--${agentId}`resolveEventSessionIds(sessionId, agentId?, agentType?) is the canonical dispatcher:
agentIdabsent → falls through toresolveSessionIds(sessionId)(parent task, unchanged)agentIdpresent → callsresolveSubagentSessionIds(sessionId, agentId, agentType):- Resolve parent session to obtain
parentTaskId - Call
ensureRuntimeSession(virtualId, title, { parentTaskId })→ server creates (or reuses) the background child task - Return the child
(taskId, sessionId)for this hook invocation
- Resolve parent session to obtain
This relies entirely on the server's idempotent ensureRuntimeSession endpoint (EnsureRuntimeSessionUseCase). Since v0.2.0 the plugin no longer persists a session cache to disk — every hook subprocess calls the endpoint directly; repeated calls with the same runtimeSessionId return the same (taskId, sessionId) without creating duplicates.
Lifecycle
| Event | Behaviour |
|---|---|
SubagentStart | Eagerly calls resolveSubagentSessionIds → child background task created immediately via idempotent ensureRuntimeSession(virtualId, …, { parentTaskId }) |
PreToolUse (inside subagent) | Calls resolveEventSessionIds(sessionId, agentId) → ensures session exists before first tool fires |
PostToolUse/* (inside subagent) | All tool events routed to child task timeline via resolveEventSessionIds |
Stop (inside subagent) | assistant.response recorded on child task; session-end skipped (SubagentStop handles it) |
SubagentStop | Calls POST /api/runtime-session-end for the virtual session to trigger auto-completion; cursor for sub--{agentId} is deleted |
Code Implementation Notes
| Situation | Current | Recommended |
|---|---|---|
transcript_path usage | - | Do not use, may be missing |
agent_type default | || "unknown" | Preserve as || "" to distinguish compact agents |
custom_instructions empty | || "" (already handled) | Keep current code |
compact_summary logging | - | Length limit required (several KB) |
tool_response logging | Being removed in hookLogPayload | Preserve |
session_id in subagent hooks | Parent's session_id is always sent | Use agent_id + resolveEventSessionIds to route to child task |