Add guardrail hooks design document
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>
This commit is contained in:
270
docs/plans/2025-01-06-guardrail-hooks-design.md
Normal file
270
docs/plans/2025-01-06-guardrail-hooks-design.md
Normal file
@@ -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 |
|
||||||
Reference in New Issue
Block a user