This article examines the public mirror’s March 31, 2026 source snapshot. The mirror is not an official Anthropic source release. Implementation claims concern this fixed snapshot, not necessarily the current product release; official documentation establishes product or API contracts.
A hook configuration tells you which script runs, but not why its result matters. To understand the behavior, follow the event to the code that consumes its output: prompt processing, permission checks, tool execution, turn completion, or conversation compaction.
Reading goal: identify what each hook receives, how matching selects it, how stdout becomes a structured result, and which part of the current request that result is allowed to change.
shape-level hook event
-> getMatchingHooks(settings, plugin, skill, session)
-> execute matched hook handlers
-> parse stdout as protocol result
-> { decision, additionalContext, updatedInput? }
PreToolUse runs before permission/execution.
PostToolUse runs after the tool result exists.
Stop and Compact run at turn or memory boundaries.
1. Events describe different execution steps
HOOK_EVENTS includes UserPromptSubmit, PreToolUse, PostToolUse, PermissionRequest, SessionStart, Stop, PreCompact, PostCompact, and subagent events. Some run before work, some after work, and some when a session is reconstructed. They are not interchangeable notifications.
Every event receives common fields such as session_id, transcript path, working directory, permission mode, and optional agent identity. Event-specific schemas add the data needed at that moment: tool hooks receive the tool name, input, and id; Stop receives the last assistant message; PreCompact receives the trigger and custom instructions, while PostCompact receives the generated summary.
| Event | Input available to the hook | Possible effect |
|---|---|---|
UserPromptSubmit | The submitted prompt. | Stop the query or add a separate context message. |
PreToolUse | Tool name, input, and tool-use id. | Modify input, add context, or propose allow, ask, or deny. |
PostToolUse | The tool request and result. | Add context; MCP tool output may also be replaced. |
SessionStart | The source: startup, resume, clear, or compact. | Add initial context, a user message, or watched paths. |
Stop / compact events | Turn completion or compaction details. | Continue or stop, adjust compact instructions, or restore context. |
2. Hooks are collected, matched, and checked before execution
getHooksConfig() combines saved settings, registered hooks, session hooks, and session function hooks for the current event. A managed-only policy can exclude plugin and session hooks. getMatchingHooks() then chooses a matching field based on the event: tool name for tool events, source for SessionStart, trigger for compact, notification type for notifications, and agent type for subagent events.
Matchers support exact strings, pipe-separated alternatives, and regular expressions. Tool-related events may also use if conditions such as Bash(git *). Before execution, Claude Code removes duplicates by source and checks global hook disablement, simple mode, and workspace trust. A configured hook does not automatically run in every environment.
3. PreToolUse and PostToolUse have different authority
checkPermissionsAndCallTool() runs PreToolUse before the main permission decision and before the tool changes local state. The hook can propose modified input, add context, stop execution, or return a permission preference. That preference is not the final decision.
resolveHookPermissionDecision() explicitly preserves deny and ask rules from settings. A hook's allow result cannot override them. A tool requiring interaction uses the normal permission path unless the hook supplies updated input that satisfies that interaction; requireCanUseTool still forces the normal path. Otherwise hook allow uses a rule-and-tool check and may return without traversing the entire normal permission stack. Multiple hook results use the priority deny > ask > allow.
This checkRuleBasedPermissions() helper still parses the input and runs tool.checkPermissions(), preserving tool denials, content-specific ask rules, and safetyCheck asks. It skips the full mode and default-allow flow; its checks extend beyond settings rules.

