Issue 00 · Autumn 2026日本語 · RSS
AgentKoji

Claude Code Hooks: Events, Exit Codes and Examples

Claude Code has 33 hook events, and exit 2 blocks on 16 of them. One-page tables of events, exit codes and JSON fields, plus links to our tested hook guides.

TL;DR

  • Hooks are commands, HTTP endpoints, MCP tool calls, prompts or subagents that Claude Code runs at fixed points in its lifecycle. The docs list 33 events.
  • Exit 0 means success and exit 2 is a blocking error on events that can block. Any other exit code with no valid JSON is a non-blocking error, unless the hook sets onFailure to block.
  • To stop a tool call before it runs, use PreToolUse with exit 2 or a permissionDecision of deny.

Hooks are user-defined shell commands, HTTP endpoints, MCP tool calls, LLM prompts or subagents that Claude Code runs automatically at specific points in its lifecycle. They fire the same way in the terminal, IDE extensions, the Desktop app and cloud sessions. This page is a reference: one table of every event, one table of exit codes and JSON fields, a minimal example, and links to our guides that test each kind of hook. We checked everything against the official hooks reference on 2026-10-24, with Claude Code 2.1.310.

Every hook event

The docs list 33 events. “Exit 2” says what happens when a hook on that event exits with code 2. “Matcher” says what the matcher field is compared with; “none” means the event has no matcher support and always fires.

