Skip to content

Instantly share code, notes, and snippets.

@HenryQW
Last active September 7, 2025 20:24
Show Gist options
  • Save HenryQW/29589ab608ce3e2e59e6df1359286862 to your computer and use it in GitHub Desktop.
Save HenryQW/29589ab608ce3e2e59e6df1359286862 to your computer and use it in GitHub Desktop.
Guradrails to prevent Claude Code from performing high-risk operations. Use `PreToolUse` hooks for activation. O(n) complexity with < 20ms execution time.
#!/usr/bin/env python3
# scripts/guardrails.py
"""Safety guardrails for shell commands executed by tools.
Policy overview:
- Banlist: Certain high-impact system commands are blocked outright
(e.g., diskutil, fdisk, parted, mkfs*). These are denied regardless of args.
- Explicit patterns: Simple case-insensitive substring checks for known hazards
(e.g., fork bomb, chmod -R 777, dd if=/dev/zero).
- Heuristics: Minimal, conservative checks for risky patterns like rm -rf on
root/home, root-level find -delete, piping remote scripts into a shell, and
writing to block devices via dd.
Keep this file small and readable; extend ban/heuristics carefully.
Usage:
Add to ~/.claude/settings.json:
"hooks": {
"PreToolUse": [
{
"matcher": ".*",
"hooks": [
{
"type": "command",
"command": "python /path/to/guardrails.py"
}
]
}
]
}
"""
import os
import sys
import shlex
from typing import List
# Keep this small and focused: fast checks first, then minimal heuristics.
#
# Policy: Some system-impacting commands are banned outright (banlist below),
# regardless of arguments. Heuristic checks then handle nuanced cases.
# Explicit hazardous substrings (checked case-insensitively)
DANGEROUS_SUBSTRINGS = (
# Core explicit hazards (case-insensitive check)
"rm -rf /",
"rm -rf ~",
"sudo rm",
":(){:|:&};:",
"dd if=/dev/zero",
"chmod -r 777", # covers "-R" via lowercase normalization
)
# Commands that are ALWAYS blocked due to high system impact, regardless of args.
# Adjust this list conservatively; prefer explicit items to avoid surprises.
BAN_COMMANDS = {
# Disk and partition management
"diskutil",
"fdisk",
"sfdisk",
"parted",
"wipefs",
# LVM / RAID / crypto (high-impact system storage)
"lvremove",
"vgremove",
"pvremove",
"mdadm",
"cryptsetup",
# Mounting state changes
"mount",
"umount",
# Power and shutdown
"reboot",
"poweroff",
"halt",
"shutdown",
}
# Command name prefixes that are ALWAYS blocked (e.g., mkfs, mkfs.ext4, ...)
BAN_PREFIXES = ("mkfs",) # blocks mkfs and all mkfs.* variants
# Common wrapper commands that may prefix the real command
WRAPPER_COMMANDS = {"sudo", "env", "time", "nohup", "nice", "stdbuf", "ionice"}
# Well-known device nodes considered safe (not block devices)
SAFE_DEVICE_PATHS = (
"/dev/null",
"/dev/zero",
"/dev/tty",
"/dev/random",
"/dev/urandom",
)
def _block(reason: str, command: str) -> None:
print(f"🛑 BLOCKED: {reason}", file=sys.stderr)
print(f"Command was: {command}", file=sys.stderr)
sys.exit(1)
def _tokenize(cmd: str) -> List[str]:
try:
return shlex.split(cmd, posix=True)
except Exception:
# Fallback to a naive split if quoting is malformed
return cmd.split()
def _parse_lower(cmd: str) -> tuple[str, List[str]]:
"""Return lowercased raw command and tokens for uniform checks."""
lower_raw = cmd.lower()
tokens = _tokenize(cmd)
return lower_raw, [t.lower() for t in tokens]
def _rm_risky(tokens: List[str], raw_cmd: str) -> bool:
"""Consolidated rm hazards: many wildcards, no-preserve-root, root/home wipes.
tokens should be lowercased; raw_cmd is the original command (can be lowercased).
"""
if not tokens:
return False
try:
rm_idx = tokens.index("rm")
except ValueError:
return False
# Many wildcards in rm command (coarse but useful signal)
if raw_cmd.count("*") > 2:
return True
# Flags and targets
flags = "".join(t for t in tokens[rm_idx + 1 :] if t.startswith("-"))
targets = [t for t in tokens[rm_idx + 1 :] if not t.startswith("-")]
# --no-preserve-root is always risky
if any(t == "--no-preserve-root" for t in tokens[rm_idx + 1 :]):
return True
# Only care for recursive forceful deletes
rf = ("r" in flags) and ("f" in flags)
if not rf:
return False
if not targets:
return False
risky_literals = {"/", "~", "/*"}
for t in targets:
if t in risky_literals:
return True
if t.startswith("/*"):
return True
# very broad wildcard at top-level or under home
if t == "*" or (t.startswith("~/") and "*" in t):
return True
return False
def _base_command(tokens: List[str]) -> str:
"""Return the base command name (without path), lowercased.
Skips common wrappers (sudo, env VAR=val, time, etc.).
"""
if not tokens:
return ""
i = 0
# Skip common wrappers and env assignments (e.g., VAR=val)
wrappers = WRAPPER_COMMANDS
while i < len(tokens):
t = tokens[i]
if t in wrappers or ("=" in t and not t.startswith("-")):
i += 1
continue
break
if i >= len(tokens):
return ""
t = tokens[i]
# Strip leading path segments
if "/" in t:
t = t.rsplit("/", 1)[-1]
return t.lower()
def _is_banned_command(tokens: List[str]) -> str:
"""Return banned identifier if command is banned unconditionally, else ''."""
base = _base_command(tokens)
if not base:
return ""
if base in BAN_COMMANDS:
return base
for pref in BAN_PREFIXES:
if base.startswith(pref):
return pref + "*"
return ""
def _is_block_device_path(p: str) -> bool:
if not p.startswith("/dev/"):
return False
# Allowlist of harmless device files
if p in SAFE_DEVICE_PATHS or p.startswith("/dev/fd/") or p.startswith("/dev/pts/"):
return False
return True
def _device_write_operation(tokens: List[str]) -> bool:
"""Detect potentially destructive writes to block devices via dd/shred or redirection."""
base = _base_command(tokens)
if not base:
return False
# dd writing to block devices
if base == "dd":
for t in tokens:
if t.startswith("of="):
path = t[3:]
if _is_block_device_path(path):
return True
# Also catch redirection like '> /dev/sdX' via naive scan
for i, t in enumerate(tokens[:-1]):
if t in {">", ">>", "2>", "1>"} and _is_block_device_path(tokens[i + 1]):
return True
# shred applied to devices
if base == "shred":
return any(_is_block_device_path(t) for t in tokens if not t.startswith("-"))
return False
def _find_delete_root(tokens: List[str]) -> bool:
base = _base_command(tokens)
if base != "find":
return False
# Consider root-level search paths
has_root_path = any(
t.startswith("/") and not t.startswith("/tmp")
for t in tokens
if not t.startswith("-")
)
if not has_root_path:
return False
if "-delete" in tokens:
return True
# find ... -exec rm ... ;
if "-exec" in tokens and "rm" in tokens:
return True
return False
def _chmod_chown_recursive_root(tokens: List[str]) -> bool:
base = _base_command(tokens)
if base not in {"chmod", "chown", "chgrp"}:
return False
# Collect flag string
try:
base_idx = tokens.index(base)
except ValueError:
return False
flags = "".join(t for t in tokens[base_idx + 1 :] if t.startswith("-"))
recursive = "r" in flags.lower()
if not recursive:
return False
targets = [t for t in tokens[base_idx + 1 :] if not t.startswith("-")]
return any(t == "/" for t in targets)
def _pipe_remote_to_shell(tokens: List[str], raw: str) -> bool:
# Detect common patterns like: curl ... | sh | bash
if "|" in tokens:
try:
pipe_idx = tokens.index("|")
except ValueError:
pipe_idx = -1
if pipe_idx > 0:
left = tokens[:pipe_idx]
right = tokens[pipe_idx + 1 :]
if any(x in left for x in ("curl", "wget", "fetch")) and any(
x in right for x in ("sh", "bash", "zsh")
):
return True
# Detect sh -c "curl ..." style
base = _base_command(tokens)
if (
base in {"sh", "bash", "zsh"}
and "-c" in tokens
and ("curl" in raw or "wget" in raw)
):
return True
return False
def _system_power_command(tokens: List[str]) -> bool:
"""Detect power actions including systemctl reboot/poweroff/halt.
Direct power commands are already banned via BAN_COMMANDS; this extends
coverage to 'systemctl' invocations that perform the same actions.
"""
banned_direct = {"reboot", "poweroff", "halt", "shutdown"}
base = _base_command(tokens)
if base in banned_direct:
return True
if base == "systemctl" and any(x in tokens for x in banned_direct):
return True
return False
def check() -> None:
command = os.environ.get("TOOL_COMMAND", "")
cmd = command.strip()
if not cmd:
return # Nothing to check
cmd_lc, tokens_lc = _parse_lower(cmd)
# 1) Banlist: block certain system-impacting tools unconditionally
banned = _is_banned_command(tokens_lc)
if banned:
_block(f"Command '{banned}' is banned by safety policy.", command)
# 2) Fast explicit substring checks (case-insensitive)
for pat in DANGEROUS_SUBSTRINGS:
if pat in cmd_lc:
_block(f"Dangerous command detected: {pat}", command)
# 3) Heuristic: consolidated rm hazards
if _rm_risky(tokens_lc, cmd_lc):
_block("Risky 'rm' usage detected.", command)
# 4) Device writes (dd/shred to block devices)
if _device_write_operation(tokens_lc):
_block("Potentially destructive write to block device.", command)
# 5) Root-level find -delete / -exec rm
if _find_delete_root(tokens_lc):
_block("Root-level 'find' with delete/rm detected.", command)
# 6) Recursive chmod/chown/chgrp on root
if _chmod_chown_recursive_root(tokens_lc):
_block("Recursive permission/ownership change on '/'.", command)
# 7) Remote script piped into a shell
if _pipe_remote_to_shell(tokens_lc, cmd_lc):
_block("Piping remote script into a shell.", command)
# 8) System power commands (systemctl variants)
if _system_power_command(tokens_lc):
_block("System power/reboot command detected.", command)
if __name__ == "__main__":
check()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment