56 lines
1.8 KiB
Python
56 lines
1.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Block known-dangerous shell mutations in unattended Claude workers."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import re
|
|
import sys
|
|
from typing import Any
|
|
|
|
|
|
DENIED_COMMANDS: tuple[tuple[str, re.Pattern[str]], ...] = (
|
|
("force-push is prohibited", re.compile(r"(?:^|[;&|]\s*)git\s+push\b[^\n]*(?:--force(?:-with-lease)?|(?:^|\s)-f(?:\s|$))", re.I)),
|
|
("destructive Git reset is prohibited", re.compile(r"(?:^|[;&|]\s*)git\s+reset\b[^\n]*--hard\b", re.I)),
|
|
("destructive Git clean is prohibited", re.compile(r"(?:^|[;&|]\s*)git\s+clean\b[^\n]*(?:-[a-z]*f[a-z]*|--force)\b", re.I)),
|
|
)
|
|
|
|
|
|
def command_from_payload(payload: dict[str, Any]) -> str:
|
|
"""Extract the Bash command from a Claude PreToolUse hook payload."""
|
|
tool_input = payload.get("tool_input")
|
|
if not isinstance(tool_input, dict):
|
|
return ""
|
|
value = tool_input.get("command")
|
|
return value if isinstance(value, str) else ""
|
|
|
|
|
|
def denial_reason(command: str) -> str | None:
|
|
"""Return a concise block reason for a denied command."""
|
|
for reason, pattern in DENIED_COMMANDS:
|
|
if pattern.search(command):
|
|
return reason
|
|
return None
|
|
|
|
|
|
def main() -> int:
|
|
"""Apply the policy to one Claude hook event."""
|
|
try:
|
|
payload = json.load(sys.stdin)
|
|
except (json.JSONDecodeError, OSError):
|
|
print("Claude command policy received invalid JSON", file=sys.stderr)
|
|
return 2
|
|
if not isinstance(payload, dict):
|
|
print("Claude command policy received an invalid event", file=sys.stderr)
|
|
return 2
|
|
command = command_from_payload(payload)
|
|
reason = denial_reason(command)
|
|
if reason:
|
|
print(f"Blocked by the Hermes owner-workspace policy: {reason}.", file=sys.stderr)
|
|
return 2
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|