EventFires whenExit 2Matcher
SessionStartA session starts or resumesShows stderr to the user onlyHow it started (startup, resume, clear, compact, fork)
SetupOnly with --init-only, or --init / --maintenance with -pIgnoredinit, maintenance
InstructionsLoadedA CLAUDE.md or .claude/rules/*.md file is loadedIgnoredLoad reason
UserPromptSubmitA prompt is submitted, before Claude processes itBlocks the promptnone
UserPromptExpansionA typed command expands into a promptBlocks the expansionCommand name
MessageDisplayAn assistant message streams to the screenOriginal text is shownnone
PreToolUseAfter Claude builds a tool call, before it runsBlocks the tool callTool name
PermissionRequestClaude Code is about to ask for permissionNot honored; deny with the decision objectTool name
PostToolUseA tool completes successfullyShows stderr to Claude; the tool already ranTool name
PostToolUseFailureA tool that started executing failsShows stderr to ClaudeTool name
PostToolBatchEvery tool call in a batch has finishedStops the loop before the next model callnone
PermissionDeniedAuto mode denies a tool callIgnoredTool name
NotificationClaude Code sends a notificationIgnoredNotification type
SubagentStartA subagent is spawned or resumedShows stderr to the user onlyAgent type
SubagentStopA subagent finishes respondingKeeps the subagent workingAgent type
TaskCreatedA task is created with TaskCreateRolls back the creationnone
TaskCompletedA task is marked completedKeeps it from being marked completednone
StopThe main agent finishes respondingKeeps Claude workingnone
StopFailureThe turn ends on an API errorIgnored, except terminalSequenceError type
TeammateIdleAn agent-team teammate is about to go idleKeeps the teammate workingnone
ConfigChangeA configuration file changes mid-sessionBlocks the change (except policy_settings)Configuration source
CwdChangedA shell command changes the working directoryShows stderr to the user onlynone
DirectoryAddedA directory is added with /add-dirStderr goes to the debug logHow it was added
FileChangedA watched file changes on diskShows stderr to the user onlyLiteral filenames to watch
WorktreeCreateClaude Code creates a worktreeAny non-zero exit fails the creationnone
WorktreeRemoveClaude Code removes a hook-created worktreeAny non-zero exit fails the removalnone
PreCompactBefore a compact operationBlocks compactionmanual, auto
PostCompactAfter a compact operationShows stderr to the user onlymanual, auto
PreModelSwitchBefore a requested model switch is appliedBlocks the switchTarget model name
PostModelSwitchAfter the session’s model changesShows stderr to the user onlyTarget model name
SessionEndA session endsShows stderr to the user onlyWhy it ended
ElicitationAn MCP server asks for user inputDeclines the requestMCP server name
ElicitationResultThe user answers an MCP elicitationBlocks the responseMCP server name

Source: https://code.claude.com/docs/en/hooks (checked 2026-10-24)

Sixteen events can block with exit 2: PreToolUse, UserPromptSubmit, UserPromptExpansion, Stop, SubagentStop, TeammateIdle, TaskCreated, TaskCompleted, ConfigChange, PostToolBatch, PreCompact, PreModelSwitch, Elicitation, ElicitationResult, WorktreeCreate and WorktreeRemove. WorktreeCreate and WorktreeRemove fail on any non-zero exit code, and WorktreeRemove only when the directory still exists afterward.

Exit codes and JSON output

Your hookResult
exit 0Success. JSON on stdout, if any, is applied. For most events stdout goes to the debug log only; on UserPromptSubmit, UserPromptExpansion, SessionStart and PostModelSwitch, plain stdout is added as context for Claude
exit 2Blocking error on events that can block. Stderr is the reason. JSON is still read, but even "allow" can’t override the block
Any other code, including 1With plain text or no output: a non-blocking error; the action goes ahead and a hook error notice appears. With JSON that passes validation: the exit code is ignored and the JSON decides. The docs recommend exit 2 for policy hooks
Fails to start, times out, or prints invalid JSONNon-blocking error by default. Exception: on PreModelSwitch, a hook canceled at its timeout blocks the model switch
"onFailure": "block" on a command or HTTP hookA failure (including exit 1) does what exit 2 does on that event; on PermissionRequest, where exit 2 isn’t honored, it denies the request. Requires Claude Code v2.1.295 or later. Has no effect on Stop, SubagentStop, TaskCompleted, TeammateIdle or async hooks

Common JSON fields:

FieldWhereWhat it does
hookSpecificOutput.permissionDecisionPreToolUseallow, deny, ask or defer. Precedence across hooks: deny > defer > ask > allow. defer only works in -p runs
hookSpecificOutput.permissionDecisionReasonPreToolUseShown in the prompt for ask, shown to Claude for deny
hookSpecificOutput.updatedInputPreToolUseReplaces the whole tool input
decision: "block" and reasonPostToolUse, Stop, UserPromptSubmit and othersTop-level decision. On PostToolUse it adds the reason next to the tool result; on Stop it keeps Claude working
hookSpecificOutput.additionalContextSeveral eventsText added to Claude’s context
systemMessageMost events (some discard it)A message shown to the user

Source: https://code.claude.com/docs/en/hooks (checked 2026-10-24)

Where hooks live, and a minimal example

LocationScope
~/.claude/settings.jsonAll your projects
.claude/settings.jsonOne project; can be committed
.claude/settings.local.jsonOne project; not shared
Managed policy settingsOrganization-wide
Plugin hooks/hooks.jsonWhile the plugin is enabled
Skill or subagent frontmatterAfter the skill is invoked, or while the subagent runs

Hook entries merge across these levels instead of replacing each other (source: https://code.claude.com/docs/en/hooks, checked 2026-10-24).

The smallest useful PreToolUse hook blocks one command. This one stops npm publish in a dummy project:

{
  "hooks": {
    "PreToolUse": [
      { "matcher": "Bash",
        "hooks": [ { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/no-publish.sh" } ] }
    ]
  }
}
#!/usr/bin/env bash
# .claude/hooks/no-publish.sh  (PreToolUse, matcher: Bash)
cmd=$(jq -r '.tool_input.command // ""')
case "$cmd" in
  *"npm publish"*) echo "Blocked: publishing is done by a human." >&2; exit 2 ;;
esac
exit 0

We piped dummy JSON into it on 2026-10-24. {"tool_name":"Bash","tool_input":{"command":"npm publish"}} printed Blocked: publishing is done by a human. and exited 2. npm test exited 0. The settings file passed jq and claude doctor. We did not watch it fire in a live session.

Our hook guides, in reading order

Start with the concepts, then copy an example, then debug, then design.

Summary

FAQ

What is a PreToolUse hook in Claude Code?
A hook that runs after Claude builds a tool call and before the call runs. It can allow, deny, ask or defer the call, or block it by exiting 2.
How do I block a command with a Claude Code hook?
Register a PreToolUse hook with a matcher such as Bash, check tool_input.command in the script, and exit 2 with the reason on stderr. Claude sees the stderr as the reason.
What does exit code 2 mean in Claude Code hooks?
It is a blocking error. On events that can block, such as PreToolUse, UserPromptSubmit and Stop, Claude Code stops the action. On events that can't, such as PostToolUse, it only shows stderr.
Does exit code 1 block a tool call?
Not by itself. Exit 1 with plain text or no output is a non-blocking error, and the tool call goes ahead. If the hook prints JSON that passes validation, the JSON decides, so a deny still blocks. With onFailure set to block (v2.1.295 or later), exit 1 counts as a failure and blocks. The docs recommend exit 2 for policy hooks.
Why is my Claude Code hook not running?
Common causes are a settings file in the wrong place, invalid JSON, a misspelled event name, a matcher with the wrong case, or a script that isn't executable. Our checklist covers each one.