Hooks
Use Claude Code hooks for predictable automation and repository guardrails.
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.CwdChangedandFileChangedfor directory- or file-specific rules, such as reloading environment variables.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.
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.