# 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 1. Claude invokes Bash/Write/Edit 2. `guardrail.py` runs via PreToolUse hook 3. Check session allowlist - if present, allow 4. Evaluate rules + path context - decide action 5. Allow: return `{"decision": "allow"}` 6. Block/Confirm: log, return `{"decision": "block", "reason": "..."}` 7. For confirm: user approves, Claude calls `guardrail-confirm.py`, retries ## Configuration **Location:** `~/.claude/state/guardrails.json` ```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/path - `action`: `block` (hard stop) or `confirm` (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):** 1. **Blocked paths check** - Is target in `blocked_paths`? - Yes: apply block/confirm regardless of other factors - Protects system-critical locations absolutely 2. **Explicit allowlist** - Is target under a `safe_paths` entry? - Yes: path is safe - Supports glob patterns (`~/projects/*`) 3. **Git-aware detection** - Is target inside a git repository? - Walk up from target path looking for `.git` directory - Found: treat as safe (it's an intentional project) 4. **Default** - Path is outside safe areas - Operations here trigger `outside_safe_paths` rules ### 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`: ```json { "PreToolUse": [ { "matcher": "Bash|Write|Edit", "hooks": [ { "type": "command", "command": "~/.claude/hooks/scripts/guardrail.py", "timeout": 5 } ] } ] } ``` ### Hook Input (stdin) ```json { "tool_name": "Bash", "tool_input": { "command": "rm -rf ~/Downloads/old-project" } } ``` ### Hook Output (stdout) Allow: ```json {"decision": "allow"} ``` Block: ```json { "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 1. **Guardrail blocks** with detailed message including confirm command 2. **Claude reports** to user, asks for confirmation 3. **User confirms** in conversation 4. **Claude runs confirm script:** ```bash python ~/.claude/hooks/scripts/guardrail-confirm.py "rm -rf ~/Downloads/old-project" ``` 5. **Confirm script** adds to session allowlist 6. **Claude retries** the original command 7. **Guardrail checks allowlist** - finds match - allows ### Session Allowlist **Location:** `~/.claude/state/guardrail-session.json` ```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 `block` action - Every `confirm` action (required approval) - When confirmed operation is allowed ### Log Entry Structure ```json {"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 timestamp - `tool`: Bash, Write, or Edit - `operation`: Command or file path - `action`: `block`, `confirm_required`, or `confirmed_allow` - `rule`: Which rule triggered - `path_context`: `safe`, `outside`, `blocked`, or `n/a` ## Implementation Checklist - [ ] Create `state/guardrails.json` with starter rules - [ ] Create `hooks/scripts/guardrail.py` main logic - [ ] Create `hooks/scripts/guardrail-confirm.py` confirm helper - [ ] Modify `hooks/hooks.json` to add PreToolUse registration - [ ] Modify `hooks/scripts/session-end.sh` to 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 |