PreToolUse hooks to prevent dangerous operations: - Intercepts Bash, Write, Edit before execution - Contextual response (block vs confirm) - Path-aware with git repo detection - Session allowlist for user confirmations - Audit logging 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
8.3 KiB
Guardrail Hooks Design
Date: 2025-01-06 Status: Approved
Overview
PreToolUse guardrail hooks that prevent dangerous operations by intercepting Bash, Write, and Edit tool calls before execution.
Goals
- Prevent catastrophic operations (destructive file ops, dangerous system commands, infrastructure mistakes)
- Contextual response: hard block for severe threats, confirmation for moderate risks
- Path-aware: operations inside projects are more permissive than outside
- Auditable: log all interventions for review
Non-Goals
- Git command guardrails (future consideration)
- Rate limiting or resource protection
- Workflow compliance enforcement
Architecture
~/.claude/
├── hooks/
│ └── hooks.json # Hook registration (add PreToolUse)
├── hooks/scripts/
│ ├── guardrail.py # Main PreToolUse logic
│ └── guardrail-confirm.py # Adds operation to session allowlist
├── state/
│ ├── guardrails.json # Rules configuration
│ └── guardrail-session.json # Session allowlist (cleared on session end)
└── logs/
└── guardrail.jsonl # Audit log (blocked/confirmed only)
Flow
- Claude invokes Bash/Write/Edit
guardrail.pyruns via PreToolUse hook- Check session allowlist - if present, allow
- Evaluate rules + path context - decide action
- Allow: return
{"decision": "allow"} - Block/Confirm: log, return
{"decision": "block", "reason": "..."} - For confirm: user approves, Claude calls
guardrail-confirm.py, retries
Configuration
Location: ~/.claude/state/guardrails.json
{
"version": 1,
"safe_paths": [
"~/.claude",
"~/projects"
],
"blocked_paths": [
"/etc", "/usr", "/var", "/boot", "/sys", "/proc",
"~/.ssh", "~/.gnupg", "~/.aws"
],
"rules": {
"bash": [
{"pattern": "rm -rf /", "action": "block"},
{"pattern": "rm -rf ~", "action": "block"},
{"pattern": "rm -rf \\*", "action": "block"},
{"pattern": "chmod -R 777", "action": "block"},
{"pattern": ":(){ :|:& };:", "action": "block"},
{"pattern": "mkfs\\.", "action": "block"},
{"pattern": "dd if=.* of=/dev/", "action": "block"},
{"pattern": "> /dev/sda", "action": "block"},
{"pattern": "shutdown", "action": "confirm"},
{"pattern": "reboot", "action": "confirm"},
{"pattern": "systemctl (stop|disable|mask)", "action": "confirm"},
{"pattern": "rm ", "action": "confirm", "outside_safe_paths": true},
{"pattern": "kubectl delete", "action": "confirm"},
{"pattern": "docker rm", "action": "confirm"}
],
"write": [
{"path_patterns": ["blocked_paths"], "action": "block"},
{"path_patterns": ["outside_safe_paths"], "action": "confirm"}
],
"edit": [
{"path_patterns": ["blocked_paths"], "action": "block"},
{"path_patterns": ["outside_safe_paths"], "action": "confirm"}
]
}
}
Key Concepts
pattern: regex matched against command/pathaction:block(hard stop) orconfirm(require approval)outside_safe_paths: rule only applies when target is outside safe directories- Bash rules check command string; Write/Edit rules check file path
Safe Paths Logic
Evaluation order (first match wins):
-
Blocked paths check - Is target in
blocked_paths?- Yes: apply block/confirm regardless of other factors
- Protects system-critical locations absolutely
-
Explicit allowlist - Is target under a
safe_pathsentry?- Yes: path is safe
- Supports glob patterns (
~/projects/*)
-
Git-aware detection - Is target inside a git repository?
- Walk up from target path looking for
.gitdirectory - Found: treat as safe (it's an intentional project)
- Walk up from target path looking for
-
Default - Path is outside safe areas
- Operations here trigger
outside_safe_pathsrules
- Operations here trigger
Path Normalization
- Expand
~to actual home directory - Resolve symlinks to real paths
- Handle relative paths by resolving against CWD
Examples
| Target Path | Result | Reason |
|---|---|---|
/etc/hosts |
blocked/confirm | In blocked_paths |
~/.ssh/config |
blocked/confirm | In blocked_paths |
~/.claude/hooks/test.py |
safe | In safe_paths |
~/projects/myapp/src/main.py |
safe | In safe_paths |
~/random-repo/file.txt |
safe | Git repo detected |
~/Downloads/file.txt |
outside | No match, triggers rules |
Edge case: If a git repo exists under a blocked path (unlikely), blocked path wins.
Hook Implementation
Registration
Add to ~/.claude/hooks/hooks.json:
{
"PreToolUse": [
{
"matcher": "Bash|Write|Edit",
"hooks": [
{
"type": "command",
"command": "~/.claude/hooks/scripts/guardrail.py",
"timeout": 5
}
]
}
]
}
Hook Input (stdin)
{
"tool_name": "Bash",
"tool_input": {
"command": "rm -rf ~/Downloads/old-project"
}
}
Hook Output (stdout)
Allow:
{"decision": "allow"}
Block:
{
"decision": "block",
"reason": "GUARDRAIL BLOCKED: 'rm' outside safe paths.\nTarget: ~/Downloads/old-project\nRule: rm_outside_safe\n\nTo proceed, confirm with user then run:\npython ~/.claude/hooks/scripts/guardrail-confirm.py 'rm -rf ~/Downloads/old-project'"
}
Confirmation Flow
- Guardrail blocks with detailed message including confirm command
- Claude reports to user, asks for confirmation
- User confirms in conversation
- Claude runs confirm script:
python ~/.claude/hooks/scripts/guardrail-confirm.py "rm -rf ~/Downloads/old-project" - Confirm script adds to session allowlist
- Claude retries the original command
- Guardrail checks allowlist - finds match - allows
Session Allowlist
Location: ~/.claude/state/guardrail-session.json
{
"confirmed": [
{"tool": "Bash", "operation": "rm -rf ~/Downloads/old-project", "ts": "2025-01-06T10:24:00"}
]
}
Cleanup: session-end.sh clears this file so confirmations don't persist across sessions.
Matching: Exact match on tool + operation string.
Logging
Location: ~/.claude/logs/guardrail.jsonl
Format: JSON Lines (append-only)
Logged events:
- Every
blockaction - Every
confirmaction (required approval) - When confirmed operation is allowed
Log Entry Structure
{"ts": "2025-01-06T10:23:45", "tool": "Bash", "operation": "rm -rf /", "action": "block", "rule": "rm_rf_root", "path_context": "n/a"}
{"ts": "2025-01-06T10:24:01", "tool": "Bash", "operation": "rm ~/Downloads/old", "action": "confirm_required", "rule": "rm_outside_safe", "path_context": "outside"}
{"ts": "2025-01-06T10:24:30", "tool": "Bash", "operation": "rm ~/Downloads/old", "action": "confirmed_allow", "rule": "session_allowlist"}
Fields
ts: ISO 8601 timestamptool: Bash, Write, or Editoperation: Command or file pathaction:block,confirm_required, orconfirmed_allowrule: Which rule triggeredpath_context:safe,outside,blocked, orn/a
Implementation Checklist
- Create
state/guardrails.jsonwith starter rules - Create
hooks/scripts/guardrail.pymain logic - Create
hooks/scripts/guardrail-confirm.pyconfirm helper - Modify
hooks/hooks.jsonto add PreToolUse registration - Modify
hooks/scripts/session-end.shto clear session allowlist - Create
logs/directory - Test: block scenario (catastrophic command)
- Test: confirm scenario (rm outside safe paths)
- Test: allow scenario (operation in safe path)
- Test: git-aware detection
Starter Rules
| Category | Pattern | Action |
|---|---|---|
| Catastrophic | rm -rf /, rm -rf ~, mkfs.*, dd.*of=/dev/ |
block |
| Fork bomb | :(){ :|:& };: |
block |
| Dangerous chmod | chmod -R 777 |
block |
| System commands | shutdown, reboot |
confirm |
| Service control | systemctl (stop|disable|mask) |
confirm |
| Destructive outside safe | rm (outside safe paths) |
confirm |
| K8s destructive | kubectl delete |
confirm |
| Docker destructive | docker rm, docker system prune |
confirm |
| Write to blocked paths | Any Write/Edit to /etc, ~/.ssh, etc. |
block |
| Write outside safe | Any Write/Edit outside safe paths | confirm |