PreToolUse and the Permission System
- Configure a PreToolUse hook to block, auto-approve, or modify tool calls before they execute
- Apply the if filter to target hooks with sub-tool precision beyond what the matcher provides
- Wire PermissionRequest and PermissionDenied hooks to complete the permission pipeline
The Permission Lifecycle: Three Interception Points
Before Claude Code executes a tool call, it evaluates whether the action is permitted. This evaluation involves three distinct hook events, each firing at a different point in the permission process.
PreToolUse fires before every tool call, before any permission dialog appears. It is the earliest interception point and the most powerful — it can block the call entirely, approve it silently, or modify the input before it reaches the tool. Most security enforcement happens here.
PermissionRequest fires when a permission dialog is about to appear. If PreToolUse did not return a decision, Claude Code would normally pause and show you a prompt. PermissionRequest lets a hook intercept that moment and provide a decision automatically — approving safe patterns without bothering you, denying risky ones without waiting for input.
PermissionDenied fires after a permission denial, whether it came from PreToolUse, PermissionRequest, or a user clicking deny. It cannot undo the denial, but it can react — logging the blocked attempt, sending a notification, or offering an alternative approach via retry: true.
Together these three events give you complete control over the permission system without relying on Claude Code's default behavior.
PreToolUse: Blocking and Modifying Tool Calls
PreToolUse decision logic lives in the hookSpecificOutput object returned by the hook. The key field is permissionDecision, which accepts four values.
"deny" blocks the tool call. Claude Code surfaces the permissionDecisionReason string as the explanation. The tool does not run. This is the right value for security gates that should never let certain commands through.
"allow" approves the call without showing a permission dialog. Claude Code skips the normal approval step and runs the tool immediately. Use this to auto-approve patterns you know are safe so the user is not interrupted.
"ask" shows the permission dialog even if the tool would normally run without approval. Use this for commands that are usually safe but deserve a second look in specific contexts.
"defer" passes the decision to the next handler in the chain. If no subsequent handler returns a decision, Claude Code uses its default behavior. Use defer when your hook only handles certain patterns and wants to stay out of the way for everything else.
A minimal blocking hook for destructive rm commands:
#!/bin/bash input=$(cat) cmd=$(echo "$input" | jq -r .tool_input.command // empty) if echo "$cmd" | grep -qE "rms+-[a-zA-Z]*r[a-zA-Z]*f|rms+-[a-zA-Z]*f[a-zA-Z]*r"; then echo "$cmd matched destructive rm pattern" >&2 cat <
updatedInput: Rewriting Tool Calls Before Execution
PreToolUse can do more than block or approve — it can transform the tool input before it reaches the tool. The updatedInput field in hookSpecificOutput replaces the entire tool_input object with a new one. Claude Code uses the updated input when it runs the tool.
This enables a range of useful patterns. A hook can add a --dry-run flag to every git push command until you explicitly disable it. A hook can rewrite a curl command to add authentication headers it would otherwise lack. A hook can normalize file paths before an Edit call to ensure they are always relative to the project root.
Using updatedInput together with permissionDecision: "allow" creates a sanitization layer — dangerous commands are transformed to safe equivalents and auto-approved, rather than blocked outright.
The if Filter: Sub-Tool Precision
The matcher in a hook configuration targets tool names. But for the Bash tool, every shell command goes through the same tool. You often need to distinguish between git status (safe, auto-approve) and git push --force (dangerous, block).
The if field on a handler provides a second targeting layer using permission-rule syntax. Unlike the matcher, which selects which tool names trigger the handler, if evaluates a pattern against the tool input itself and only activates the handler when the pattern matches.
For Bash commands, if: "Bash(git *)" matches only when the bash command begins with git (stripping leading VAR=value assignments). if: "Edit(*.ts)" matches only when the file being edited has a .ts extension. if: "Bash(rm *)" targets only rm commands.
The if filter is best-effort — if the permission pattern fails to parse, the hook runs anyway. Design hooks so their logic handles mismatches gracefully rather than relying solely on the filter.
PermissionRequest: Auto-Approving Dialogs
When Claude Code would normally show a permission dialog, the PermissionRequest event fires first. A hook can intercept this and return a decision, skipping the dialog entirely.
The output format is different from PreToolUse. PermissionRequest decisions live in hookSpecificOutput.decision with a behavior field set to "allow" or "deny". An optional updatedInput field works the same way as in PreToolUse.
A common use case: auto-approve read-only git commands in auto mode while still prompting for git push. A PermissionRequest hook that matches Bash can read the command and return allow for git status, git log, and git diff while returning nothing (or deny) for git push. This creates a custom permission policy layered on top of whatever permission mode is active.
PermissionDenied: Reacting to Blocked Actions
After any permission denial, PermissionDenied fires. The hook receives tool_name, tool_input, and the reason the denial occurred. It cannot reverse the denial — the action is already blocked — but it can respond.
Common responses: log the blocked attempt to an audit file, send a notification to a monitoring system, or set retry: true in the hook output. The retry: true field tells Claude Code to attempt the action again immediately, which is useful when the denial was due to a recoverable condition (a missing auth token has been refreshed, a required service is now available).
The three-hook pipeline — PreToolUse gates, PermissionRequest auto-approves, PermissionDenied audits — gives you complete control over who can do what and a full record of what was blocked.
- PreToolUse is the primary security gate — it fires before every tool call and can block execution, auto-approve without showing a dialog, or rewrite the tool input before it runs
- permissionDecision has four values — deny stops the call, allow approves without a dialog, ask forces the dialog anyway, defer passes to the next handler in the chain
- updatedInput rewrites the tool call before execution — add flags, sanitize arguments, or swap dangerous commands for safer equivalents that can then be auto-approved
- PermissionRequest fires when a permission dialog is about to appear — use it to auto-approve specific patterns while still prompting for everything else
- The if filter adds a second targeting layer — Bash(git *) passes only when the bash command starts with git, enabling sub-command precision without complex regex matchers