PreToolUse — Building Real Gates, Not Just Logs
PreToolUse is the only hook that can stop Claude before it acts. Understanding exit codes, the decision JSON format, and common blocking patterns turns it from a logging mechanism into a real enforcement gate.
Quick Reference
- →PreToolUse fires before every tool call — it can block the action entirely
- →Exit 0: allow (proceed silently). Exit 1: warn but continue. Exit 2: BLOCK the action
- →Exit 1 does NOT block — this is the #1 hook mistake engineers make
- →JSON output: {"decision": "deny", "reason": "message Claude sees"} — exit 0 to use JSON
- →Matcher field filters which tools trigger the hook: "Bash", "Edit", "mcp__github__*"
- →if field for pattern-specific triggers: if: "Bash(git push*)" only fires for git push
- →Use hook logic for dynamic gates (check branch name); use deny rules for static patterns
- →PermissionRequest hook + updatedPermissions: auto-approve future similar requests
Why PreToolUse Is the Most Important Hook
Every other hook observes or reacts. PreToolUse controls. It fires before the tool executes — which means your hook script can examine the action and decide whether it happens at all.
This is fundamentally different from a deny rule in settings.json. A deny rule is a static pattern: Bash(git push --force*). A PreToolUse hook is code: it can read the current git branch, check who's running the session, inspect the file being edited, or make a network call to an approval system before deciding whether to allow or block.
Deny rules are for static, unconditional blocks: always deny force-push. PreToolUse hooks are for conditional logic: deny push to main, but allow push to main if the current user is a designated release engineer. If the rule doesn't need branching logic, use a deny rule — it's simpler. If it does, use a PreToolUse hook.