Claude Code Hooks: What They Are and How to Use Them
Claude Code hooks are shell commands the harness runs at fixed points in the agent loop. Unlike a rule in
CLAUDE.md, a hook is not a suggestion the model can reason around - it is code that runs, and its
exit status decides whether the tool call happens. This page covers every hook event, how matchers select tool
calls, what the exit codes mean, and a working example that blocks a command.
What are Claude Code hooks?
A hook is a command Claude Code executes automatically when a specific event fires. The harness pipes a JSON payload to your command on stdin - session id, transcript path, the tool name, and the tool input - and reads your exit status to decide what happens next. Because hooks live in the harness rather than the prompt, they behave the same on turn 1 and turn 400, and they apply to subagents that never saw your top-level instructions.
Hooks are configured in settings.json. Project-scoped hooks live in .claude/settings.json
(commit these - they are how a team shares policy), .claude/settings.local.json holds machine-specific overrides, and
~/.claude/settings.json applies to every project on the machine.
Every Claude Code hook event
Only some events can stop what the agent is about to do. That column is the one that matters when you are writing policy rather than telemetry.
| Event | When it fires | Can it block? |
|---|---|---|
PreToolUse | Before a tool call runs | Yes - exit 2 blocks the call |
PostToolUse | After a tool call returns | No - the call already ran |
UserPromptSubmit | When you submit a prompt | Yes - can block the prompt |
SessionStart | When a session begins or resumes | No - used to inject context |
SessionEnd | When a session terminates | No |
Stop | When the main agent finishes responding | Yes - can force it to continue |
SubagentStop | When a subagent finishes | Yes |
PreCompact | Before context compaction runs | No |
Notification | When Claude Code sends a notification | No |
Full field-level reference lives in the official Claude Code hooks documentation; this guide focuses on the decisions you have to make when turning hooks into enforced policy.
Matchers: choosing which tool calls fire the hook
A matcher is compared against the tool name, not the command text. "Bash" fires on shell
calls, "Edit|Write" fires on either file-writing tool, and "*" (or omitting the matcher)
fires on everything. Matching the command text is your hook's job, not the matcher's - which is why the example
below parses tool_input itself.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/deny-force-push.sh"
}
]
}
]
}
}
Exit codes: how a hook actually blocks something
- Exit 0 - allow. The tool call proceeds.
- Exit 2 - block. On
PreToolUsethe call never runs, and whatever your hook wrote to stderr is fed back to the model, so the agent learns why it was stopped and can pick a different approach. - Any other non-zero - surfaces an error without blocking. Useful for a hook that is still being trialled; dangerous if you believed it was enforcing something.
The stderr message is the part teams under-use. A bare block teaches the agent nothing and it retries a variant three times; a block that explains the constraint redirects it on the first try.
#!/usr/bin/env bash
# .claude/hooks/deny-force-push.sh
# Claude Code pipes the pending tool call to stdin as JSON.
payload="$(cat)"
command="$(printf '%s' "$payload" | python3 -c 'import json,sys; print(json.load(sys.stdin)["tool_input"].get("command",""))')"
if printf '%s' "$command" | grep -Eq 'git +push +(-f|--force)( |$)'; then
# stderr is fed back to the model, so say WHY.
echo "Blocked: force-push rewrites shared history. Open a PR instead." >&2
exit 2 # exit 2 = block the tool call
fi
exit 0 # anything else runs normally
Make the script executable and test it standalone before trusting it - pipe a fake payload into it and check the exit status. A hook that silently fails open is worse than no hook, because you stop checking.
Why teams move rules out of CLAUDE.md and into hooks
Advisory rules fail in three predictable places. They decay under context compaction, when the summary that replaces your conversation drops the constraint. They never reach subagents, which start with their own context. And they are negotiable - a model under pressure to finish a task can talk itself past a sentence in a prompt, but it cannot talk itself past a non-zero exit status.
The failure that convinces most teams is a runaway turn. An agent handed an open-ended objective can loop for
hours without producing anything, exhausting a whole day of model quota while every health check stays green. No
component owns that failure: the model is "working", the harness sees a live session, and the usage graph is the
only place it shows up. A Stop or PreToolUse hook that counts turns is a ten-line fix.
Claude Code security: what hooks do and do not give you
Hooks are a control point, not a sandbox. They run with your user privileges, so they can deterministically refuse a destructive command, require an approval, or cap runaway turns - but they cannot contain a process once it has started. A realistic Claude Code security posture layers them: hooks for policy, OS-level isolation for blast radius, and an audit trail so you can answer what the agent actually did.
Two rules worth adopting early. Keep the hook itself trivial and fast - it runs on the critical path of every matching tool call. And make the hook's own configuration something the agent cannot rewrite, or you have built a lock whose key is in the same drawer.
Where ThumbGate fits
ThumbGate is a set of Claude Code hooks you do not have to write and maintain yourself. It installs
PreToolUse enforcement with a deny-list for destructive commands, runaway-turn limits, and
prevention rules generated from mistakes the agent already made - so a correction you give once becomes a gate
that holds in every later session, including in subagents.
Start with the free CLI and keep your own hooks; ThumbGate composes with them rather than replacing them. If you want the enforcement designed around your actual incidents instead of a generic deny-list, the $499 Managed AI Agent Workflow Gate installs one hard, test-backed gate for the failure that is hurting you most - see what it covers.
Frequently asked questions
What are hooks in Claude Code?
Claude Code hooks are shell commands the harness runs automatically at fixed points in the agent loop - before a tool call, after a tool call, when you submit a prompt, when the session starts, and when the agent stops. They are configured in settings.json, they run outside the model, and a PreToolUse hook can block a tool call before it executes.
What is the difference between a hook and a rule in CLAUDE.md?
A CLAUDE.md rule is advisory: it goes into the prompt and the model decides whether to follow it. A hook is deterministic: it is code the harness executes, and its exit status decides whether the tool call runs. Under long sessions or context compaction, advisory rules degrade; hooks do not.
How do I use Claude Code hooks?
Add a hooks block to .claude/settings.json in your project (or ~/.claude/settings.json for every project), pick an event such as PreToolUse, give it a matcher such as Bash, and point it at a command. Claude Code pipes a JSON payload describing the pending tool call to your command on stdin.
How do I set up hooks for Claude Code so they block a command?
Use the PreToolUse event and exit with status 2 from your hook. Exit code 2 blocks the tool call and returns your stderr text to the model, so the agent sees why it was stopped. Exit code 0 allows the call; any other non-zero code surfaces an error without blocking.
What is a Claude Code hook matcher?
The matcher selects which tool calls the hook fires on. It is matched against the tool name - for example Bash, Edit, Write, or a pattern like Edit|Write. Omit it, or use *, to run the hook on every tool call.
Do Claude Code hooks work with subagents and background sessions?
Yes. Hooks are harness-level, so they apply to tool calls made by subagents too, and SubagentStop fires when a subagent finishes. This is the main reason to enforce policy in hooks rather than in a prompt - a subagent never reads your top-level instructions.
Are Claude Code hooks a security boundary?
They are a control point, not a sandbox. Hooks run with your user privileges and can deterministically block risky tool calls, but they do not contain a process once it has started. Treat them as policy enforcement - deny-listing destructive commands, requiring approval, capping runaway turns - layered with OS-level isolation.