Claude Code сливает ключи в коммиты вдвое чаще среднего: почему хук PreToolUse их пропускает

В публичных коммитах, сделанных с помощью 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.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.