Skip to content

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:

FieldTypeDescription
session_idstringCurrent Claude Code session ID
transcript_pathstringPath to conversation transcript JSONL file
cwdstringCurrent working directory
hook_event_namestringEvent name
permission_modestring"default" | "plan" | "acceptEdits" | "dontAsk" | "bypassPermissions"
agent_idstring?Included only inside subagent
agent_typestring?Included when --agent flag or subagent is used

[Observed] transcript_path and permission_mode are actually missing in some events. Missing events: SessionStart, SessionEnd, SubagentStart, PreCompact, PostCompact Hook code should not depend on these fields.


Per-Event Payloads

SessionStart

Trigger: After Claude Code startup, resume, /clear, /compact

FieldTypeValue
hook_event_namestring"SessionStart"
sourcestring"startup" | "resume" | "clear" | "compact"
modelstringModel ID in use ([Observed] undocumented in spec)

[Observed] No transcript_path, permission_mode. [Observed] model field actually exists (e.g., "claude-sonnet-4-6").


SessionEnd

Trigger: On session closure

FieldTypeValue
hook_event_namestring"SessionEnd"
reasonstring"clear" | "resume" | "logout" | "prompt_input_exit" | "bypass_permissions_disabled" | "other"

[Observed] No transcript_path, permission_mode.


UserPromptSubmit

Trigger: When user message is submitted

FieldTypeValue
hook_event_namestring"UserPromptSubmit"
promptstringFull user input text

PreToolUse

Trigger: Just before tool execution

FieldTypeValue
hook_event_namestring"PreToolUse"
tool_namestringTool name (Bash, Edit, Write, Read, Glob, Grep, Agent, Skill, mcp__*, etc.)
tool_inputobjectPer-tool input (see below)
tool_use_idstringUnique tool call ID

[Observed] When tool is called inside subagent, agent_id and agent_type are 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/tool

The 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:

typescript
// 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:

ToolAdditional MetadataDescription
WebSearchmetadata.webUrls: string[]Search query stored up to 300 chars. Displayed in Dashboard Exploration tab Web Lookups section
WebFetchmetadata.webUrls: string[]Fetched URL stored up to 300 chars
All explore toolsmetadata.toolInputOriginal tool_input stored as JSON string (for debugging)
mcp__*Per-tool custom fieldsUnique 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

FieldTypeValue
hook_event_namestring"PostToolUse"
tool_namestringTool name
tool_inputobjectSame as PreToolUse
tool_responseobjectTool execution result (varies per tool, can be large)
tool_use_idstringUnique tool call ID

[Observed] tool_response can be very large, including full file contents in Read/Edit, etc. Recommended to remove or truncate when logging.


PostToolUseFailure

Trigger: After tool execution failure

FieldTypeValue
hook_event_namestring"PostToolUseFailure"
tool_namestringTool name
tool_inputobjectSame as PreToolUse
tool_use_idstringUnique tool call ID
errorstringError message
is_interruptboolean?Whether interrupted by user

SubagentStart

Trigger: When subagent starts

FieldTypeValue
hook_event_namestring"SubagentStart"
agent_idstringUnique subagent ID
agent_typestringSubagent type name (e.g., "general-purpose")

[Observed] No transcript_path, permission_mode.


SubagentStop

Trigger: When subagent stops Fire order: SubagentStopPostToolUse(Agent)

FieldTypeValue
hook_event_namestring"SubagentStop"
agent_idstringUnique subagent ID
agent_typestringSubagent type name
stop_hook_activebooleanWhether stop hook is active
agent_transcript_pathstringSubagent transcript path
last_assistant_messagestringFull last response from subagent

[Observed] When /compact is performed, a compact-specific subagent runs internally. In this case, agent_type comes 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)

FieldTypeValue
hook_event_namestring"PreCompact"
triggerstring"manual" | "auto"
custom_instructionsstringUser compact instructions (empty string "" if not provided)

[Observed] No transcript_path, permission_mode. [Observed] custom_instructions is empty string "" if not provided, not null.


PostCompact

Trigger: After context compression completes

FieldTypeValue
hook_event_namestring"PostCompact"
triggerstring"manual" | "auto"
compact_summarystringFull compression summary (<analysis>...</analysis><summary>...</summary> format, can be very long)

[Observed] No transcript_path, permission_mode. [Observed] compact_summary is 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

FieldTypeValue
session_idstringCurrent Claude Code session ID
versionstringClaude Code client version
model.idstringModel ID (e.g., "claude-sonnet-4-6")
model.display_namestringHuman-readable model name
context_window.used_percentagenumber | nullContext usage as % of the active model's window
context_window.remaining_percentagenumber | nullRemaining % of the active model's window
context_window.total_input_tokensnumberAccumulated input tokens
context_window.total_output_tokensnumberAccumulated output tokens
context_window.context_window_sizenumberActive model's total window size (varies by model)
context_window.current_usageobject | nullLatest API call breakdown (input / output / cache-creation / cache-read)
cost.total_cost_usdnumberCumulative session cost in USD
rate_limits.five_hour.used_percentagenumber5-hour rate-limit usage (Pro/Max only)
rate_limits.five_hour.resets_atnumberUnix seconds; reset timestamp for 5-hour window
rate_limits.seven_day.used_percentagenumber7-day rate-limit usage (Pro/Max only)
rate_limits.seven_day.resets_atnumberUnix seconds; reset timestamp for 7-day window

[Observed] rate_limits is only present for Pro/Max plans. [Observed] used_percentage is 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.snapshot event (lane telemetry) 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.XXX to stdout for Claude Code's status bar. Skips posting (but still writes stdout) when there is no meaningful used_percentage or rate_limits payload yet.
  • The Timeline's bottom context chart consumes context.snapshot events 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
  └─ SessionEnd

Subagent 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:

  • agentId absent → falls through to resolveSessionIds(sessionId) (parent task, unchanged)
  • agentId present → calls resolveSubagentSessionIds(sessionId, agentId, agentType):
    1. Resolve parent session to obtain parentTaskId
    2. Call ensureRuntimeSession(virtualId, title, { parentTaskId }) → server creates (or reuses) the background child task
    3. Return the child (taskId, sessionId) for this hook invocation

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

EventBehaviour
SubagentStartEagerly 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)
SubagentStopCalls POST /api/runtime-session-end for the virtual session to trigger auto-completion; cursor for sub--{agentId} is deleted

Code Implementation Notes

SituationCurrentRecommended
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 loggingBeing removed in hookLogPayloadPreserve
session_id in subagent hooksParent's session_id is always sentUse agent_id + resolveEventSessionIds to route to child task

Local-first documentation for Agent Tracer.