В публичных коммитах, сделанных с помощью 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()
Шесть шагов вызова через хук: от решения агента до exit 0 или exit 2 с причиной в stderr
Упавший хук для агента выглядит так же, как нарушенное правило.

Команда ./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; }
Где искать ключ перед коммитом: в индексе, в правках против HEAD и новых файлах, в неотправленных коммитах; упавшая проверка блокирует вызов
Ключ, стёртый с диска после git add, всё равно уезжает в коммит.

Тот же отчёт GitGuardian нашёл больше двадцати тысяч секретов в публичных конфигах MCP. Конфиги MCP с ключами держите в .gitignore и отдавайте сканеру отдельно.

Какие действия агента проверять хуком, а какие оставить правилом в CLAUDE.md

Хуки у меня стоят на всех ИИ-агентах для программирования, и правило очень простое: если откат ошибки дороже пяти минут разговора с агентом, правило проверяет код. Стиль, тон и порядок работы остаются правилами в CLAUDE.md → такую ошибку агент исправит одним ответом.

  1. Начните с двух правил: ключи перед commit и push (в паре с родными git-хуками, о них ниже), проверка перед выкладкой.

  2. Режьте составную команду на части и передавайте в проверку аргументы своей части.

  3. Сбой и таймаут хука блокируют: таймаут проверки меньше таймаута хука, любое исключение — код 2.

  4. На каждое правило держите сценарии регресса: команды на пропуск и на блок. У меня их несколько десятков, новый обычно появляется после поломки.

  5. Файл с правилами хуков агент не должен править сам, нужен хук на Edit|Write по пути файла.

Пятый пункт я проверил на себе: регулярка в правилах блокировала лишнее, и Claude Code не дал агенту без моего подтверждения поправить файл с правилами хуков. Правила разрешений ведут себя не так, как ждёшь (issue #27333). На составных командах они не срабатывают, поэтому на dev.to проверку тоже переносят в хук PreToolUse (dev.to).

Что ещё очень важно — хук видит команду, а не намерение, и git -C другая/папка push или вызов через свой скрипт не поймает. Ключи надёжнее ловить родными git-хуками pre-commit и pre-push, они срабатывают уже после git add, а хуком агента запретить --no-verify.