PostToolUse runs after a result exists. It can return a blocking error or additional context for the next model request. updatedMCPToolOutput is deliberately narrower: Claude Code applies it only when the completed tool is an MCP tool, so an ordinary built-in tool cannot have its result silently replaced through this field.
4. Stdout is parsed as a small protocol
A command hook writes to stdout, but Claude Code does not treat every line as an instruction. parseHookOutput() trims the output and attempts JSON parsing only when it begins with {. Valid JSON is checked against hook output schemas and translated into fields such as preventContinuation, permission behavior, updated input, additional context, MCP output replacement, and retry.
Non-JSON command output also has defined status handling. Exit code 0 is successful, exit code 2 produces blocking feedback, and other nonzero codes are shown as non-blocking errors. For JSON output, {"continue": false} stops the current step and can carry a stopReason. Structured fields, not arbitrary prose, determine what the runtime changes.
4.1 Accepted, completed, and delivered are separate states
A command hook can request background execution through its async configuration or by writing {"async":true} as the first stdout line. forceSyncExecution prevents that transfer. The returned backgrounded: true and success status mean the runtime accepted ownership of the background process, not that its work succeeded.
async configuration / first line {"async":true}
→ registered in background: foreground step continues
→ process completed: read final output
→ later main-thread attachment collection
→ systemMessage / additionalContext enter model messages
Accepted ≠ successfully completed ≠ delivered to the modelThe registry waits for completed, skips the async marker while extracting a response, marks the response as collected, and removes the entry. Running, killed, or empty-output processes take separate paths. This is an in-process Map, not a durable queue that reconstructs jobs after restart. Attachment collection produces async_hook_response; message conversion turns only systemMessage and additionalContext into model context. It does not replay a permission decision or undo a tool.
asyncRewake is a separate route: it bypasses the registry and enqueues a task notification when the process finishes with exit code 2. That may wake an idle model or enter queued messages during a query. Ordinary async hooks do not inherit this active wakeup behavior.
| Output | Runtime interpretation |
|---|---|
| Plain stdout | Display or context material according to the event, not an automatic permission decision. |
| Non-JSON exit code 2 | Blocking feedback; the event’s caller decides which step is blocked. |
continue: false | Stop continuation with an optional stop reason. |
updatedInput | Replace input through allow/ask or passthrough handling. |
additionalContext | A separate hook-context attachment. |
async: true | Background acceptance, not completion. |
5. Prompt, session, stop, and compact hooks change different moments
UserPromptSubmit runs before the model query. A blocking result sets shouldQuery to false; additional context becomes a separate hook_additional_context message instead of being disguised as user text.
SessionStart is not limited to process startup. Its source may be startup, resume, clear, or compact. The handler can collect context, an initial user message, and file-watch paths whenever the session becomes ready to continue.
Stop runs after the assistant turn settles, not after each tool. These outcomes differ: continue: false becomes preventContinuation, emits hook_stopped_continuation, and stops the loop. A blocking error instead adds feedback to messages and runs another model turn with stopHookActive: true. SubagentStop provides the corresponding event for child loops.
Full compaction has three related hook moments. PreCompact can add instructions before the summary is generated. After the summary and important attachments are restored, Claude Code runs SessionStart with source: compact so session-level information can be added again. PostCompact runs after the replacement message components have been prepared, but before compactConversation() returns them. The query loop installs the returned messages afterward.

The distinction is visible in the query-loop branch: stopping continuation ends the query, whereas blocking completion requests further work. Hook output must be interpreted at its consuming event.
6. Choose a hook by the action you need
| Goal | Hook | Restriction that still applies |
|---|---|---|
| Add project context before a query. | UserPromptSubmit or SessionStart | The content is marked as hook context, not user-authored text. |
| Validate or adjust tool input. | PreToolUse | Initial input passes schema and semantic validation; modified input follows the selected permission path. Full semantic validation is not guaranteed to run again. |
| Add guidance after a tool result. | PostToolUse | Only MCP tool output can be replaced. |
| Make an automated permission decision. | PermissionRequest | An unanswered headless request is denied. |
| Act when a turn or child agent ends. | Stop or SubagentStop | This concerns the whole loop, not one tool result. |
The useful rule is to read a hook name together with the code that consumes its result. The same script mechanism has different authority before a prompt, before a tool, after a result, or during compaction.
Sources
The source-code claims in this article are based on the public mirror and the linked official documentation. Server-side model scheduling is not inferred from client-side hooks.