What You Need to Know
A hook is your own code, wired to a named point in the agent's lifecycle, called an event. At that point it 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.
Take a hair salon that runs an agent on its booking system. The agent can call a tool that deletes a client record. The owner has told the agent three times never to delete a client who still has an appointment booked, and it has done it anyway.
Why the Hook Is the Control
The documented framing is 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 one still leaves the choice with the model. A hook is a check the model does not take part in. For the salon, that is a hook that checks the diary before any deletion runs.
On the safety path, 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 pre-execution inspection point. The script reports its decision through its exit code. Exit code 2 blocks, and the reason on stderr reaches Claude. 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 it, a hastily written script that exits 1 on failure lets the call through, and the salon's deletion would run.
The script and settings block below show the pattern.
#!/usr/bin/env bash
# .claude/hooks/protect-in-use.sh — deny a purge that names a block still in use.
input=$(cat)
target=$(jq -r '.tool_input.block_id // empty' <<<"$input")
if [ -n "$target" ] && grep -qx "$target" "$CLAUDE_PROJECT_DIR/.claude/in-use.txt"; then
echo "Block $target is still in use and cannot be removed." >&2
exit 2 # 2 blocks. 1 would be a non-blocking error and the purge would run.
fi
exit 0{
"hooks": {
"PreToolUse": [
{
"matcher": "mcp__archive__purge_block",
"hooks": [
{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-in-use.sh" }
]
}
]
}
}A hook can answer in JSON instead. The JSON form is hookSpecificOutput carrying a permissionDecision of allow, deny, ask or defer, with a permissionDecisionReason. As of September 2026, defer is honoured only in a non-interactive -p run, where the tool does not execute. The process exits with stop_reason: "tool_deferred" and the call preserved. 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 wins regardless. Even a printed allow cannot override it.
Matchers
The matcher decides whether the gate exists for a given call. As of September 2026, 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 trips people up. mcp__archive contains only exact-match characters, so it matches no tool. mcp__archive__.* matches every tool the server has, including ones added later. The salon'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. That makes it the place to keep a pasted secret away from the model. It does not keep the secret off disk. By default the block message repeats the prompt (set suppressOriginalPrompt in hookSpecificOutput to leave it out). The text can still land in the local transcript and prompt history. If a receptionist pasted a card number into the salon's chat, 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 permission rules are consulted. Second, in bypassPermissions mode, hooks still execute and can block operations. A hook deny binds there too, so the salon's delete hook would hold 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 hook-shaped. "Never delete a client with a future booking" is hook-shaped. "Be careful with client data" is not.
A hook adds latency to the path it guards, and both obvious ways to remove that latency open the gate. 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 carry disableAllHooks, so the control has an off switch. Somebody under deadline pressure will find it. Only managed settings keep managed hooks running when a lower-scope file turns them off.
Hook configuration is executable, security-relevant configuration. So an interactive session holds back settings-file hooks until the folder is trusted, and HTTP hooks sit behind a URL allowlist. The settings file therefore belongs under review like any other control. Trust is no protection in a -p or Agent SDK run, which never shows 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 reach subprocesses that open files themselves. OS-level enforcement means the sandbox, not a rule.
Not every hook type is deterministic
Five 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. Both are documented as "for decisions that require judgment rather than deterministic rules", and agent hooks are still experimental. Wiring either of them to a destructive path hands the decision back to a model. Removing that discretion is the point of this sub-skill. Where the rule is a boundary, use command.
In the exam
This is the smallest sub-skill in the certification, worth about half a live item. It has one idea. The guide scopes it as "leveraging hooks for guardrails and safety controls to prevent destructive actions within Claude applications". Every item built from that sentence turns on the same property: deterministic control. The item shape is always "which control stops this from happening", and the answer is the deterministic one. For the salon, that is the PreToolUse hook that exits 2 on the deletion.
Keep the domain boundary clean. Configuring hooks as a Claude Code feature belongs to Domain 3. Hook mechanics inside the Agent SDK harness belong to Domain 1. What lives here is the judgement that a deterministic pre-execution check stops a destructive action where a prompt instruction cannot.
Exam Traps
Choosing the audit hook when the item asks what prevents the action, because a PostToolUse hook that logs every removal reads as responsible engineering.
It is a real control for accountability and a fully-stated, plausible distractor, but it runs after the destructive call has already executed. Prevention is a PreToolUse block, and a permanent deletion is not something a later record undoes.
Assuming any non-zero exit code blocks, so a script that exits 1 on failure is treated as a working gate.
Exit 2 blocks on the events that support blocking. Exit 0 is no objection, and any other non-zero with plain-text or empty stdout is a non-blocking error that lets the action proceed, unless the hook sets onFailure to block. A gate that exits 1 has quietly disabled itself and will keep reporting a hook error while the tool call runs.
Over-claiming the reach of a hook in either direction: treating a hook allow as an override, or assuming a path rule constrains everything that touches the path.
A hook deny does bind even in bypassPermissions mode, but a hook returning allow does not clear a matching deny or ask rule, and path-based Read and Edit rules do not apply to a subprocess that opens files itself. For enforcement across every process, the answer is the sandbox rather than a rule.
Practice Scenario
A stencil cutter's records team runs an agent that tidies the cutting-pattern archive and can call an archive-maintenance tool that permanently removes entries. Twice it has removed patterns that were still in use, and each time the team has strengthened the wording in its instructions. Which control actually stops the third time?
Build Exercise
One Rule, One Hook, One Proof That It Fired
What you'll learn
- How to tell a hook-shaped rule 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 hook-shaped, and finding that out in prose is cheaper than finding it out in bash.
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 is the difference between a guardrail and an agent nobody can use, and the reason on stderr is what lets Claude adjust rather than retry blindly.
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 the neighbouring legitimate action and confirm the hook stayed out of the way.
Why: Both halves matter: a gate that blocks everything gets disabled within a week, and a gate nobody has watched fire is an assumption.
You should see: One blocked call with your reason surfaced to Claude, and one adjacent call that proceeds untouched.
- 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 contract is the keyed fact in this sub-skill, and seeing a gate fail open is what makes it stick.
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 sold internally as the safety strategy, which is the anti-pattern the exam builds distractors 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