Skip to main content

Hook Input and Output

Each hook gets a JSON description of what happened. It answers with an exit code, or with exit 0 and a JSON object. The answer tells Claude Code to go on, to block, or to add context. For example, exit code 2 with a message on stderr blocks a PreToolUse call and tells Claude why.

You control the answer your hook gives, not the input. Each event decides which fields it reads.

Example​

A command hook reads the input, then answers with JSON to ask you before running terraform apply.

.claude/hooks/ask-terraform.sh
#!/bin/bash
input=$(cat)
cmd=$(jq -r '.tool_input.command // ""' <<<"$input")
if [[ "$cmd" == terraform\ apply* ]]; then
cat <<'JSON'
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "ask",
"permissionDecisionReason": "terraform apply changes real infrastructure"
}
}
JSON
fi
exit 0

The script exits 0 and prints one JSON object. Claude Code reads it and shows a permission prompt.

Common Input Fields​

A command hook reads this JSON on stdin. An HTTP hook gets it as the POST body.

FieldStatusWhat it holdsNotes
session_idDocumentedThe session identifier.A string.
transcript_pathDocumentedThe path to the conversation JSONL file.Written async, so it can lag. On Stop, use last_assistant_message. See Transcript Format.
cwdDocumentedThe working directory when the hook runs.Follows cd and worktrees.
prompt_idDocumentedThe UUID of the prompt in progress.Equals prompt.id in OpenTelemetry events. Absent before the first input.
scratchpad_dirDiffersThe session scratchpad folder.Needs 2.1.257. Sent on most events.
permission_modeDocumenteddefault, plan, acceptEdits, auto, dontAsk, or bypassPermissions.Manual mode is sent as default. Not on every event.
effortDocumentedAn object {level} with low, medium, high, xhigh, or max.Only in tool-use context, and only when the model supports effort. Sent on tool events, Stop, and SubagentStop.
hook_event_nameDocumentedThe name of the event.In the type file it is a literal on each event type, not on BaseHookInput.
agent_idDocumentedThe subagent or in-process teammate that the hook fires in.Absent on the main thread. On task and teammate events it names the actor (2.1.290).
agent_typeDocumentedThe agent name.Set in a subagent, or on the main thread with --agent.
mcp_serverDocumentedAn object {name, source} for an MCP tool.Needs 2.1.274. source is an open set: sdk, plugin, user, project, local, dynamic, managed, enterprise, claudeai, agent.

Universal Output Fields​

A hook prints one JSON object to stdout and exits 0. Every event accepts these keys. Some events drop them, and Events says which.

FieldStatusWhat it doesNotes
continueDocumentedfalse stops Claude after the hook.Beats event decisions. Ignored by TaskCreated, and by TaskCompleted when it comes from TaskUpdate.
stopReasonDocumentedThe message to show when continue is false.Stays in the conversation.
systemMessageDocumentedA warning for you.Some events drop it or send it somewhere else.
terminalSequenceDocumentedAn escape sequence that Claude Code writes for you.Allowed: OSC 0, 1, 2, 9, 99, 777, and BEL. Interactive mode only. Works on events that drop other fields. Not for WorktreeCreate command hooks.
decisionDiffersA top-level block.See Decision Patterns below.
reasonDocumentedThe text for the block.You or Claude sees it, by event.
hookSpecificOutputDocumentedAn event-specific object.Needs hookEventName set to the event name.
{"async":true,"asyncTimeout":n}UndocumentedA first reply that moves the hook to the background.See Configuration. Command hooks only.
10,000 character capDocumentedApplies to additionalContext, systemMessage, initialUserMessage, and plain stdout.Each string alone. Over the cap, Claude Code saves it to a file, and the model gets the path and the first 2,000 characters.

Decision Patterns​

PatternStatusWhat it doesEvents
Top-level decision: "block"DocumentedBlocks. reason is the message.UserPromptSubmit, UserPromptExpansion, PostToolUse, PostToolUseFailure, PostToolBatch, Stop, SubagentStop, ConfigChange, PreCompact, TaskCreated
hookSpecificOutput.permissionDecisionDocumentedallow, deny, ask, or defer. defer works only in -p.PreToolUse. PreModelSwitch has no defer.
hookSpecificOutput.decision.behaviorDocumentedallow or deny. Needs a decision object.PermissionRequest
continue: false or exit 2DocumentedStops the teammate.TeammateIdle, TaskCompleted
Path returnDocumentedA command prints the path. An HTTP hook returns worktreePath. A failure fails the creation.WorktreeCreate
Context onlyDocumentedadditionalContext. It cannot block.SessionStart, SubagentStart, PostModelSwitch, Setup, Notification
NoneDocumentedSide effects only.SessionEnd, PostCompact, InstructionsLoaded, StopFailure, CwdChanged, DirectoryAdded, FileChanged. The last two still return watchPaths.

Exit Codes​

Exit codeStatusWhat it doesNotes
0DocumentedSuccess.Claude Code reads JSON from stdout. Plain stdout becomes context only on UserPromptSubmit, UserPromptExpansion, SessionStart, and PostModelSwitch. Claude never sees stderr from an exit 0 hook.
2DocumentedA blocking error on events that can block.stderr is the reason. JSON fields are read but cannot override the block. Elicitation and ElicitationResult ignore them.
1DocumentedNot a block.Exit 1 is a non-blocking error. Use exit 2 for policy.
Any other codeDocumentedA non-blocking error.The action goes on. The notice has the first line of stderr. JSON on stdout can still decide.
Invalid JSONDocumentedA non-blocking error.The rules for one object, text, and several JSON lines are in the Exit code 0 section of the Hooks Reference.
tip

Pick one style per hook. Use exit codes alone, or exit 0 with JSON. If you mix them, exit 2 keeps its blocking effect and Claude Code still reads the JSON fields.

What Exit 2 Does per Event​

EffectStatusEvents
Blocks the actionDocumentedPreToolUse, UserPromptSubmit, UserPromptExpansion, Stop, SubagentStop, TeammateIdle, TaskCreated, TaskCompleted, ConfigChange (not policy_settings), PostToolBatch, PreCompact, PreModelSwitch, Elicitation, ElicitationResult
Fails the stepDocumentedWorktreeCreate always. WorktreeRemove if the folder still exists.
Shows stderr to Claude, no blockDocumentedPostToolUse, PostToolUseFailure
Shows stderr to you, no blockDocumentedSubagentStart, SessionStart, SessionEnd, CwdChanged, FileChanged, PostCompact, PostModelSwitch
IgnoredDocumentedPermissionRequest, PermissionDenied, Notification, Setup, StopFailure, InstructionsLoaded, MessageDisplay, and DirectoryAdded (its stderr goes to the debug log)

HTTP Responses​

ResponseStatusWhat it does
2xx with an empty bodyDocumentedSuccess. The same as exit 0 with no output.
2xx with a JSON objectDocumentedParsed like command JSON. A body that fails validation is a non-blocking error.
2xx with other text, non-2xx, or a connection errorDocumentedA non-blocking error. The text is not added to context. onFailure: "block" blocks.

How It Works​

  • Hooks: the overview.
  • Events: the fields of each event.
  • Handlers: what each handler type can return.
  • Agent Loop: a PreToolUse exit 2 example in context.

Source: Hooks Reference and the program (BaseHookInput, McpServerProvenance, ClassicResultFields, and the hook output schema). The exit code 2 table was checked against the Hooks Reference. Not verified: the exact list of events for which the program honors continue and systemMessage.