TimoBy Amotion AI

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 listsWhat it means in practice
Hooks as guardrailsYour own script runs at a fixed point in Claude Code's loop and can stop what Claude is about to do
Safety controlsA hook fires on every matching event, so the rule holds every time. An instruction in CLAUDE.md is a request.
Preventing destructive actionsBlock recursive deletes, force pushes, dropped tables and edits to protected files before the tool runs
Within Claude applicationsClaude 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.

LocationApplies toShared
~/.claude/settings.jsonAll your projectsNo, your machine only
.claude/settings.jsonOne projectYes, commit it to the repository
.claude/settings.local.jsonOne projectNo, kept out of git
Managed policy settingsThe whole organisationYes, set by admins
A plugin's hooks/hooks.jsonWherever the plugin is enabledYes, 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

  1. Claude proposes a tool call, for example Bash with the command rm -rf build/.
  2. Claude Code finds every PreToolUse hook whose matcher fits the tool name and runs them in parallel.
  3. Each hook receives JSON on stdin, including tool_name, tool_input (for Bash, tool_input.command), cwd and session_id.
  4. The hook answers with its exit code or with JSON on stdout.
Hook resultWhat happens
Exit 0, no JSONNo objection. The normal permission flow continues. This is not an approval.
Exit 2The tool call is blocked. Text written to stderr goes to Claude as the reason.
Any other exit code, such as 1A 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 PreToolUse hook when you need your own logic on the full command.
  • PostToolUse runs after the tool has finished. It can add feedback or change what Claude sees, but it cannot undo the action. Guardrails belong in PreToolUse.

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:

EventFiresExit 2
PreToolUseBefore a tool callBlocks the call; stderr goes to Claude
PostToolUseAfter a tool call succeedsCannot block, the tool already ran; stderr goes to Claude
UserPromptSubmitWhen a prompt is submittedBlocks the prompt before Claude sees it
StopWhen Claude is about to finish its turnStops Claude from finishing, so it keeps working
SessionStartWhen a session starts or resumesNo block; stderr is shown to the user
NotificationWhen Claude Code sends a notificationIgnored

Rewrite, log and check completion

Blocking is not the only move.

  • Rewrite the call. A PreToolUse hook can return updatedInput inside hookSpecificOutput. 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 PostToolUse hook 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 Stop hook 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 with auto mode: 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

RequirementUseWhy
Never run recursive deletes or force pushesPreToolUse hook on Bash that exits 2Your logic checks the whole command on every call
Never read .envDeny rule Read(./.env) in permissionsA simple path rule is enough
Format every file after an editPostToolUse hook on Edit|WriteRuns after the edit, every time
A person approves deploy commandsHook returning "ask", or an ask ruleShows the permission prompt
Every developer in the company gets the guardHook in managed settingsUsers cannot override it
Agent written with the Agent SDKSDK hook callback (see CCAR-F 1.5)Same idea, configured in code
Never commit a literal token into .mcp.jsonA CLAUDE.md line for intent, plus a PreToolUse hook on Edit|Write that exits 2 on token patternsThe line explains the rule; the hook enforces it
Claude must not finish while tests failStop hook that returns "decision": "block"Claude keeps working until the check passes
Commands must run without secrets in themPreToolUse hook returning updatedInputThe call runs with the secret replaced
A CI job runs claude --bare -p and must keep the guardPass the hook in a --settings fileBare 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. PostToolUse sees 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.

Question 1

A developer writes a PreToolUse hook on Bash to stop git push --force. When the script detects a force push, it prints a warning and calls exit 1. In testing, the force push still runs and the transcript shows a hook error notice. Why?

Answer: D. Only exit 2 or a JSON "deny" blocks a PreToolUse call; other non-zero codes let the call proceed. A is wrong because PreToolUse receives tool_input.command, and PostToolUse runs too late. B is wrong because "Bash" matches the Bash tool. C invents a restriction that does not exist.

Question 2

A platform team wants every developer's Claude Code to refuse edits to .env files and to infra/prod/. Developers must not be able to switch this off in their own settings. Where should the guard go?

Answer: B. Managed settings cannot be overridden by users, and managed hooks keep running even if a user sets disableAllHooks. A is guidance, not enforcement. C acts after the edit and sits in a file the user controls. D is a personal, uncommitted file each developer can change.

Build exercise

  1. In a test repository, add block_destructive.py and the settings block above. Run /hooks and confirm the hook is listed under PreToolUse.
  2. 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.
  3. Change sys.exit(2) to sys.exit(1) and repeat on a new throwaway folder. Note that the delete runs and a hook error appears. Change it back.
  4. Add a second Bash hook 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

Sources