- .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 вернулся к обычным промптам)
73 lines
3.8 KiB
Python
73 lines
3.8 KiB
Python
#!/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())
|