Claude Hooks: CCDV-F study guide
CCDV-F · Security and Safety, topic weight 1.0% of the exam
Claude Hooks is a topic in Domain 7, Security and Safety (8.1% of the CCDV-F exam), and carries 1.0% on its own. It tests one decision: when an action must never happen, such as rm -rf on the wrong folder or an edit to .env, block it with a hook that runs on every matching tool call instead of asking Claude to be careful. Hooks written as callbacks in Agent SDK code are covered in CCAR-F 1.5; this page covers Claude Code hooks configured in settings files.
What the official guide covers
The Claude Certified Developer Foundations exam guide (version 1.0, effective July 2026) describes this topic as using hooks for guardrails and safety controls that prevent destructive actions in Claude applications:
| What the guide lists | What it means in practice |
|---|---|
| Hooks as guardrails | Your own script runs at a fixed point in Claude Code's loop and can stop what Claude is about to do |
| Safety controls | A hook fires on every matching event, so the rule holds every time. An instruction in CLAUDE.md is a request. |
| Preventing destructive actions | Block recursive deletes, force pushes, dropped tables and edits to protected files before the tool runs |
| Within Claude applications | Claude Code reads hooks from settings files; the Agent SDK takes them as callback functions in code |
Where hooks are configured
Hooks live in a hooks block in a settings file. The file you choose sets who gets the hook.
| Location | Applies to | Shared |
|---|---|---|
~/.claude/settings.json | All your projects | No, your machine only |
.claude/settings.json | One project | Yes, commit it to the repository |
.claude/settings.local.json | One project | No, kept out of git |
| Managed policy settings | The whole organisation | Yes, set by admins |
A plugin's hooks/hooks.json | Wherever the plugin is enabled | Yes, bundled with the plugin |
Type /hooks in Claude Code to open the hooks browser and see what is registered. Setting "disableAllHooks": true turns hooks off, but hooks in managed settings still run unless disableAllHooks is also set there. Admins can set allowManagedHooksOnly so only the organisation's hooks run.
Headless runs with claude -p load the same hooks as an interactive session, including a cloned project's hooks, with no trust prompt. Adding --bare skips hook discovery altogether, so in a bare CI run a guard runs only if you pass it with --settings.
How a PreToolUse hook decides
- Claude proposes a tool call, for example
Bashwith the commandrm -rf build/. - Claude Code finds every
PreToolUsehook whosematcherfits the tool name and runs them in parallel. - Each hook receives JSON on stdin, including
tool_name,tool_input(for Bash,tool_input.command),cwdandsession_id. - The hook answers with its exit code or with JSON on stdout.
| Hook result | What happens |
|---|---|
| Exit 0, no JSON | No objection. The normal permission flow continues. This is not an approval. |
| Exit 2 | The tool call is blocked. Text written to stderr goes to Claude as the reason. |
| Any other exit code, such as 1 | A non-blocking error, unless stdout holds a valid JSON decision. The transcript shows a hook error notice and the tool call goes ahead. |
Exit 0 with JSON permissionDecision | "deny" blocks and sends permissionDecisionReason to Claude; "ask" shows the permission prompt; "allow" skips the prompt |
When several hooks match, the most restrictive answer wins, in the order deny, defer, ask, allow. A matcher can be an exact name ("Bash"), a list ("Edit|Write") or a regular expression ("mcp__.*__write.*"). An optional if field narrows a hook further using permission rule syntax, such as "Bash(rm *)".
A destructive-command guard
Save this as .claude/hooks/block_destructive.py:
#!/usr/bin/env python3
import json
import re
import sys
data = json.load(sys.stdin)
command = data.get("tool_input", {}).get("command", "")
BLOCKED = [
(r"\brm\s+-\w*(rf|fr)\w*", "recursive force delete"),
(r"\bgit\s+push\b.*(--force|\s-f\b)", "force push"),
(r"\bdrop\s+(table|database)\b", "dropping a table or database"),
]
for pattern, label in BLOCKED:
if re.search(pattern, command, re.IGNORECASE):
print(f"Blocked: {label} is not allowed here. Ask a person to run it.", file=sys.stderr)
sys.exit(2) # 2 blocks the call; stderr becomes Claude's feedback
sys.exit(0) # no objection; normal permission rules apply
Register it in .claude/settings.json so the whole team gets it:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "python3 \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block_destructive.py"
}
]
}
]
}
}
When Claude tries rm -rf build/, the call never runs. Claude reads the Blocked: message and can suggest a safer step. To return structured output instead, exit 0 and print {"hookSpecificOutput": {"hookEventName": "PreToolUse", "permissionDecision": "deny", "permissionDecisionReason": "..."}}. Use one approach per hook.
How hooks and permission rules work together
- A hook that exits 2 stops the call before permission rules are evaluated, so the block holds even when an allow rule would let the call through.
- A hook that returns
"allow"does not bypass deny or ask rules. Deny rules, including managed ones, still apply. - Bash permission rules match the command text, so patterns that try to restrict arguments are fragile. Anthropic's permissions page suggests a
PreToolUsehook when you need your own logic on the full command. PostToolUseruns after the tool has finished. It can add feedback or change what Claude sees, but it cannot undo the action. Guardrails belong inPreToolUse.
What exit 2 does on each event
Claude Code fires many hook events. A handful do most guardrail work, and exit code 2 means something different on each:
| Event | Fires | Exit 2 |
|---|---|---|
PreToolUse | Before a tool call | Blocks the call; stderr goes to Claude |
PostToolUse | After a tool call succeeds | Cannot block, the tool already ran; stderr goes to Claude |
UserPromptSubmit | When a prompt is submitted | Blocks the prompt before Claude sees it |
Stop | When Claude is about to finish its turn | Stops Claude from finishing, so it keeps working |
SessionStart | When a session starts or resumes | No block; stderr is shown to the user |
Notification | When Claude Code sends a notification | Ignored |
Rewrite, log and check completion
Blocking is not the only move.
- Rewrite the call. A
PreToolUsehook can returnupdatedInputinsidehookSpecificOutput. It replaces the tool's arguments before the call runs, so a hook can swap a live-looking token in a Bash command for a placeholder and let the rest run. Because it replaces the arguments, include every field you are not changing. - Keep an audit trail. A
PostToolUsehook with no matcher can append every tool name, input and result to a log. It runs on every call whatever the model decides, which is the record a compliance reviewer asks for. - Refuse to stop early. A
Stophook can run the test suite and, if it fails, print{"decision": "block", "reason": "Tests fail: fix them before finishing"}. Claude continues with that reason as its next instruction. This pairs well withautomode: the classifier judges whether an action is safe, not whether the code works.
Plugin hooks run as you
Hooks from settings files and from enabled plugins merge; none replaces another, and every matching hook runs. Install a plugin for its skills and its PreToolUse and Stop hooks come with it, running with your user privileges on every matching event. Read a plugin's hooks/hooks.json before you enable it, and use allowManagedHooksOnly where only approved hooks may run.
Hook types other than command exist: http, mcp_tool, prompt and agent. For a guardrail that must give the same answer every time, a command script is the usual choice, because prompt and agent hooks ask a model to judge.
Which control to use
| Requirement | Use | Why |
|---|---|---|
| Never run recursive deletes or force pushes | PreToolUse hook on Bash that exits 2 | Your logic checks the whole command on every call |
Never read .env | Deny rule Read(./.env) in permissions | A simple path rule is enough |
| Format every file after an edit | PostToolUse hook on Edit|Write | Runs after the edit, every time |
| A person approves deploy commands | Hook returning "ask", or an ask rule | Shows the permission prompt |
| Every developer in the company gets the guard | Hook in managed settings | Users cannot override it |
| Agent written with the Agent SDK | SDK hook callback (see CCAR-F 1.5) | Same idea, configured in code |
Never commit a literal token into .mcp.json | A CLAUDE.md line for intent, plus a PreToolUse hook on Edit|Write that exits 2 on token patterns | The line explains the rule; the hook enforces it |
| Claude must not finish while tests fail | Stop hook that returns "decision": "block" | Claude keeps working until the check passes |
| Commands must run without secrets in them | PreToolUse hook returning updatedInput | The call runs with the secret replaced |
A CI job runs claude --bare -p and must keep the guard | Pass the hook in a --settings file | Bare mode reads no settings-file hooks |
Rules that decide exam answers
- Exit 2 blocks; exit 1 does not. Any exit code other than 0 or 2 is a non-blocking error and the tool call proceeds.
- Guardrails go in PreToolUse.
PostToolUsesees the result after the damage is done. - Exit 0 is not approval. It only means the hook has no objection; permission rules still decide.
- A deny from any matching hook wins. Several hooks can run on one call; the most restrictive answer applies.
- Hooks enforce; CLAUDE.md asks. If the question says "always" or "never", the answer is a hook or a deny rule.
- Scope follows the file. Organisation-wide guards go in managed settings; team guards go in
.claude/settings.json.
Where it appears in the exam
Claude Hooks is 1.0% of scored items in Domain 7, Security and Safety (8.1%), so a 53-item exam may hold one question on it or none. The same idea appears elsewhere: the guide lists "hooks for deterministic actions" under Agent Construction, and its Domain 7 sample answer relies on guardrails or hooks so injected instructions cannot trigger sensitive actions. Expect a coding agent that must not run destructive commands or touch protected files.
Two sample questions
These are original Timo practice questions. They are not official exam questions.
Build exercise
- In a test repository, add
block_destructive.pyand the settings block above. Run/hooksand confirm the hook is listed underPreToolUse. - Create a throwaway folder and ask Claude to delete it with
rm -rf. Confirm the call is blocked and that Claude reads the reason and suggests another step. - Change
sys.exit(2)tosys.exit(1)and repeat on a new throwaway folder. Note that the delete runs and a hook error appears. Change it back. - Add a second
Bashhook that appends every command to a log file and exits 0. Run a blocked command and confirm both hooks ran and the block still won.
Practise this topic
- Claude Certified Developer practice exam: free, 20 questions, no sign-up
- CCDV-F study guide: all topics
- Worked example: Claude Agent SDK hooks
- Same topic in another exam: 1.5 Agent SDK hooks
- Previous topic: Guardrails and Safe Deployment
- Next topic: Identity, Secrets and Key Management
Sources
- Claude Certified Developer Foundations Exam Guide, version 1.0, effective July 2026 (Anthropic), Domain 7 topic: Claude Hooks
- Claude Code documentation: Hooks reference
- Claude Code documentation: Automate actions with hooks
- Claude Code documentation: Configure permissions
By Amotion AI