diff --git a/docs/plans/2025-01-06-guardrail-hooks-design.md b/docs/plans/2025-01-06-guardrail-hooks-design.md new file mode 100644 index 0000000..5d47c4c --- /dev/null +++ b/docs/plans/2025-01-06-guardrail-hooks-design.md @@ -0,0 +1,270 @@ +# 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 |