В публичных коммитах, сделанных с помощью Claude Code, секреты встречаются в 3,2% случаев, а в среднем по GitHub — в 1,5% (GitGuardian). Кто вписал ключ, агент или человек, по этим данным не видно. Запретить это строкой в CLAUDE.md мало: на длинной сессии агент правило теряет, поэтому ключи я проверяю хуком PreToolUse перед каждым вызовом инструмента. Но простой хук ключ пропустит: он срабатывает до git add и видит пустой индекс, а упав с ошибкой, не блокирует ничего.
CLAUDE.md не гарантирует, что Claude выполнит правило
По документации Claude Code, CLAUDE.md — это инструкции человека, которые подгружаются в каждую сессию. Гарантии исполнения там нет: правило лежит в том же контекстном окне, что и вся переписка. А факт из середины длинного контекста языковая модель находит хуже (Liu et al.). Правило из начала сессии к середине работы попадает как раз туда. У меня агент так однажды нарушил прямой запрет из CLAUDE.md и ради одной цифры для статьи запустил платную серию запросов к API.
Агент Replit летом 2025 года нарушил прямой запрет заказчика на изменения и удалил рабочую базу с данными больше тысячи компаний (AIID).
Как написать хук Claude Code, который остановит команду агента, даже если сам упадёт
Claude Code hooks — это скрипты, которые Claude Code запускает на событиях сессии. Перед вызовом инструмента событие PreToolUse приходит на stdin JSON-объектом с полями tool_name и tool_input. Код выхода 2 блокирует вызов, текст из stderr агент получает как причину отказа; второй способ — выйти с 0 и вернуть JSON с permissionDecision: deny. (документация).
Хук подключается в .claude/settings.json на PreToolUse с matcher: "Bash" и таймаутом 120 секунд. Движок один, правила лежат в JSON: какую команду ловить, какой проверкой, с какой причиной отказа.
import json, os, shlex, subprocess, sys ROOT = os.environ.get("CLAUDE_PROJECT_DIR", ".") def segments(command): # && || ; | и перевод строки, кавычки учтены lex = shlex.shlex(command, posix=True, punctuation_chars=";&|\n") lex.whitespace, lex.whitespace_split = " \t\r", True seg = [] for tok in [*lex, "\n"]: if set(tok) <= set(";&|\n"): while seg and "=" in seg[0]: # VAR=x git push seg.pop(0) yield seg seg = [] else: seg.append(tok) def check(payload): rules = json.load(open(os.path.join(ROOT, ".claude/rules.json"))) for seg in segments(payload["tool_input"]["command"]): for rule in rules: prefix = rule["command"].split() head = [os.path.basename(t) for t in seg[:1]] + seg[1:len(prefix)] if head != prefix: continue # команду упомянули, но не запускают args = seg[len(prefix):] # аргументы именно этой части done = subprocess.run(shlex.split(rule["require"]) + args, cwd=ROOT, capture_output=True, timeout=100) if done.returncode: return f"{rule['reason']}: {shlex.join(seg)}" def main(): try: reason = check(json.load(sys.stdin)) except Exception as e: # сбой, таймаут, кривой вход reason = f"hook error: {e}" if reason: print(reason, file=sys.stderr) sys.exit(2) if __name__ == "__main__": main()

Команда ./deploy.sh хорошая.md && ./deploy.sh плохая.md даёт две части, и проверка идёт дважды, каждая со своим файлом; поиск по подстроке пропустил бы её целиком. Слова git push в сообщении коммита правило не тронут. Непойманное исключение в Python даёт код 1, а любой код, кроме 2, Claude Code считает неблокирующим, поэтому хук ловит всё, включая таймаут проверки, и выходит с 2. В моём движке отказ идёт JSON-ответом deny, код 2 — для сбоев; в примере для краткости всё идёт через код 2.
Почему хук не замечает ключ API, если агент добавляет файл и коммитит одной командой
Для безопасности ИИ-агентов главный рубеж — коммит. Первая версия поиска ключей перед git commit и git push сканировала файлы на диске. Ревью кода нашло в ней два пропуска: ключ можно добавить в индекс, стереть с диска и закоммитить, а неотправленные коммиты перед push она не смотрела вовсе.
Есть и третья ловушка: хук срабатывает до команды. Агенты часто коммитят одной командой git add -A && git commit или git commit -a, и в момент проверки индекс ещё пуст. Проверка перед коммитом должна смотреть индекс, правки против HEAD и новые файлы, перед push — все коммиты, которых нет на удалённом репозитории. Я пришёл к такой версии:
#!/usr/bin/env bash # scan_secrets.sh commit|push; код 2 — ключ или сбой git set -uo pipefail added() { "$@" | sed -n '/^+++ /d; s/^+//p'; } # только добавленные строки changes() { if [ "$1" = commit ]; then added git diff --cached --no-color --no-ext-diff || return 1 if git rev-parse -q --verify HEAD >/dev/null; then # в пустом репозитории HEAD нет added git diff HEAD --no-color --no-ext-diff || return 1 fi git ls-files -o --exclude-standard -z | while IFS= read -r -d '' f; do cat -- "$f"; done else added git log -p --no-color -m --branches --not --remotes fi } out=$(mktemp) || exit 2 changes "$1" > "$out" || { rm -f "$out"; echo "git не ответил" >&2; exit 2; } # шаблон упрощён, в работе возьмите gitleaks или trufflehog grep -qaE '(^|[^A-Za-z0-9_])(sk-|ghp_|AKIA)[A-Za-z0-9_-]{16,}' "$out"; rc=$?; rm -f "$out" [ "$rc" = 1 ] || { echo "похоже на ключ" >&2; exit 2; }

Тот же отчёт GitGuardian нашёл больше двадцати тысяч секретов в публичных конфигах MCP. Конфиги MCP с ключами держите в .gitignore и отдавайте сканеру отдельно.
Какие действия агента проверять хуком, а какие оставить правилом в CLAUDE.md
Хуки у меня стоят на всех ИИ-агентах для программирования, и правило очень простое: если откат ошибки дороже пяти минут разговора с агентом, правило проверяет код. Стиль, тон и порядок работы остаются правилами в CLAUDE.md → такую ошибку агент исправит одним ответом.
Начните с двух правил: ключи перед commit и push (в паре с родными git-хуками, о них ниже), проверка перед выкладкой.
Режьте составную команду на части и передавайте в проверку аргументы своей части.
Сбой и таймаут хука блокируют: таймаут проверки меньше таймаута хука, любое исключение — код 2.
На каждое правило держите сценарии регресса: команды на пропуск и на блок. У меня их несколько десятков, новый обычно появляется после поломки.
Файл с правилами хуков агент не должен править сам, нужен хук на Edit|Write по пути файла.
Пятый пункт я проверил на себе: регулярка в правилах блокировала лишнее, и Claude Code не дал агенту без моего подтверждения поправить файл с правилами хуков. Правила разрешений ведут себя не так, как ждёшь (issue #27333). На составных командах они не срабатывают, поэтому на dev.to проверку тоже переносят в хук PreToolUse (dev.to).
Что ещё очень важно — хук видит команду, а не намерение, и git -C другая/папка push или вызов через свой скрипт не поймает. Ключи надёжнее ловить родными git-хуками pre-commit и pre-push, они срабатывают уже после git add, а хуком агента запретить --no-verify.
