gendesign/scripts/claude-hooks/check-dangerous-commands.py

90 lines
4.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""PreToolUse hook: блокирует запрещённые git/fs/docker команды, включая shell-обёртки.
Deny-префиксы в settings.json позиционны и обходятся обёртками
(`powershell -c "git push --force"` не матчится ни одним deny-паттерном).
Этот hook regex-сканирует ВСЮ строку команды Bash-тула, поэтому ловит и пейлоады
внутри powershell/pwsh/bash -c.
Ограничение (как у check-secret-read.py): видит только текст top-level команды —
опасная операция внутри вызываемого скрипта не детектится. Known-bypass (осознанно):
long-form `rm --recursive --force`, PowerShell-алиасы ri/rd/del, сокращения -Rec.
Fail-open по дизайну: это tripwire-слой поверх deny-правил и review, а не
единственная защита.
Протокол: exit 0 = allow, exit 2 + stderr = block (сообщение уходит Claude).
"""
import json
import re
import sys
# (pattern, объяснение). Все паттерны применяются к полной строке команды.
PATTERNS: list[tuple[str, str]] = [
(r"git\s+(?:-C\s+\S+\s+)?push\b[^\n]*?\s(-f|--force(?:-with-lease|-if-includes)?)\b",
"git push --force/-f/--force-with-lease запрещён без явного approval юзера"),
(r"git\s+(?:-C\s+\S+\s+)?push\b[^\n]*?--no-verify",
"git push --no-verify запрещён (pre-push hooks обязательны)"),
(r"git\s+(?:-C\s+\S+\s+)?push\b[^\n]*\s\S+\s+(\S+:)?main(?![\w/-])",
"прямой push в main запрещён — только ветка + PR (git-pr.md)"),
(r"git\s+(?:-C\s+\S+\s+)?commit\b[^\n]*?--amend\b",
"git commit --amend запрещён без явного approval юзера"),
(r"git\s+(?:-C\s+\S+\s+)?commit\b[^\n]*?(--no-verify\b|\s-n\b)",
"git commit --no-verify/-n запрещён (pre-commit hooks обязательны)"),
(r"git\s+(?:-C\s+\S+\s+)?rebase\b[^\n]*?(\s-i\b|--interactive\b)",
"git rebase -i/--interactive запрещён"),
(r"git\s+(?:-C\s+\S+\s+)?reset\b[^\n]*?--hard\b",
"git reset --hard запрещён (уничтожает локальные изменения)"),
(r"git\s+(?:-C\s+\S+\s+)?clean\b[^\n]*?\s-[a-zA-Z]*f",
"git clean -f* запрещён (удаляет untracked-файлы)"),
(r"git\s+(?:-C\s+\S+\s+)?checkout\b[^\n]*?\s-f\b",
"git checkout -f запрещён (теряет незакоммиченные изменения)"),
(r"\brm\b(?=[^\n|;&]*\s-[a-zA-Z]*r)(?=[^\n|;&]*\s-[a-zA-Z]*f)",
"rm -rf (и вариации -fr / -r -f) запрещён"),
(r"(?i)remove-item\b(?=[^\n]*-recurse)(?=[^\n]*-force)",
"Remove-Item -Recurse -Force запрещён"),
(r"(?i)docker(?:-|\s+)compose\s+down\b[^\n]*(\s-v\b|--volumes\b)",
"docker compose down -v удаляет volumes"),
(r"docker\s+volume\s+rm\b",
"docker volume rm запрещён"),
]
def strip_commit_messages(cmd: str) -> str:
"""Вырезает содержимое -m/--message аргументов git commit.
Иначе false-positive: `git commit -m "запретили rm -rf"` блокируется из-за
текста сообщения. Пейлоады обёрток (powershell -c "git push --force") не
страдают — они не привязаны к -m.
"""
return re.sub(
r"(\s(?:-m|--message))\s+(\"(?:[^\"\\]|\\.)*\"|'[^']*')",
r"\1 MSG",
cmd,
)
def main() -> int:
try:
data = json.load(sys.stdin)
if data.get("tool_name") != "Bash":
return 0
cmd = (data.get("tool_input") or {}).get("command") or ""
cmd = strip_commit_messages(cmd)
for pattern, reason in PATTERNS:
if re.search(pattern, cmd):
print(
f"BLOCKED (check-dangerous-commands): {reason}. "
"Правило CLAUDE.md #5 / git-pr.md § Запреты. Если операция реально нужна — "
"попроси юзера выполнить её самому или дать явное разрешение; "
"не пытайся обойти через обёртки/скрипты.",
file=sys.stderr,
)
return 2
return 0
except Exception:
return 0 # fail-open: tripwire-слой, не единственная защита
if __name__ == "__main__":
sys.exit(main())