Claude Code hooks: the events, a working example, and how to block a tool call
Claude Code hooks are your own commands (or HTTP calls, MCP tools, prompts or subagents) that run automatically at fixed points of a session: before a tool runs, after it returns, when Claude stops, and more. You configure them in a settings file with an event, a matcher and a handler. A PreToolUse hook that exits with code 2 blocks the tool call, which makes hooks the place for rules that must always hold.
What are Claude Code hooks?
The Claude Code docs define hooks as "user-defined shell commands, HTTP endpoints, MCP tool calls, LLM prompts, or subagents that execute automatically at specific points in Claude Code's lifecycle". In practice: you tell Claude Code "every time X happens, run this", and it does, without asking the model.
That last point is the whole value. A line in CLAUDE.md asks Claude to run the linter after editing; a PostToolUse hook runs it, every time.
The Claude Code hook events you will use most
| Event | When it fires | Typical use |
|---|---|---|
SessionStart |
A session begins or resumes | Load context, set up the environment |
UserPromptSubmit |
You submit a prompt, before Claude sees it | Add context, reject a prompt |
PreToolUse |
Before a tool call runs (can block it) | Guard dangerous commands and protected files |
PermissionRequest |
A tool call needs a permission decision | Auto-answer known cases |
PostToolUse |
After a tool call succeeds | Format, lint, test, log |
PostToolUseFailure |
After a tool call fails | Collect errors |
Notification |
Claude Code sends a notification | Desktop or chat alerts |
SubagentStart / SubagentStop |
A subagent starts or finishes | Track delegated work |
Stop |
Claude finishes responding | Final checks, a summary, a sound |
PreCompact / PostCompact |
Around context compaction | Archive the full transcript |
SessionEnd |
The session terminates | Clean up |
There are more (task, worktree, file change, model switch and elicitation events among them); the reference lists them all.
Where to configure Claude Code hooks
| File | Applies to |
|---|---|
~/.claude/settings.json |
All your projects |
.claude/settings.json |
One project, shared with the team through git |
.claude/settings.local.json |
One project, only you |
| Managed policy settings | The whole organization |
A plugin's hooks/hooks.json |
While the plugin is enabled |
| Skill or subagent frontmatter | While that skill or subagent is active |
Each hook has three levels: the event, a matcher that filters when it fires (for tool events, the tool name, such as Edit|Write), and one or more handlers.
A Claude Code hooks example: lint after every edit
json
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command", "command": ".claude/hooks/lint-changed-file.sh" }
]
}
]
}
}
A command hook receives the event as JSON on stdin (the tool name, its input, the session), so your script can read which file was edited and lint only that one.
Exit codes: how a hook blocks a tool call
| Exit code | What Claude Code does |
|---|---|
0 |
Success; any JSON the hook printed is applied |
2 |
Blocking error: on events that can block, the action is stopped |
| Anything else | Non-blocking error: the action goes ahead |
The trap is exit code 1. It looks like "failed, so stop", but without valid JSON it is a non-blocking error and the tool call still runs. The docs are explicit: if your hook enforces a policy, use exit code 2.
Instead of exit codes, a PreToolUse hook can print a decision:
json
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Writes to the production config are not allowed"
}
}
permissionDecision can be allow, deny, ask or defer, and the reason is shown to Claude, so it can change course instead of retrying blindly.
Hook types: command, HTTP, MCP tool, prompt and agent
commandruns a shell command with the event on stdin.httpPOSTs the event JSON to a URL.mcp_toolcalls a tool on a configured MCP server.promptsends the event to a Claude model for a single-turn judgment.agentspawns a subagent that can read files before deciding (experimental).
Hooks vs CLAUDE.md vs permissions
| Runs | Use it for | |
|---|---|---|
| CLAUDE.md | As instructions Claude reads | Conventions and preferences: usually followed |
| Hook | As code at a fixed event | Actions that must happen every time, checks that must not be skipped |
| Permission rules | Before every tool call | Hard allow and deny lists for tools and commands |
The docs add a nuance worth keeping: a hook's optional if filter is best effort, so for a hard allow or deny on a tool, use the permission system; use hooks for the logic around it.
Hooks in the Agent SDK and on the Claude Certified Architect exam
The Claude Agent SDK offers the same events as callbacks in your own code. Task statement 1.5 of the Claude Certified Architect exam guide is about exactly this: PostToolUse hooks that normalize data from different tools (timestamps, status codes) before the model sees it, and interception hooks that block policy-violating calls, such as a refund above a threshold, and redirect to a human. The principle the guide tests: when a business rule needs guaranteed compliance, choose a hook over a prompt instruction, because prompt instructions alone have a non-zero failure rate.
Related: Claude skills can carry their own hooks, and all articles list the rest.
Sources: Claude Code hooks reference; Claude Code overview; Agent SDK hooks; Claude Certified Architect Foundations exam guide v1.0, task statements 1.4 and 1.5.
FAQ
What are Claude Code hooks?
User-defined shell commands, HTTP endpoints, MCP tool calls, LLM prompts or subagents that execute automatically at specific points in Claude Code's lifecycle, such as before a tool call or when Claude finishes responding.
Where do I put Claude Code hooks?
In a settings file: ~/.claude/settings.json for all your projects, .claude/settings.json for a project you share, or .claude/settings.local.json for a project only you use. Plugins, skills and subagents can also define hooks.
How does a hook block a tool call?
A PreToolUse hook blocks the call by exiting with code 2, or by printing JSON with permissionDecision set to deny and a reason. Exit code 1 is a non-blocking error, so the action still goes ahead.
Should a business rule go in a hook or in the prompt?
If it must hold every time, in a hook: hooks run as code, while prompt instructions are followed most of the time but not with certainty.