gendesign/scripts/claude-hooks/check-dangerous-commands.py
bot-backend 4a33347aee chore(claude): security hardening — hooks/settings в git, dangerous-commands hook, restricted DB для reviewer/qa
- .claude/settings.json закоммичен (секретов нет): бот-окна и чистые checkout
  получают hooks/deny/statusLine вместо молчаливого их отсутствия
- новый PreToolUse hook check-dangerous-commands.py: ловит force/amend/no-verify/
  rm -rf/reset --hard/push в main И внутри powershell-обёрток (deny-префиксы позиционны);
  24/24 тест-кейсов; + Read-deny **/.env
- postgres MCP reviewer/qa → --access-mode=restricted (декларируемый read-only
  теперь enforced, не проза)
- auto-analyst: принята throttle-политика ready>=10 (конфликт с work-as-analyst устранён,
  just-in-time нарезка)
- defaultMode:auto перенесён на project-уровень (глобально снят — balance_platform
  вернулся к обычным промптам)
2026-07-02 18:10:26 +03:00

73 lines
3.8 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 команды —
опасная операция внутри вызываемого скрипта не детектится. 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+push\b[^\n]*?\s(-f|--force(?:-with-lease|-if-includes)?)\b",
"git push --force/-f/--force-with-lease запрещён без явного approval юзера"),
(r"git\s+push\b[^\n]*?--no-verify",
"git push --no-verify запрещён (pre-push hooks обязательны)"),
(r"git\s+push\b[^\n]*\s\S+\s+(\S+:)?main\b",
"прямой push в main запрещён — только ветка + PR (git-pr.md)"),
(r"git\s+commit\b[^\n]*?--amend\b",
"git commit --amend запрещён без явного approval юзера"),
(r"git\s+commit\b[^\n]*?(--no-verify\b|\s-n\b)",
"git commit --no-verify/-n запрещён (pre-commit hooks обязательны)"),
(r"git\s+rebase\b[^\n]*?(\s-i\b|--interactive\b)",
"git rebase -i/--interactive запрещён"),
(r"git\s+reset\b[^\n]*?--hard\b",
"git reset --hard запрещён (уничтожает локальные изменения)"),
(r"git\s+clean\b[^\n]*?\s-[a-zA-Z]*f",
"git clean -f* запрещён (удаляет untracked-файлы)"),
(r"git\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 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 ""
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())