What you will be able to do
- Name the hook events that fire around each tool call, each turn and each session
- Tell which hook events can stop an action and which only observe it after the fact
- Write matchers that use exact names or regular expressions to target specific tools
- Choose a handler type and a settings location for a guardrail hook
Key concept
Hook as a deterministic control point — A hook is a handler that you register against a lifecycle event. Claude Code runs it every time that event fires, whatever the model intended. A hook on an event that fires before an action takes effect can refuse that action, so a written policy becomes a check that always runs.
1.Hooks as the layer that runs on every tool call
A prompt instruction such as "never run rm -rf" is something Claude weighs. A hook is something Claude Code executes. You attach a handler to a lifecycle event, and the handler runs each time that event fires, whatever the model reasoned its way to. Of all the defences described in [Guardrail layering](lesson:7.2), this one does not depend on the model following instructions: it runs on every tool call, including calls the model was talked into by injected content.
Hook events fire at three different rates. SessionStart and SessionEnd fire once per session. UserPromptSubmit, Stop and StopFailure fire once per turn. PreToolUse and PostToolUse fire around every tool call inside the agentic loop, with one exception: EndConversation calls skip both. Guardrails care most about the tool-call pair, because tool calls are where an agent changes files, runs shell commands and calls external services.
The same mechanism covers more than blocking. One hook can log every Bash command to an audit file. Another can make a person approve database writes. A third can rewrite a tool's input before it runs. Whichever you build, the handler is your own code, sitting at a fixed point in the loop.
Checkpoint 1 of 7· Check yourself
A team registers a PreToolUse hook with matcher * so that it audits every tool call. Which call will the hook never see?
PreToolUse and PostToolUse fire on every tool call in the agentic loop, whatever the model decided. EndConversation calls are the one documented exception and skip both events.
“on every tool call inside the agentic loop: PreToolUse and PostToolUse, except EndConversation calls, which skip both”Source: code.claude.com
2.Which hook events can stop an action
Whether a hook can block comes down to timing. A hook can stop an action only if its event fires before that action takes effect. PreToolUse fires before the tool executes, so it can cancel the call. UserPromptSubmit fires before Claude processes the prompt, so blocking it means the prompt never reaches Claude. UserPromptExpansion can stop a typed command from expanding into a prompt, and PreModelSwitch can veto a requested model switch. PostToolUse and PostToolUseFailure fire after the tool has already succeeded or failed. Use them to audit, lint or report back to Claude. They are too late to prevent anything.
| Hook event | Can block? | What happens on exit 2 |
|---|---|---|
| PreToolUse | Yes | Blocks the tool call |
| UserPromptSubmit | Yes | Blocks the prompt, so it never reaches Claude |
| UserPromptExpansion | Yes | Blocks the expansion |
| Stop | Yes | Prevents Claude from stopping, continues the conversation |
| ConfigChange | Yes | Blocks the configuration change from taking effect (except policy_settings) |
| PostToolUse | No | Shows stderr to Claude; the tool already ran |
| PermissionRequest | No | Exit code 2 isn’t honored; deny through the decision object instead |
"Block" does not always mean "refuse a dangerous action". On Stop and SubagentStop, blocking keeps Claude working. You would use that to stop a turn ending before the tests pass, not to protect the system. PermissionRequest catches people out the other way round. It fires exactly when a tool call needs approval, yet exit code 2 does nothing there, so a hook that wants to refuse must answer through the decision object in its JSON output.
Checkpoint 2 of 7· Check yourself
An auditor lists hook events that can prevent the associated action. Which entry is wrong?
PostToolUse fires after the tool succeeded. Exit 2 only shows stderr to Claude, and the edit has already been made.
“Shows stderr to Claude; the tool already ran”Source: code.claude.com
Checkpoint 3 of 7· Exam question
A platform team wants to prevent Claude Code from ever running `rm -rf` against production paths, regardless of what the model decides to do. Which hook event should they use to guarantee the command never executes?
Correct answer: A — PreToolUse, because it fires before the tool call executes and can deny it
- A. Correct. PreToolUse fires before the tool call runs and supports permissionDecision: deny, which stops the command from ever executing.
- B. Incorrect. PostToolUse fires after the tool has already succeeded, so by the time it runs the destructive command has already executed.
- C. Incorrect. Stop fires once per turn when Claude finishes responding, long after any tool calls in that turn have already run.
- D. Incorrect. Notification only reports status events like permission prompts or idle states; it has no ability to block tool execution.
Sources1
3.Aiming a hook with matchers and if conditions
Hook configuration has three levels. You choose an event, add a matcher group that filters when it fires, and list one or more handlers to run when it matches. For tool events (PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest, PermissionDenied), the matcher is compared with the tool name. Other events filter on other fields: SessionStart on how the session started, PreCompact on whether compaction was manual or auto. UserPromptSubmit, Stop, PostToolBatch and a few others have no matcher support and fire on every occurrence.
How a matcher string is read depends on the characters in it. *, an empty string or no matcher at all matches everything. A string made only of letters, digits, _, -, spaces, commas and | is an exact name or a list of exact names, so Edit|Write matches those two tools and nothing else. Any other character turns the whole string into an unanchored JavaScript regular expression. ^Notebook therefore matches every tool whose name starts with Notebook, and mcp__memory__.* matches every tool on the memory MCP server. MCP tools are named mcp__<server>__<action>, which is why a regex is the usual way to cover a whole server.
A matcher can only see the tool name. To narrow a hook further, add an if condition on the handler, written in permission-rule syntax such as Bash(rm *). The handler then starts only for calls whose arguments fit. A script that runs on every Bash call has to parse and dismiss most of them. With if, it runs only for the commands it exists to judge.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"if": "Bash(rm *)",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-rm.sh",
"args": []
}
]
}
]
}
}Checkpoint 4 of 7· Put it in order
Put the stages of resolving this hook for a rm -rf /tmp/build call in order
- 1.Claude Code acts on the result the handler returned
- 2.The matcher checks the tool name against Bash
- 3.The hook handler runs
- 4.The if condition checks the call against Bash(rm *)
- 5.The PreToolUse event fires with the tool name and input
The event comes first, then the matcher group filters it by tool name, then the if condition filters it by arguments, and only after that does the handler run and return a result.
“Add a matcher group to filter when it fires”Source: code.claude.com
Checkpoint 5 of 7· Match them up
Match each matcher value to what it fires on
Tap a term, then the definition that fits it.
Letters, digits, _, -, spaces, commas and | give an exact match or a list of exact names. Any other character, such as ^ or ., makes the string an unanchored regular expression.
“JavaScript regular expression, unanchored”Source: code.claude.com
Sources1
4.Handler types and where hook configuration lives
The handler's type field decides what kind of code makes the decision. A guardrail that pattern-matches a command string fits naturally in a command handler, which is a script that reads the event JSON and replies through its exit code or stdout. The other types send the event to a service you run, to an MCP tool, or to a model.
| type | What the handler does |
|---|---|
| command | Runs a local command or script that receives the event data |
| http | POSTs the event data to a URL |
| mcp_tool | Calls a tool on a configured MCP server |
| prompt | Single-turn LLM evaluation |
| agent | Multi-turn verification with tool access; experimental and may change |
For a hard rule, prefer a command handler. It returns the same decision for the same input. Prompt and agent handlers put a model's judgment inside the check, which helps when a policy is too fuzzy for a regex. HTTP handlers can be restricted by an administrator: once allowedHttpHookUrls is set at any settings level, only hook URLs on the merged allowlist run.
Where you write the hook decides who it protects. A guardrail in ~/.claude/settings.json covers all of your own projects but no one else. Put it in .claude/settings.json and it is committed with the repository, so everyone who clones the repo gets it. .claude/settings.local.json is gitignored and stays personal. Managed policy settings apply across the organization and are controlled by administrators. Hooks can also come with a plugin or be declared in a skill's or subagent's frontmatter, scoped to when that component is active.
| Location | Scope | Shareable |
|---|---|---|
| ~/.claude/settings.json | All your projects | No, local to your machine |
| .claude/settings.json | Single project | Yes, can be committed to the repo |
| .claude/settings.local.json | Single project | No, gitignored when Claude Code saves a setting to it |
| Managed policy settings | Organization-wide | Yes, admin-controlled |
| Plugin hooks/hooks.json | When plugin is enabled | Yes, bundled with the plugin |
| Subagent frontmatter | While that subagent is running | Yes, defined in the subagent file |
Checkpoint 6 of 7· Check yourself
A tech lead wants a hook that blocks rm -rf to protect every developer who clones one repository, with no per-machine setup. Where should it go?
.claude/settings.json is scoped to the project and can be committed. The user file and the local file stay on one machine, and subagent frontmatter only applies while that subagent runs.
“Yes, can be committed to the repo”Source: code.claude.com
Checkpoint 7 of 7· Exam question
A security engineer writes a command-hook script for PreToolUse that inspects `tool_input.command` and, upon detecting a dangerous pattern, prints an explanation to stderr and exits without emitting any JSON. Which exit code causes Claude Code to block the tool call using that stderr message?
Correct answer: A — Exit code 2, which is treated as a blocking error and feeds stderr back to Claude
- A. Correct. Exit code 2 signals a blocking error: stdout is ignored, stderr is surfaced to Claude, and the pending tool call is blocked.
- B. Incorrect. Exit code 0 means success; Claude Code parses stdout as JSON for a decision instead of treating the call as blocked.
- C. Incorrect. Exit code 1 has no special reserved meaning for JSON parsing; it is treated as a non-blocking error like other non-2 nonzero codes.
- D. Incorrect. Only exit code 2 is defined as the blocking-error signal; other nonzero codes are treated as non-blocking errors that let execution continue.
Exam traps
Each one states something that sounds right. Open it to see what is actually true.
1.A PostToolUse hook that exits 2 can stop a destructive tool call.Why is that wrong?
PostToolUse fires after the tool has already succeeded. Exit 2 only sends stderr back to Claude. Prevention has to happen in PreToolUse.
Covered in Which hook events can stop an action
2.Exiting 2 from a PermissionRequest hook denies the permission request.Why is that wrong?
PermissionRequest ignores exit code 2. To refuse, the hook has to return a decision in its JSON output.
Covered in Which hook events can stop an action
3.
Edit|Writeis a regular expression, so it also matches tools whose names merely contain Edit or Write.Why is that wrong?A matcher made only of letters, digits,
_,-, spaces, commas and|is a list of exact names. Only other characters switch it to a regular expression.
Sources
Every claim above is drawn from one of these pages, quoted as it was written on the date shown.
- 1.https://code.claude.com/docs/en/hooksOfficial docs
“on every tool call inside the agentic loop: PreToolUse and PostToolUse, except EndConversation calls, which skip both”
↩︎ Hooks as the layer that runs on every tool call“per turn: UserPromptSubmit, Stop, and StopFailure”
↩︎ Hooks as the layer that runs on every tool call“Blocks the prompt, so it never reaches Claude.”
↩︎ Which hook events can stop an action“Prevents Claude from stopping, continues the conversation”
↩︎ Which hook events can stop an action“Before Claude Code applies a model switch that you or a client requested. Can block the switch”
↩︎ Which hook events can stop an action“mcp__memory__.* matches every tool from the memory server”
↩︎ Aiming a hook with matchers and if conditions“always fires on every occurrence”
↩︎ Aiming a hook with matchers and if conditions“when defined at any settings level, Claude Code runs an HTTP hook handler only if its URL matches the merged allowlist”
↩︎ Handler types and where hook configuration lives“Shows stderr to Claude; the tool already ran”
↩︎ Exam trap 1“Exit code 2 isn’t honored for this event and the permission flow proceeds unchanged. Deny through the decision object instead”
↩︎ Exam trap 2“Edit|Write and Edit, Write each match either tool exactly”
↩︎ Exam trap 3“Shows stderr to Claude; the tool already ran”
↩︎ Prediction“Add a matcher group to filter when it fires”
↩︎ Checkpoint“JavaScript regular expression, unanchored”
↩︎ Checkpoint“Yes, can be committed to the repo”
↩︎ Checkpoint - 2.https://code.claude.com/docs/en/agent-sdk/hooksOfficial docs
“Require human approval for sensitive actions like database writes or API calls”
↩︎ Hooks as the layer that runs on every tool call“Block dangerous operations before they execute, like destructive shell commands or unauthorized file access”
↩︎ Key concept - 3.https://code.claude.com/docs/en/hooks-guideOfficial docs
“multi-turn verification with tool access. Agent hooks are experimental and may change.”
↩︎ Handler types and where hook configuration lives