What you will be able to do
- Explain why a hook enforces a safety rule where a prompt instruction only requests it
- Tell apart the hook events that can prevent an action from the ones that only observe it
- Trace how an event passes through matcher, if condition and handler, and name the five handler types
- Choose the settings location for a guardrail that must be shared or enforced organization-wide
Key concept
Hook as a deterministic guardrail — A hook is your own code that Claude Code runs at a fixed point in the agent lifecycle. Because it fires every time its event matches, a PreToolUse hook can refuse a destructive tool call no matter what the model decided to do.
1.Why a guardrail belongs in a hook, not in the prompt
Suppose an agent can run shell commands and edit files, and you never want it to run rm -rf or overwrite a .env file. You could write that rule into the system prompt or CLAUDE.md. Claude would usually follow it. But an instruction in context is something the model weighs against everything else it is reading, and 'usually' is not a safety control.
A hook moves the rule out of the model and into code that the harness runs. Anthropic's tips describe hooks as a way to deterministically run logic at points in the agent lifecycle. The Agent SDK documentation puts the safety use first in its list of what hooks are for: they can block dangerous operations before they run, like destructive shell commands or unauthorized file access. The same list also covers auditing every tool call, sanitizing inputs and outputs, and requiring human approval for sensitive actions like database writes.
The split to remember: the prompt shapes what Claude *tries* to do, and the hook decides what is actually *allowed* to happen. A hook doesn't need the model to agree with it.
2.Which events can stop an action, and which only see it
Claude Code emits many hook events, but not all of them can serve as a guardrail. What matters is when an event fires compared with the action it describes. PreToolUse and PostToolUse both fire on every tool call in the agentic loop (EndConversation calls skip both). Only PreToolUse fires *before* execution, and the hooks reference says plainly that it can block the call. PostToolUse fires after a tool call succeeds. By then the file is already written or the command has already run, so a PostToolUse hook suits formatting, logging or audit work, not prevention.
A handful of other events are also documented with a blocking or modifying role. The table below keeps to that wording.
| Event | When it fires | Guardrail role |
|---|---|---|
| PreToolUse | Before a tool call executes | Can block it |
| UserPromptExpansion | When a user-typed command expands into a prompt, before it reaches Claude | Can block the expansion |
| PreModelSwitch | Before Claude Code applies a requested model switch | Can block the switch |
| ElicitationResult | After a user responds to an MCP elicitation, before the response is sent back to the server | Modify or block the response |
| PermissionRequest | When a tool call needs a permission decision | Custom permission handling |
| PostToolUse | After a tool call succeeds | Observe only: audit, format, log |
| PostToolUseFailure | After a tool call fails | Observe only: handle or log errors |
| PostToolBatch | After a full batch of parallel tool calls resolves | Observe only: runs before the next model call |
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.
3.How a hook resolves: event, matcher, if condition, handler
Configuring a hook takes three choices: an event to respond to (such as PreToolUse), a matcher group that filters when it fires (such as 'only for the Bash tool'), and one or more handlers to run on a match. At runtime Claude Code works through the same chain in order. The event fires with a JSON payload such as { "tool_name": "Bash", "tool_input": { "command": "rm -rf /tmp/build" } }. The matcher checks the tool name. An optional if condition narrows the match further using permission-rule syntax. The handler runs, and Claude Code acts on what it returns.
In the reference example below, the matcher limits the hook to Bash, and "if": "Bash(rm *)" means the script only starts for rm commands, not for every shell call.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"if": "Bash(rm *)",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-rm.sh",
"args": []
}
]
}
]
}
}The type field picks the kind of handler. There are five documented types, and they differ in where the guardrail logic actually runs.
| type | What the handler does |
|---|---|
| command | Executes shell commands or scripts |
| http | Sends the event JSON as a POST request to a URL |
| mcp_tool | Calls a tool on a configured MCP server |
| prompt | Evaluates a prompt with an LLM, using the $ARGUMENTS placeholder for context |
| agent | Runs an agentic verifier with tools for complex verification tasks |
prompt, which evaluates a prompt with an LLM, and agent, which runs an agentic verifier with tools. They are useful for checks that need judgement. For a rule that must hold every time, such as never running rm -rf, a command handler with an explicit pattern check keeps the decision fully deterministic.
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.
4.Where a guardrail is configured decides who it protects
A hook only protects the sessions that load it, so where you put it is part of the design. The hooks reference lists these locations:
| 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 |
A guardrail the whole team relies on belongs in .claude/settings.json, committed to the repo. If the organization has to enforce it, managed policy settings are the place. Managed policy can also lock the setup down: the reference describes a managed configuration in which user, project, local and plugin hooks are all blocked, apart from plugins that managed settings force-enable. HTTP handlers can be restricted as well. When allowedHttpHookUrls is defined, Claude Code runs an HTTP hook only if its URL matches the merged allowlist.
Sources3
Exam traps
Each one states something that sounds right. Open it to see what is actually true.
1.A PostToolUse hook that detects
rm -rfin the command can stop the deletion.Why is that wrong?PostToolUse fires only after the tool call has succeeded, so the deletion has already happened. Prevention needs PreToolUse, which fires before the call executes and can block it.
Covered in Which events can stop an action, and which only see it
2.Putting a guardrail hook in
.claude/settings.local.jsonshares it with everyone who works on the project.Why is that wrong?The local settings file is project-scoped but not shareable. It is gitignored when Claude Code saves a setting to it. For a team-wide hook, commit it in
.claude/settings.json, or use managed policy settings to enforce it across the organization.Covered in Where a guardrail is configured decides who it protects
Sources
Every claim above is drawn from one of these pages, quoted as it was written on the date shown.
- 1.
“Hooks let you deterministically run logic at points in the agent lifecycle.”
↩︎ Why a guardrail belongs in a hook, not in the prompt“Hooks let you deterministically run logic at points in the agent lifecycle.”
↩︎ Key concept - 2.https://code.claude.com/docs/en/agent-sdk/hooksOfficial docs
“Block dangerous operations before they execute, like destructive shell commands or unauthorized file access”
↩︎ Why a guardrail belongs in a hook, not in the prompt“Require human approval for sensitive actions like database writes or API calls”
↩︎ Why a guardrail belongs in a hook, not in the prompt“Modify or block the response before it returns to the server”
↩︎ Which events can stop an action, and which only see it - 3.https://code.claude.com/docs/en/hooksOfficial docs
“PreToolUse | Before a tool call executes. Can block it”
↩︎ Which events can stop an action, and which only see it“on every tool call inside the agentic loop: PreToolUse and PostToolUse, except EndConversation calls, which skip both”
↩︎ Which events can stop an action, and which only see it“Define one or more hook handlers to run when matched”
↩︎ How a hook resolves: event, matcher, if condition, handler“Your user, project, local, and plugin hooks are blocked.”
↩︎ Where a guardrail is configured decides who it protects“Claude Code runs an HTTP hook handler only if its URL matches the merged allowlist”
↩︎ Where a guardrail is configured decides who it protects“PostToolUse | After a tool call succeeds”
↩︎ Exam trap 1“No, gitignored when Claude Code saves a setting to it”
↩︎ Exam trap 2 - 4.https://code.claude.com/docs/en/plugins-referenceOfficial docs
“agent: run an agentic verifier with tools for complex verification tasks”
↩︎ How a hook resolves: event, matcher, if condition, handler“prompt: evaluate a prompt with an LLM (uses $ARGUMENTS placeholder for context)”
↩︎ How a hook resolves: event, matcher, if condition, handler