Skip to content

Instantly share code, notes, and snippets.

@Ar9av
Created May 31, 2026 02:46
Show Gist options
  • Select an option

  • Save Ar9av/396fc877b81680220eb2348c50d453c3 to your computer and use it in GitHub Desktop.

Select an option

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
#!/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