CertSafari
    CLAUDE-CERTIFIED-DEVELOPER-FOUNDATIONS-CCDV-F · Lessons

    Domain 7 · Lesson 21/25

    Blocking Destructive Actions with PreToolUse Hooks

    Claude Hooks

    8 min read
    2.03% of exam
    5 sources
    Published 29 Sep 2026
    Docs as of 27 Sep 2026

    What you will be able to do

    • Write a PreToolUse command hook that denies a destructive shell command with a reason
    • Register the same guardrail as a programmatic callback in the Claude Agent SDK
    • Place hooks correctly in the permission evaluation order, including under bypassPermissions
    • Distinguish a Claude Code hook from Enterprise inference hooks

    1.Returning a deny decision from a command hook

    A command hook gets the event as JSON and decides by what it prints. The script below is the handler from the Claude Code hooks reference, registered on PreToolUse with matcher Bash and if condition Bash(rm *). It reads tool_input.command with jq. When the command contains rm -rf, it prints a hookSpecificOutput object whose permissionDecision is "deny" and whose permissionDecisionReason gives the reason. Claude Code acts on that result, and the command never runs.

    block-rm.sh: a PreToolUse handler that denies rm -rf, from the Claude Code hooks referencebash
    #!/bin/bash
    # .claude/hooks/block-rm.sh
    COMMAND=$(jq -r '.tool_input.command')
    
    if echo "$COMMAND" | grep -q 'rm -rf'; then
      jq -n '{
        hookSpecificOutput: {
          hookEventName: "PreToolUse",
          permissionDecision: "deny",
          permissionDecisionReason: "Destructive command blocked by hook"
        }
      }'
    else
      exit 0  # no decision; normal permission flow applies
    fi

    Look at the else branch. Exiting 0 with no output does *not* mean 'approved'. The comment says there is no decision, so the normal permission flow applies. A guardrail hook only has to handle the cases it cares about. For everything else it stays out of the way, and the rest of the permission system carries on as it would have without the hook.

    The hooks guide shows a second style with the same effect. Its protect-files.sh hook, registered on PreToolUse with matcher Edit|Write, checks tool_input.file_path against patterns such as .env, package-lock.json and .git/. On a match it writes a 'Blocked: …' message to stderr and exits with code 2.

    Sources12

    2.The same guardrail as an Agent SDK callback

    If you build your own agent on the Claude Agent SDK, you can write the guardrail as an in-process callback rather than a shell script. The callback receives the hook input (tool name, tool input, event name), the tool use ID, and a context object. It returns the same hookSpecificOutput shape. You register it in ClaudeAgentOptions under the event name with a HookMatcher, for example HookMatcher(matcher="Write|Edit", hooks=[protect_env_files]).

    A Python Agent SDK PreToolUse callback that denies edits to .env filespython
    async def protect_env_files(input_data, tool_use_id, context):
        # Extract the file path from the tool's input arguments
        file_path = input_data["tool_input"].get("file_path", "")
        file_name = file_path.split("/")[-1]
    
        # Block the operation if targeting a .env file
        if file_name == ".env":
            return {
                "hookSpecificOutput": {
                    "hookEventName": input_data["hook_event_name"],
                    "permissionDecision": "deny",
                    "permissionDecisionReason": "Cannot modify .env files",
                }
            }
    
        # Return empty object to allow the operation
        return {}

    SDK applications can combine two kinds of hook. Filesystem hooks in .claude/settings.json are for sharing hooks between CLI and SDK sessions. They support all five handler types and fire in the main agent and in any subagents it spawns, but only after you load them: the SDK example sets settingSources: ["project"] for exactly that reason. Programmatic callbacks passed to query() are for application-specific logic and structured decisions. They also fire inside subagents, and their input carries agent_id and agent_type, so you can tell which agent triggered the hook. For language coverage, PreToolUse, PostToolUse and PermissionRequest exist in both the Python and TypeScript SDKs. Several other events, such as PreModelSwitch and ElicitationResult, are TypeScript-only.

    A team configures a PreToolUse hook that returns exit code 0 with the following JSON on stdout: `{"hookSpecificOutput": {"hookEventName": "PreToolUse", "permissionDecision": "deny", "permissionDecisionReason": "Writing to /etc is not allowed"}}`. What happens to the pending tool call?

    Sources3

    3.Where hooks sit in permission evaluation

    Hooks are one step in a layered permission check, and they come first. The Agent SDK permissions guide gives the order below. Each step can settle the call, and anything left undecided moves on to the next step.

    Permission evaluation order in the Claude Agent SDK
    StepWhat it does
    1. HooksRun custom code to allow, deny, or modify tool requests
    2. Deny rulese.g. disallowed_tools=["Bash(rm *)"] denies matching calls in every permission mode
    3. Ask rulesExplicit ask rules route the call to approval
    4. Permission modebypassPermissions, acceptEdits, plan, or other modes
    5. Allow rulese.g. allowed_tools=["Read", "Grep"] auto-approves those tools
    6. canUseTool callbackPrompts users for approval at runtime, when no earlier step resolves the call

    This ordering is what makes a hook dependable. Loosening the mode does not switch it off. Deny rules, explicit ask rules and hooks are all evaluated before the mode check, so they can still block a tool under acceptEdits or bypassPermissions. The later steps still matter, though. A hook that returns no decision passes the call to the rules, the mode, and finally to canUseTool, where a person can approve it at runtime. That is how you get 'approve before it runs, then let it execute' for sensitive tools. The permission hook event for this is PermissionRequest, which Anthropic's tips suggest using to route permission prompts to Slack for review.

    Sources4

    4.A different layer: Enterprise inference hooks

    Don't confuse Claude Code hooks, which run on the machine or inside the SDK process, with inference hooks. Inference hooks are a beta Enterprise feature managed by Owners and Primary Owners. With them turned on, Claude sends every prompt to a server you host. That server checks the prompt against your policy and answers allow or deny, and Claude continues only after it gets the answer. The check runs inside Claude's infrastructure, not on someone's device, so it doesn't depend on anything installed on employees' machines. One setup covers Claude, Claude Code, Cowork and tool calls made through skills, plugins and connectors. Common uses are data loss prevention, transcript archival and enforcing organization policy.

    The two layers complement each other. A PreToolUse hook blocks a specific tool call inside one agent. An inference hook applies organization-wide policy to content before it reaches Claude.

    Sources5

    Exam traps

    Each one states something that sounds right. Open it to see what is actually true.

    1. 1.Running the agent in bypassPermissions or acceptEdits mode skips PreToolUse hooks, because those modes auto-approve tool calls.Why is that wrong?

      Hooks are evaluated before the permission mode is checked. A hook that denies a call blocks it whatever the mode is.

      Covered in Where hooks sit in permission evaluation

    2. 2.When a guardrail hook exits 0 without output, the tool call is approved and skips any further permission checks.Why is that wrong?

      Exiting 0 with no decision means the hook has no opinion. The call carries on through the normal permission flow of rules, mode and approval.

      Covered in Returning a deny decision from a command hook

    Practise it for real

    Add a project-level PreToolUse hook that stops Claude Code from editing .env, package-lock.json or anything under .git/.

    1. 1.Create .claude/hooks/protect-files.sh from the hooks guide. It reads tool_input.file_path from stdin with jq, compares it with PROTECTED_PATTERNS=(".env" "package-lock.json" ".git/"), and on a match writes a 'Blocked: …' message to stderr and runs exit 2. Otherwise it runs exit 0.

      Why: The script holds the deterministic rule, so blocking doesn't depend on the model following an instruction.

      You should see: A script file that exits 2 for protected paths and 0 for everything else.

    2. 2.On macOS or Linux, make it executable: chmod +x .claude/hooks/protect-files.sh.

      Why: A command handler runs the script directly, so it needs execute permission.

      You should see: ls -l .claude/hooks/protect-files.sh shows the execute bit.

    3. 3.Register it in .claude/settings.json under hooks.PreToolUse with "matcher": "Edit|Write" and a command handler pointing at "$CLAUDE_PROJECT_DIR"/.claude/hooks/protect-files.sh.

      Why: PreToolUse fires before the edit runs, the matcher limits the hook to file-writing tools, and .claude/settings.json can be committed so the whole team gets it.

      You should see: The hook is part of the project configuration and can be committed.

    4. 4.Test it: ask Claude Code to add a line to .env, then to edit an ordinary source file.

      Why: Testing both branches shows the hook blocks exactly the protected paths and nothing else.

      You should see: The .env edit is blocked with the 'matches protected pattern' message. The source-file edit goes ahead as normal.

    Stuck? Get a nudge

    If nothing gets blocked, check that the matcher covers the tool Claude actually used (Edit vs Write) and that the script is executable.

    Sources

    Every claim above is drawn from one of these pages, quoted as it was written on the date shown.

    1. 1.
      “no decision; normal permission flow applies”
      ↩︎ Returning a deny decision from a command hook
      “no decision; normal permission flow applies”
      ↩︎ Exam trap 2
    2. 2.
      “PROTECTED_PATTERNS=(".env" "package-lock.json" ".git/")”
      ↩︎ Returning a deny decision from a command hook
    3. 3.
      “Application-specific logic, structured decisions, and in-process integration. These also fire inside subagents.”
      ↩︎ The same guardrail as an Agent SDK callback
      “settingSources: ["project"], // Loads hooks from .claude/settings.json”
      ↩︎ The same guardrail as an Agent SDK callback
    4. 4.
      “Deny rules, explicit ask rules, and hooks are evaluated before the mode check and can still block a tool.”
      ↩︎ Where hooks sit in permission evaluation
      “canUseTool callback: prompt users for approval at runtime, when no earlier step resolves the call.”
      ↩︎ Where hooks sit in permission evaluation
      “Deny rules, explicit ask rules, and hooks are evaluated before the mode check and can still block a tool.”
      ↩︎ Exam trap 1
    5. 5.
      “Inference hooks lets your compliance team inspect and enforce policy on every prompt, tool call response, and uploaded file text before it reaches Claude.”
      ↩︎ A different layer: Enterprise inference hooks

    Ready to test yourself?

    Practise the 20 questions on this subdomain.