What You Need to Know
A hook is your own code. You connect it to a named point in the agent's lifecycle, called an event. At that point, the hook inspects what is about to happen and can allow, modify or block it. PreToolUse is the event that fires before a tool call executes. A matcher decides which tools a hook fires on.
This lesson uses one made-up example throughout: a food delivery app company whose developers run an agent on the admin system. The agent can call a tool that deletes a restaurant record. The team lead has told the agent three times never to delete a restaurant that still has open orders. The agent has deleted such a restaurant anyway.
Why the Hook Is the Control
The docs say that hooks give "deterministic control: certain actions always happen rather than relying on the LLM to choose to run them". When a model keeps ignoring an instruction, a firmer instruction still leaves the choice with the model. A hook is a check that the model has no role in. For the delivery company, the control is a hook that checks for open orders before any deletion runs.
For safety, a hook can do four things:
- block a class of action before it executes
- require approval for a high-consequence operation
- validate or redact a tool input
- produce an audit record that exists whether or not the model mentions what it did
Blocking With PreToolUse
PreToolUse is the point where a hook inspects a call before the call executes. The script reports its decision through its exit code. Exit code 2 blocks the call, and Claude receives the reason that the script writes to stderr. Exit 0 means no objection. That is not approval: the normal permission flow still applies. Any other non-zero exit, with plain-text or empty stdout, is a non-blocking error, and the action proceeds. As of October 2026, setting onFailure: "block" on the hook makes any failed run block instead. Without that setting, a hastily written script that exits 1 on failure lets the call proceed, and the restaurant deletion would run.
The script and settings block below show the pattern.
#!/usr/bin/env bash
# .claude/hooks/protect-open-orders.sh — deny a delete that names a restaurant with open orders.
input=$(cat)
target=$(jq -r '.tool_input.restaurant_id // empty' <<<"$input")
if [ -n "$target" ] && grep -qx "$target" "$CLAUDE_PROJECT_DIR/.claude/open-orders.txt"; then
echo "Restaurant $target still has open orders and cannot be deleted." >&2
exit 2 # 2 blocks. 1 would be a non-blocking error and the delete would run.
fi
exit 0{
"hooks": {
"PreToolUse": [
{
"matcher": "mcp__admin__delete_restaurant",
"hooks": [
{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-open-orders.sh" }
]
}
]
}
}A hook can answer in JSON instead. The JSON form is hookSpecificOutput. It carries a permissionDecision of allow, deny, ask or defer, and a permissionDecisionReason. As of October 2026, defer is honoured only in a non-interactive -p run. In that run, the tool does not execute. The turn ends with stop_reason: "tool_deferred", and the transcript keeps the call. The calling application resumes the session once it has an answer.
Where several hooks return different decisions, the documented precedence is deny, then defer, then ask, then allow. On blocking-capable events, exit 2 takes precedence regardless. Even a printed allow cannot override it.
Matchers
The matcher decides whether the hook runs for a given call. As of September 2026, the characters in a matcher decide how it is compared. A matcher made only of letters, digits, _, -, spaces, commas and | is compared as an exact name or list of names. Any other character makes it an unanchored JavaScript regular expression, so Edit.* also matches NotebookEdit. On MCP servers, this rule causes people to make mistakes. mcp__admin contains only exact-match characters, so it matches no tool. mcp__admin__.* matches every tool the server has, including tools that the server adds later. The delivery company's matcher should name the delete tool exactly.
Other Safety Events
PreToolUse is not the only safety event. UserPromptSubmit runs when a prompt is submitted, before Claude processes it. A decision of "block" stops the prompt before it reaches Claude. So UserPromptSubmit is the place to keep a pasted secret away from the model. It does not stop the secret from being written to disk. By default the block message repeats the prompt (set suppressOriginalPrompt in hookSpecificOutput to omit it). The text can still appear in the local transcript and prompt history. If a developer pasted a customer's card number into a prompt, the hook could keep it from Claude but not from local history.
PostToolUse cannot undo a call that has already run. It can change what Claude sees. A decision: "block" only adds a reason beside the original output. updatedToolOutput replaces the output before it reaches Claude. That is the documented split for redaction: PreToolUse for outbound tool inputs, PostToolUse for inbound tool results.
Where It Sits in the Permission Order
Two published facts make the hook the strongest available answer. First, permissions evaluate hooks, then deny rules, then ask rules, then the mode, then allow rules, then the approval callback. A blocking hook therefore stops the call before the permission rules are consulted. Second, in bypassPermissions mode, hooks still execute and can block operations. A hook deny applies in that mode too, so the delivery company's delete hook would block the deletion even in that mode.
A hook returning allow does not clear a matching deny or ask rule. Hooks can tighten restrictions. They cannot loosen them past what the permission rules allow.
What a Hook Cannot Do
A hook enforces only what you can express as a check on the observable action: a tool name and its input. A rule about intent is not a check that a hook can make. "Never delete a restaurant with open orders" is a check that a hook can make. "Be careful with restaurant data" is not.
A hook adds latency to the action that it checks. Both obvious ways to remove that latency let the action proceed without the check. An async hook runs in the background and cannot block. Its decision, permissionDecision and continue have no effect once the action it would have controlled has completed. A command hook that reaches its timeout on PreToolUse does not block either. The call continues through the normal permission flow.
Settings include disableAllHooks, so the control can be disabled. Somebody under deadline pressure will use that setting. Only managed settings keep managed hooks running when a lower-scope file disables them.
Hook configuration is executable, security-relevant configuration. So an interactive session does not run settings-file hooks until the folder is trusted. HTTP hooks are limited by a URL allowlist. The settings file therefore needs review like any other control. Trust is no protection in a -p or Agent SDK run. Those runs never show the trust dialog. Hooks committed in a repository's .claude/settings.json run there even in a folder you have never trusted.
Path-based deny rules do not apply to subprocesses that open files themselves. OS-level enforcement comes from the sandbox, not from a rule.
Not every hook type is deterministic
Five hook types exist, and only some are deterministic. command, http and mcp_tool run code you control. prompt is a single-turn model evaluation. agent is multi-turn verification with tool access. The docs describe both as "for decisions that require judgment rather than deterministic rules", and agent hooks are still experimental. If you connect either type to a destructive action, the decision returns to a model. The purpose of this sub-skill is to remove that discretion. Where the rule is a boundary, use command.
In the exam
This is the smallest sub-skill in the certification. It is worth about half a question on the real exam. It has one idea. The guide scopes it as "leveraging hooks for guardrails and safety controls to prevent destructive actions within Claude applications". Every question built from that sentence depends on the same property: deterministic control. The form of the question is always "which control stops this from happening", and the answer is the deterministic one. For the delivery company, that answer is the PreToolUse hook that exits 2 on the deletion.
Keep the domain boundaries clear. Configuring hooks as a Claude Code feature belongs to Domain 3. Hook mechanics inside the Agent SDK harness (the Agent SDK code that runs the agent) belong to Domain 1. This sub-skill covers the judgement that a deterministic pre-execution check stops a destructive action where a prompt instruction cannot.
Exam Traps
Choosing an audit hook when the question asks what prevents the action
A PostToolUse audit hook runs after the call has executed and cannot undo it. It records the removal without stopping it. Prevention needs a PreToolUse block.
Assuming any non-zero exit code blocks, so a script that exits 1 on failure counts as a working block
By default, a non-zero exit other than 2, with plain-text or empty stdout, is a non-blocking error and the action proceeds. To block, exit 2 on an event that supports blocking. Current docs (October 2026) also let onFailure 'block' make a failed run block.
Over-claiming a hook's reach: treating a hook allow as an override, or a path rule as covering every process
A hook deny applies even in bypassPermissions mode, but a hook returning allow does not clear a matching deny or ask rule. Path-based deny rules do not apply to subprocesses that open files themselves. For OS-level enforcement, use the sandbox.
Practice Scenario
An accounting software company runs an agent that tidies its archive of invoice templates. The agent can call an archive-maintenance tool that permanently removes entries. Twice it has removed templates that were still in use, and each time the team has strengthened the wording in its instructions. Which control actually stops a third removal?
Build Exercise
One Rule, One Hook, One Proof That It Fired
What you'll learn
- How to tell a rule that a hook can check from a preference
- How to register a PreToolUse hook with a matcher narrow enough to be safe
- How the exit-code contract decides whether a gate blocks or quietly fails open
- How to name what a given hook will never catch
- Pick a single rule in your project that must always hold and can be expressed as a check on a tool name and its input. Write it down as one sentence before you write any code.
Why: A rule you cannot state as a check on an observable action is not a rule that a hook can check, and it costs less to discover that in a sentence than in a bash script.
You should see: One sentence naming a tool, a condition on its input, and the outcome you want.
- Register a PreToolUse hook with a matcher narrow enough to fire only on the tool that matters, and have the script exit 2 with a clear reason on the case you want blocked and exit 0 on everything else.
Why: The matcher decides whether the hook is a guardrail or makes the agent impossible to use, and the reason on stderr is what lets Claude adjust rather than retry without knowing why the call failed.
You should see: The hook listed under the right event when you run the hooks command, and nothing firing on unrelated tools.
- Prove it. Attempt the blocked action and confirm the call never ran, then attempt a similar legitimate action and confirm the hook did not block it.
Why: Both tests matter. A hook that blocks everything gets disabled within a week. A hook that nobody has seen block a call is only assumed to work.
You should see: One blocked call with your reason shown to Claude, and one similar call that proceeds without a block.
- Deliberately break it. Make the script exit 1 instead of 2, run the blocked case again, and watch the action proceed.
Why: The exit-code rule is the fact that the exam tests in this sub-skill, and seeing a failed hook let the call proceed is what helps you remember it.
You should see: A hook error notice in the transcript and the tool call running anyway.
- Write down the three things this hook will never catch, and decide for each whether it needs a different control or is genuinely out of scope.
Why: Naming the gaps is what stops a single hook being presented inside your organisation as the safety strategy, which is the mistake that the exam builds wrong options from.
You should see: Three named gaps, each with a decision beside it.
Sources
- Automate actions with hooks — Anthropic, the deterministic-control framing, the hook types, matchers and the settings block
- Hooks reference — Anthropic, the exit-code contract, the permissionDecision JSON form including defer, matcher patterns, UserPromptSubmit, updatedToolOutput, async hooks, timeouts, workspace trust and disableAllHooks
- Configure permissions (Claude Agent SDK) — Anthropic, hooks first in the evaluation order and a hook deny binding in bypassPermissions
- Configure permissions (Claude Code) — Anthropic, the subprocess gap in path-based rules and the sandbox as OS-level enforcement
- Claude Certified Developer Foundations Exam Guide — Domain 7, Claude Hooks — Anthropic