Hooks
Use Claude Code hooks for predictable automation and repository guardrails.
Written and reviewed by Eric Han · Co-founder & CTO, reopt
Hook contract changes in v2.1.299–307
UserPromptSubmitruns before skill/custom-command expansion. A block also prevents embedded shell execution. A skill's own frontmatter hook begins on the prompt after invocation.updatedInputfromPreToolUseandPermissionRequestis validated as tool input: unknown keys are dropped and rejected input blocks the call (v2.1.300).- At most 12 prompt/agent hooks of one event run concurrently (v2.1.302). One source's skill hooks are limited to 1,024 hooks and 16,384 matcher characters per session (v2.1.304).
claude_code.safety_event(v2.1.307) records calls stopped, prompted, or returned by rules, auto mode, safety checks, hooks, or the sandbox.tool_decisionaddsclassifier_verdict,permission_mode, andclassifier_categorywhenOTEL_LOG_TOOL_DETAILS=1(v2.1.301).
Key takeaways
- Hooks are user-defined shell commands that run at lifecycle events (
PreToolUse,PostToolUse,SessionStart, and more), giving deterministic control instead of relying on the model to act. - Hooks communicate through exit codes: exit 0 proceeds, exit 2 is a blocking error whose stderr is fed back to Claude, and any other code is a non-blocking error.
- For finer control, exit 0 and print JSON like
hookSpecificOutput.permissionDecision(allow/deny/ask); never mix JSON output with exit 2. - Use
matcherto filter by tool name and theiffield (v2.1.85+) to filter by tool arguments such as"Bash(git *)"or"Edit(*.ts)". - Roll out gradually — log first, then warn, then block only proven-damaging commands — and set scope by file:
~/.claude/settings.json, project.claude/settings.json, or gitignored.claude/settings.local.json.
Hooks are user-defined shell commands that run at specific points in Claude Code's lifecycle. They give you deterministic control: certain actions always happen instead of relying on the model to choose to run them. Start with observability and lightweight checks before adding blocking policy.
Useful Hook Categories
Hooks attach to lifecycle events. The events you reach for most are:
PreToolUsefor pre-action checks on risky tool calls (for example,Bashcommands that matchrm *).PostToolUsefor post-edit formatting or targeted linting, usually with anEdit|Writematcher.SessionStart(with acompactmatcher) to re-inject critical context after compaction.CwdChanged,DirectoryAdded, andFileChangedfor directory- or file-specific rules, such as reloading environment variables.DirectoryAddedfires after/add-diror SDKregister_repo_rootadds a working directory mid-session (v2.1.219).PermissionRequest/PermissionDeniedfor permission logging or auto-approval so teams can improve allowlists.
The full event list is large and still growing (it also includes UserPromptSubmit, Stop,
SubagentStart / SubagentStop, Notification, PreCompact / PostCompact, SessionEnd, and
more); see the official Hooks reference for the complete
set and each event's input schema.
Design Rules
Hooks should be fast, deterministic, and easy to bypass only through an explicit policy decision.
They should not surprise the developer with long-running work. Command, HTTP, and MCP-tool hooks
time out after 10 minutes by default (UserPromptSubmit lowers that to 30 seconds); override per
hook with the timeout field, expressed in seconds.
Good hook behavior:
- Emits a clear message.
- Names the command it ran.
- Fails with an actionable reason on stderr so Claude can adjust.
- Stays scoped to the changed area.
Bad hook behavior:
- Installs packages.
- Mutates unrelated files.
- Runs the entire CI suite on every small edit.
- Hides the reason a command was blocked.
How Hooks Communicate
Command hooks talk to Claude Code through stdin, stdout, stderr, and the exit code. When an event fires, Claude Code passes event data as JSON on stdin; your script reads it, does its work, and signals the result.
The exit code is the simplest control:
- Exit 0 — no objection; the action proceeds normally. For
PreToolUsethis does not approve the call, the normal permission flow still applies. ForUserPromptSubmitandSessionStart, anything written to stdout is added to Claude's context. - Exit 2 — blocking error. Stderr is fed back to Claude as feedback. The effect depends on the
event:
PreToolUseblocks the tool call,UserPromptSubmitrejects the prompt,Stopprevents Claude from stopping. Some events (such asSessionStartandNotification) cannot be blocked. - Any other exit code — non-blocking error; the action proceeds and the transcript shows a
<hook name> hook errornotice with the first line of stderr.
For more control, exit 0 and print a JSON object to stdout instead. A PreToolUse hook, for
example, can return hookSpecificOutput.permissionDecision of "allow", "deny", or "ask". Do
not mix the two styles: Claude Code ignores JSON output when you exit 2.
Reference scripts and absolute paths with ${CLAUDE_PROJECT_DIR} (project root) so they resolve no
matter what the working directory is.
Block the action when a hook fails
On most events, a hook that fails or times out still lets the action go ahead, so a policy hook with a
wrong path or a crashing script lets everything through. To block the action instead, set
"onFailure": "block" on a command or http hook. The default is "continue", and the field
requires Claude Code v2.1.295 or later.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "node",
"args": ["${CLAUDE_PROJECT_DIR}/.claude/hooks/check-command.js"],
"onFailure": "block"
}
]
}
]
}
}Five cases count as a failure: a command hook that can't start, an exit code other than 0 or 2, an
HTTP connection failure or non-2xx response, reaching the hook's timeout, and JSON output that can't
be parsed or fails schema validation. With "block", a failure does what exit code 2 does on that
event, and on PermissionRequest it denies the request. The field has no effect on Stop,
SubagentStop, TaskCompleted, and TeammateIdle hooks, or on command hooks that run in the
background with async or asyncRewake.
Prompt hooks (type: "prompt") send a prompt to a Claude model for single-turn evaluation instead of
running a command. From v2.1.294, prompt hooks on Stop and SubagentStop that are written as
instructions, such as "Carry on if the build is broken", are judged so that Claude is less likely to
stop early.
Example Policy
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "pnpm --filter handbook typecheck"
}
]
}
]
}
}Use this shape only when the command is fast enough for the repository. For large projects, prefer
focused scripts that inspect changed files first. The matcher filters by tool name: a value with
only letters, digits, _, and | is treated as an exact name or pipe-separated list, and any other
character makes it a regular expression. An empty or omitted matcher fires on every occurrence of
the event.
To filter by tool arguments as well as name, use the if field, which accepts permission-rule
syntax such as "Bash(git *)" or "Edit(*.ts)" and only spawns the hook when the call matches. The
if field requires Claude Code v2.1.85 or later (earlier versions ignore it and run on every
matched call) and works only on tool events (PreToolUse, PostToolUse, PostToolUseFailure,
PermissionRequest, PermissionDenied).
Rollout Pattern
- Log the event without blocking.
- Add a warning for known risky patterns.
- Block only the commands or states that have caused real damage.
- Document the hook in onboarding material.
- Review false positives after every week of use.
Where you add a hook determines its scope: ~/.claude/settings.json applies to all your projects,
.claude/settings.json is committed for a single project, and .claude/settings.local.json is the
gitignored per-project file. Plugins, skills, and agent frontmatter can also contribute hooks.
Run /hooks to browse every configured hook grouped by event. The menu is read-only: to add,
modify, or remove a hook, edit the settings JSON directly (or ask Claude to make the change). To
turn everything off, set "disableAllHooks": true in a settings file.