--- name: dcg description: "Destructive Command Guard - High-performance Rust hook for Claude Code that blocks dangerous commands before execution. SIMD-accelerated, modular pack system, whitelist-first architecture. Essential safety layer for agent workflows." --- # DCG — Destructive Command Guard A high-performance Claude Code hook that intercepts and blocks destructive commands before they execute. Written in Rust with SIMD-accelerated filtering for sub-millisecond latency. ## Why This Exists AI coding agents are powerful but fallible. They can accidentally run destructive commands: - **"Let me clean up the build artifacts"** → `rm -rf ./src` (typo) - **"I'll reset to the last commit"** → `git reset --hard` (destroys uncommitted changes) - **"Let me fix the merge conflict"** → `git checkout -- .` (discards all modifications) - **"I'll clean up untracked files"** → `git clean -fd` (permanently deletes untracked files) DCG intercepts dangerous commands *before* execution and blocks them with a clear explanation, giving you a chance to stash your changes first. ## Critical Design Principles ### 1. Whitelist-First Architecture Safe patterns are checked *before* destructive patterns. This ensures explicitly safe commands are never accidentally blocked: ``` git checkout -b feature → Matches SAFE "checkout-new-branch" → ALLOW git checkout -- file.txt → No safe match, matches DESTRUCTIVE → DENY ``` ### 2. Fail-Safe Defaults (Default-Allow) Unrecognized commands are **allowed by default**. This ensures: - The hook never breaks legitimate workflows - Only *known* dangerous patterns are blocked - New git commands work until explicitly categorized ### 3. Zero False Negatives Philosophy The pattern set prioritizes **never allowing dangerous commands** over avoiding false positives. A few extra prompts for manual confirmation are acceptable; lost work is not. ## What It Blocks ### Git Commands That Destroy Uncommitted Work | Command | Reason | |---------|--------| | `git reset --hard` | Destroys uncommitted changes | | `git reset --merge` | Destroys uncommitted changes | | `git checkout -- ` | Discards file modifications | | `git restore ` (without `--staged`) | Discards uncommitted changes | | `git clean -f` | Permanently deletes untracked files | ### Git Commands That Destroy Remote History | Command | Reason | |---------|--------| | `git push --force` / `-f` | Overwrites remote commits | | `git branch -D` | Force-deletes without merge check | ### Git Commands That Destroy Stashed Work | Command | Reason | |---------|--------| | `git stash drop` | Permanently deletes a stash | | `git stash clear` | Permanently deletes all stashes | ### Filesystem Commands | Command | Reason | |---------|--------| | `rm -rf` (outside `/tmp`, `/var/tmp`, `$TMPDIR`) | Recursive deletion is dangerous | ## What It ALLOWS Safe operations pass through silently: ### Always Safe Git Operations `git status`, `git log`, `git diff`, `git add`, `git commit`, `git push`, `git pull`, `git fetch`, `git branch -d` (safe delete with merge check), `git stash`, `git stash pop`, `git stash list` ### Explicitly Safe Patterns | Pattern | Why Safe | |---------|----------| | `git checkout -b ` | Creating new branches | | `git checkout --orphan ` | Creating orphan branches | | `git restore --staged ` | Unstaging only, doesn't touch working tree | | `git restore -S ` | Short flag for staged | | `git clean -n` / `--dry-run` | Preview mode, no actual deletion | | `rm -rf /tmp/*` | Temp directories are ephemeral | | `rm -rf $TMPDIR/*` | Shell variable forms | ### Safe Alternative: `--force-with-lease` ```bash git push --force-with-lease # ALLOWED - refuses if remote has unseen commits git push --force # BLOCKED - can overwrite others' work ``` ## Modular Pack System DCG uses a modular "pack" system to organize patterns by category: ### Core Packs (Always Enabled) | Pack | Description | |------|-------------| | `core.git` | Destructive git commands | | `core.filesystem` | Dangerous rm -rf outside temp | ### Database Packs | Pack | Description | |------|-------------| | `database.postgresql` | DROP/TRUNCATE in PostgreSQL | | `database.mysql` | DROP/TRUNCATE in MySQL/MariaDB | | `database.mongodb` | dropDatabase, drop() | | `database.redis` | FLUSHALL/FLUSHDB | | `database.sqlite` | DROP in SQLite | ### Container Packs | Pack | Description | |------|-------------| | `containers.docker` | docker system prune, docker rm -f | | `containers.compose` | docker-compose down --volumes | | `containers.podman` | podman system prune | ### Kubernetes Packs | Pack | Description | |------|-------------| | `kubernetes.kubectl` | kubectl delete namespace | | `kubernetes.helm` | helm uninstall | | `kubernetes.kustomize` | kustomize delete patterns | ### Cloud Provider Packs | Pack | Description | |------|-------------| | `cloud.aws` | Destructive AWS CLI commands | | `cloud.gcp` | Destructive gcloud commands | | `cloud.azure` | Destructive az commands | ### Infrastructure Packs | Pack | Description | |------|-------------| | `infrastructure.terraform` | terraform destroy | | `infrastructure.ansible` | Dangerous ansible patterns | | `infrastructure.pulumi` | pulumi destroy | ### System Packs | Pack | Description | |------|-------------| | `system.disk` | dd, mkfs, fdisk operations | | `system.permissions` | Dangerous chmod/chown patterns | | `system.services` | systemctl stop/disable patterns | ### Other Packs | Pack | Description | |------|-------------| | `strict_git` | Extra paranoid git protections | | `package_managers` | npm unpublish, cargo yank | ### Configuring Packs ```toml # ~/.config/dcg/config.toml [packs] enabled = [ "database.postgresql", "containers.docker", "kubernetes", # Enables all kubernetes sub-packs ] ``` ## Environment Variables | Variable | Description | |----------|-------------| | `DCG_PACKS="containers.docker,kubernetes"` | Enable packs (comma-separated) | | `DCG_DISABLE="kubernetes.helm"` | Disable packs/sub-packs | | `DCG_VERBOSE=1` | Verbose output | | `DCG_COLOR=auto\|always\|never` | Color mode | | `DCG_BYPASS=1` | Bypass DCG entirely (escape hatch) | ## Installation ### Quick Install (Recommended) ```bash curl -fsSL "https://raw.githubusercontent.com/Dicklesworthstone/destructive_command_guard/master/install.sh?$(date +%s)" | bash # Easy mode: auto-update PATH curl -fsSL "https://raw.githubusercontent.com/Dicklesworthstone/destructive_command_guard/master/install.sh?$(date +%s)" | bash -s -- --easy-mode # System-wide (requires sudo) curl -fsSL "https://raw.githubusercontent.com/Dicklesworthstone/destructive_command_guard/master/install.sh?$(date +%s)" | sudo bash -s -- --system ``` ### From Source (Requires Rust Nightly) ```bash cargo +nightly install --git https://github.com/Dicklesworthstone/destructive_command_guard ``` ### Prebuilt Binaries Available for: Linux x86_64, Linux ARM64, macOS Intel, macOS Apple Silicon, Windows ## Claude Code Configuration Add to `~/.claude/settings.json`: ```json { "hooks": { "PreToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "command", "command": "dcg" } ] } ] } } ``` **Important:** Restart Claude Code after adding the hook. ## How It Works ### Processing Pipeline ``` ┌─────────────────────────────────────────────────────────────────┐ │ Claude Code │ │ Agent executes `rm -rf ./build` │ └─────────────────────┬───────────────────────────────────────────┘ │ ▼ PreToolUse hook (stdin: JSON) ┌─────────────────────────────────────────────────────────────────┐ │ dcg │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ Parse │───▶│ Normalize │───▶│ Quick Reject │ │ │ │ JSON │ │ Command │ │ Filter │ │ │ └──────────────┘ └──────────────┘ └──────┬───────┘ │ │ │ │ │ ┌───────────────────────────┘ │ │ ▼ │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ Pattern Matching │ │ │ │ 1. Check SAFE_PATTERNS (whitelist) ──▶ Allow if match │ │ │ │ 2. Check DESTRUCTIVE_PATTERNS ──────▶ Deny if match │ │ │ │ 3. No match ────────────────────────▶ Allow (default) │ │ │ └──────────────────────────────────────────────────────────┘ │ └─────────────────────┬───────────────────────────────────────────┘ │ ▼ stdout: JSON (deny) or empty (allow) ``` ### Stage 1: JSON Parsing - Reads hook input from stdin - Validates Claude Code's `PreToolUse` format - Non-Bash tools immediately allowed ### Stage 2: Command Normalization - Strips absolute paths: `/usr/bin/git status` → `git status` - Preserves argument paths ### Stage 3: Quick Rejection Filter - SIMD-accelerated substring search for "git" or "rm" - Commands without these bypass regex entirely (99%+ of commands) ### Stage 4: Pattern Matching - Safe patterns checked first (short-circuit on match → allow) - Destructive patterns checked second (match → deny) - No match → default allow ## Exit Codes | Code | Meaning | |------|---------| | `0` | Command is safe, proceed | | `2` | Command is blocked, do not execute | ## CLI Usage Test commands manually: ```bash # Show version with build metadata dcg --version # Test a command echo '{"tool_name":"Bash","tool_input":{"command":"git reset --hard"}}' | dcg ``` ## Example Block Message ``` ════════════════════════════════════════════════════════════════════════ BLOCKED dcg ──────────────────────────────────────────────────────────────────────── Reason: git reset --hard destroys uncommitted changes. Use 'git stash' first. Command: git reset --hard HEAD~1 Tip: If you need to run this command, execute it manually in a terminal. Consider using 'git stash' first to save your changes. ════════════════════════════════════════════════════════════════════════ ``` ### Contextual Suggestions | Command Type | Suggestion | |-------------|------------| | `git reset`, `git checkout --` | "Consider using 'git stash' first" | | `git clean` | "Use 'git clean -n' first to preview" | | `git push --force` | "Consider using '--force-with-lease'" | | `rm -rf` | "Verify the path carefully before running manually" | ## Edge Cases Handled ### Path Normalization ```bash /usr/bin/git reset --hard # Blocked /usr/local/bin/git checkout -- . # Blocked /bin/rm -rf /home/user # Blocked ``` ### Flag Ordering Variants ```bash rm -rf /path # Combined flags rm -fr /path # Reversed order rm -r -f /path # Separate flags rm --recursive --force /path # Long flags ``` All variants are handled. ### Shell Variable Expansion ```bash rm -rf $TMPDIR/build # Allowed (temp) rm -rf ${TMPDIR}/build # Allowed rm -rf "$TMPDIR/build" # Allowed rm -rf "${TMPDIR:-/tmp}/build" # Allowed ``` ### Staged vs Worktree Restore ```bash git restore --staged file.txt # Allowed (unstaging only) git restore -S file.txt # Allowed (short flag) git restore file.txt # BLOCKED (discards changes) git restore --worktree file.txt # BLOCKED (explicit worktree) git restore -S -W file.txt # BLOCKED (includes worktree) ``` ## Performance Optimizations DCG is designed for zero perceived latency: | Optimization | Technique | |--------------|-----------| | **Lazy Static** | Regex patterns compiled once via `LazyLock` | | **SIMD Quick Reject** | `memchr` crate for CPU vector instructions | | **Early Exit** | Safe match returns immediately | | **Zero-Copy JSON** | `serde_json` operates on input buffer | | **Zero-Allocation** | `Cow` for path normalization | | **Release Profile** | `opt-level="z"`, LTO, single codegen unit | **Result:** Sub-millisecond execution for typical commands. ## Pattern Counts | Type | Count | |------|-------| | Safe patterns (whitelist) | 34 | | Destructive patterns (blacklist) | 16 | ## Security Considerations ### What DCG Protects Against - Accidental data loss from `git checkout --` or `git reset --hard` - Remote history destruction from force pushes - Stash loss from `git stash drop/clear` - Filesystem accidents from `rm -rf` outside temp directories ### What DCG Does NOT Protect Against - Malicious actors (can bypass the hook) - Non-Bash commands (Python/JavaScript file writes, API calls) - Committed but unpushed work - Commands inside scripts (`./deploy.sh` contents not inspected) ### Threat Model DCG assumes the AI agent is **well-intentioned but fallible**. It catches honest mistakes, not adversarial attacks. ## Troubleshooting ### Hook not blocking commands 1. Verify `~/.claude/settings.json` has hook configuration 2. Restart Claude Code 3. Test manually: `echo '{"tool_name":"Bash","tool_input":{"command":"git reset --hard"}}' | dcg` ### Hook blocking safe commands 1. Check if there's an edge case not covered 2. File a GitHub issue 3. Temporary bypass: `DCG_BYPASS=1` or run command manually ## FAQ **Q: Why block `git branch -D` but allow `git branch -d`?** Lowercase `-d` only deletes branches fully merged. Uppercase `-D` force-deletes regardless of merge status, potentially losing commits. **Q: Why is `git push --force-with-lease` allowed?** Force-with-lease refuses to push if the remote has commits you haven't seen, preventing accidental overwrites. **Q: Why block all `rm -rf` outside temp directories?** Recursive forced deletion is extremely dangerous. A typo or wrong variable can delete critical files. Temp directories are designed to be ephemeral. **Q: What if I really need to run a blocked command?** DCG instructs the agent to ask for permission. Run the command manually in a separate terminal after making a conscious decision. ## Integration with Flywheel | Tool | Integration | |------|-------------| | **Claude Code** | Native PreToolUse hook | | **Agent Mail** | Agents can report blocked commands to coordinator | | **BV** | Flag tasks that repeatedly trigger DCG | | **CASS** | Search DCG block patterns across sessions | | **RU** | DCG protects agent-sweep from destructive commits |