Created
May 31, 2026 02:46
-
-
Save Ar9av/396fc877b81680220eb2348c50d453c3 to your computer and use it in GitHub Desktop.
Claude Code PreToolUse hook (Python, zero-deps): block destructive shell commands — rm -rf /, dd to disk, fork bombs, curl-pipe-shell, force-push to main — before they run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """ | |
| Claude Code PreToolUse hook — block obviously destructive shell commands. | |
| A minimal, dependency-free guardrail you can drop into any Claude Code project. | |
| It inspects every Bash command BEFORE it runs and blocks a small set of | |
| high-blast-radius patterns (recursive root deletes, disk overwrites, fork bombs, | |
| pipe-to-shell installs, force-push to main, etc.). | |
| This is deliberately tiny — a starting point, not a security product. It does | |
| naive regex matching and only covers the Bash tool. For a real policy engine | |
| (observe/enforce modes, secret prevention, supply-chain blocking, MCP/skill | |
| scanning, audit logs and a dashboard), see Immunity Agent: | |
| https://github.com/PrismorSec/immunity-agent | |
| --- Install ---------------------------------------------------------------- | |
| 1. Save this file, e.g. ~/.claude/hooks/block_dangerous.py | |
| chmod +x ~/.claude/hooks/block_dangerous.py | |
| 2. Register it in .claude/settings.json (project) or ~/.claude/settings.json: | |
| { | |
| "hooks": { | |
| "PreToolUse": [ | |
| { | |
| "matcher": "Bash", | |
| "hooks": [ | |
| { "type": "command", "command": "~/.claude/hooks/block_dangerous.py" } | |
| ] | |
| } | |
| ] | |
| } | |
| } | |
| Claude Code pipes a JSON event on stdin. Exit code 2 blocks the tool call and | |
| feeds the message on stderr back to the model so it can course-correct. | |
| ---------------------------------------------------------------------------- | |
| """ | |
| import json | |
| import re | |
| import sys | |
| # (pattern, human-readable reason). Patterns are intentionally conservative — | |
| # better to under-block than to nag on safe commands. Tune to your environment. | |
| DANGEROUS = [ | |
| (r"\brm\s+(-[a-z]*\s+)*-[a-z]*[rf][a-z]*\s+(-[a-z]*\s+)*(/|~|\$HOME|\*|\.)(\s|$)", | |
| "Recursive force-delete of a root / home / wildcard path"), | |
| (r":\(\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:", "Fork bomb"), | |
| (r"\bdd\b.*\bof=/dev/(sd|nvme|disk|hd)", "Raw write to a block device"), | |
| (r"\bmkfs\.", "Filesystem format"), | |
| (r">\s*/dev/(sd|nvme|disk|hd)", "Redirect onto a block device"), | |
| (r"\b(curl|wget)\b[^|]*\|\s*(sudo\s+)?(sh|bash|zsh)\b", | |
| "Pipe a remote script straight into a shell"), | |
| (r"\bchmod\s+(-[a-z]*\s+)*777\b", "World-writable chmod 777"), | |
| (r"\bgit\s+push\b.*\s(--force|-f)\b.*\b(main|master)\b", | |
| "Force-push to main/master"), | |
| (r"\bgit\s+push\b.*\b(main|master)\b.*\s(--force|-f)\b", | |
| "Force-push to main/master"), | |
| ] | |
| def main() -> int: | |
| try: | |
| event = json.load(sys.stdin) | |
| except Exception: | |
| return 0 # fail open: never break the agent on malformed input | |
| if event.get("tool_name") != "Bash": | |
| return 0 | |
| command = (event.get("tool_input") or {}).get("command", "") | |
| if not command: | |
| return 0 | |
| for pattern, reason in DANGEROUS: | |
| if re.search(pattern, command, re.IGNORECASE): | |
| print( | |
| f"BLOCKED by block_dangerous hook: {reason}.\n" | |
| f"Command: {command}\n" | |
| f"If this is genuinely intended, ask the user to run it manually.", | |
| file=sys.stderr, | |
| ) | |
| return 2 # exit code 2 => block the tool call | |
| return 0 | |
| if __name__ == "__main__": | |
| sys.exit(main()) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment