Newer
Older
openstack-caracal-ipv4 / .claude / hooks / guard-destructive.py
#!/usr/bin/env python3
"""
.claude/hooks/guard-destructive.py -- PreToolUse belt-and-suspenders for the
jumphost (2026-07-03). Settings deny/ask rules are the first line; this hook
exists because (a) a hook exit-2 blocks BEFORE permission evaluation in every
permission mode, and (b) Bash settings-rule enforcement has a documented
reliability history upstream. Blocks the NEVER class and secret-file shell
reads that Read() rules cannot see (arbitrary subprocess reads).

stdin: PreToolUse JSON. exit 0 = no opinion (permission rules proceed);
exit 2 = hard block (stderr shown to Claude). ASCII + LF.
Offline test: tests/claude-guard/run-tests.sh.
"""
import json
import re
import sys

NEVER = [
    (r"vault\s+operator\s+(init|rekey|generate-root)",
     "one-shot vault operation: operator-only, from the runbook, VERBATIM (DOCFIX-006/D-069)"),
    (r"juju\s+destroy-controller",
     "controller destruction is out of scope for any session on this host"),
    (r"\bmaas\s+list\b",
     "prints the MAAS API key (DOCFIX-016); use 'maas admin ...' directly"),
    (r"git\s+push\s+(--force|-f)\b",
     "force-push is banned on this repo"),
    (r"(cat|less|more|head|tail|cp|scp|base64|xxd|od|strings)\b[^|;&]*"
     r"(vault-init/|as-executed/|-cred\.txt|appcred)",
     "secret-adjacent file: never read key/cred material into context (whitelist-print rule)"),
    (r"rm\s+-rf\s+(/|~)\s*$",
     "catastrophic rm"),
]


def main():
    try:
        data = json.load(sys.stdin)
    except Exception:
        return 0  # malformed input: no opinion; permission rules still apply
    cmd = (data.get("tool_input") or {}).get("command", "") or ""
    for rx, why in NEVER:
        if re.search(rx, cmd):
            sys.stderr.write(
                "BLOCKED by .claude/hooks/guard-destructive.py: %s\n" % why)
            return 2
    return 0


if __name__ == "__main__":
    sys.exit(main())