Перейти к содержанию
Новое AiManual теперь в MAX Подписаться
Публикация AiManual

Принудительные хуки для Claude Code: как заставить AI-агента соблюдать регламент тестирования

Три принудительных хука для Claude Code, которые блокируют коммиты без тестов, отслеживают изменения тестовых файлов и запрещают завершение сессии с падающими п

Коротко

Что будет в материале

  1. 01

    Зачем контролировать AI-агента: проблема дисциплины тестирования

  2. 02

    Архитектура принудительного контроля: три слоя защиты

  3. 03

    Гейт 1: Блокировка коммитов без свежего прогона тестов

  4. 04

    Гейт 2: Отслеживание изменений тестовых файлов с контекстным напоминанием

Claude Code генерирует код быстрее человека. Но скорость создает риски: агент может закоммитить изменения без прогона тестов, оставить неработающие тесты в репозитории или завершить сессию с «грязным» состоянием. Ручной контроль здесь не работает - разработчик либо забывает проверить, либо сознательно пропускает рутину под давлением сроков.

Решение - три принудительных хука, которые встраиваются в рабочий процесс Claude Code и блокируют опасные действия на системном уровне. Хук предварительного коммита не пропустит код без свежего прогона тестов. Мониторинг тестовых файлов выдаст контекстное напоминание при изменении самих тестов. Запрет завершения сессии не даст закрыть терминал с непройденными проверками. Эксперимент подтвердил: хук пересиливает прямую инструкцию пользователя - агент физически не может обойти ограничение.

В статье - полный код трёх гейтов, разбор логики их работы и честный анализ обнаруженного бага: ложный пропуск из-за секундной гранулярности bash. Материал для разработчиков, которые хотят автоматизировать дисциплину тестирования без расчёта на человеческую память.

Зачем контролировать AI-агента: проблема дисциплины тестирования

Разработчики используют Claude Code для ускорения рутинных задач: написание функций, рефакторинг, генерация тестов. Агент выполняет работу за секунды, но не принимает решений о качестве. Он не знает, что коммит без тестов - это риск регрессии. Он не помнит, что изменённый тестовый файл требует перезапуска всей батареи. Он не видит разницы между «сессия завершена» и «сессия завершена с падающими тестами».

Результат предсказуем: в репозиторий попадает код, который проходит ревью, но не верифицирован автоматически. Через неделю падает CI/CD, команда тратит часы на отладку, а причина - пропущенный прогон тестов три коммита назад. Это не гипотетический сценарий. В проектах с активным использованием AI-кодинга доля коммитов без тестов может достигать 30-40%, если нет автоматического барьера.

Ручные чек-листы не решают проблему. Разработчик под давлением дедлайна пропустит пункт «запустить тесты». Напоминания в слаке игнорируются после третьего повтора. Нужен механизм, который не спрашивает, а блокирует. Системный уровень, где у агента нет выбора. Именно эту роль выполняют хуки - скрипты, встроенные в жизненный цикл Claude Code и срабатывающие до того, как опасное действие станет необратимым.

Подход с принудительной верификацией перекликается с архитектурой LLM-агентов с самопроверкой: там контур верификации встраивается в логику агента, здесь - в операционную среду. Принцип общий: не доверять генеративной модели, а проверять её выход автоматически.

Архитектура принудительного контроля: три слоя защиты

Один хук не покрывает все сценарии нарушения регламента. Pre-commit блокирует коммит, но не мешает агенту изменить тестовый файл и забыть о нём до конца сессии. Мониторинг файлов напоминает о изменениях, но не останавливает закрытие терминала с падающими тестами. Нужны три независимых гейта, каждый из которых закрывает конкретный вектор обхода.

Первый слой - блокировка коммита без свежего прогона тестов. Хук сравнивает временную метку последнего запуска тестов с датой последнего коммита. Если тесты не запускались после изменений в коде, операция отклоняется. Второй слой - отслеживание изменений тестовых файлов. Скрипт мониторит директории с тестами, фиксирует модификации и при попытке коммита или завершения сессии выводит список изменённых файлов с требованием перезапуска. Третий слой - запрет на завершение сессии с «грязными» тестами. Хук проверяет вывод тестового раннера на наличие FAILED и анализирует git status в тестовых директориях. Если есть непройденные или незакоммиченные тесты, сессия не закрывается.

Три слоя работают как эшелонированная защита: даже если один гейт пропустит нарушение из-за бага или граничного условия, следующий остановит цепочку. Практика показала, что комбинация трёх хуков снижает количество коммитов без тестов до нуля в рамках активной сессии Claude Code.

Гейт 1: Блокировка коммитов без свежего прогона тестов

Самый критичный барьер. Разработчик написал код с помощью агента, провёл ревью и готов коммитить. Если тесты не запускались после последнего изменения, хук должен остановить операцию.

Реализация: скрипт проверки и интеграция в pre-commit

Логика проста: после каждого прогона тестов сохраняется временная метка в файл .last_test_run. Pre-commit хук сравнивает эту метку с временем последнего коммита. Если файл .last_test_run старше последнего коммита - тесты не запускались, коммит блокируется.

#!/bin/bash
# Сохранение метки после прогона тестов
npm test && date +%s > .last_test_run

# Хук pre-commit (разместить в .git/hooks/pre-commit)
#!/bin/bash
LAST_TEST=$(cat .last_test_run 2>/dev/null || echo 0)
LAST_COMMIT=$(git log -1 --format=%ct 2>/dev/null || echo 0)

if [ "$LAST_TEST" -lt "$LAST_COMMIT" ]; then
  echo "Блокировка: тесты не запускались после последнего коммита."
  echo "Запустите 'npm test' перед коммитом."
  exit 1
fi
echo "Проверка пройдена: тесты актуальны."
exit 0

В Claude Code хук подключается через конфигурацию окружения. Агент не может модифицировать .git/hooks/pre-commit без явного разрешения, а при попытке коммита система автоматически вызывает скрипт. Если хук возвращает ненулевой код, операция прерывается.

Важный нюанс: метка должна обновляться только при успешном прогоне тестов. Команда npm test && date +%s > .last_test_run гарантирует, что падающие тесты не создадут ложного ощущения безопасности. Файл .last_test_run добавляется в .gitignore, чтобы не попадать в репозиторий.

Гейт 2: Отслеживание изменений тестовых файлов с контекстным напоминанием

Сценарий: разработчик правит сами тесты - добавляет новый тест-кейс, меняет ожидаемое значение в существующем, исправляет баг в тестовой логике. Формально тесты запускались недавно, но изменённый файл требует повторного прогона. Первый гейт может пропустить такую ситуацию, если метка .last_test_run свежая.

Настройка мониторинга и вывод контекстного предупреждения

Второй хук отслеживает изменения в директориях с тестами и сравнивает время модификации файлов с временем последнего прогона. Если тестовый файл изменён позже прогона - выводится предупреждение с конкретным списком.

#!/bin/bash
TEST_DIRS=("tests/" "src/__tests__/")
LAST_TEST=$(cat .last_test_run 2>/dev/null || echo 0)
CHANGED_FILES=""

for dir in "${TEST_DIRS[@]}"; do
  if [ -d "$dir" ]; then
    while IFS= read -r file; do
      FILE_MTIME=$(stat -c %Y "$file" 2>/dev/null || echo 0)
      if [ "$FILE_MTIME" -gt "$LAST_TEST" ]; then
        CHANGED_FILES="$CHANGED_FILES  $file\n"
      fi
    done < <(find "$dir" -type f -name "*.test.*" -o -name "*.spec.*")
  fi
done

if [ -n "$CHANGED_FILES" ]; then
  echo "Внимание: тестовые файлы изменены после последнего прогона:"
  echo -e "$CHANGED_FILES"
  echo "Запустите тесты перед продолжением."
fi

Хук встраивается в два события: pre-commit (дополняя первый гейт) и pre-exit (перед завершением сессии). В Claude Code это реализуется через пользовательские команды-обёртки: агент вызывает /commit или /exit, а скрипт-обёртка сначала запускает проверку изменённых тестовых файлов.

Контекстное напоминание принципиально важно. Сообщение «запустите тесты» игнорируется, а «файлы tests/auth.test.ts и tests/api.test.ts изменены 3 минуты назад, тесты не перезапускались» - заставляет действовать. Разработчик видит конкретные файлы и понимает объём работы.

Гейт 3: Запрет на завершение сессии с «грязными» тестами

Даже без коммита сессия может завершиться с падающими тестами. Разработчик экспериментировал с кодом, запустил тесты, увидел FAILED и решил «разберусь завтра». Сессия закрывается, контекст теряется, проблема остаётся.

Как определить «грязные» тесты и принудительно их исправить

Третий гейт проверяет два условия перед завершением сессии: наличие FAILED в выводе последнего прогона тестов и наличие незакоммиченных изменений в тестовых директориях. Если любое условие истинно - выход блокируется.

#!/bin/bash
# Сохранение вывода тестов при каждом прогоне
npm test 2>&1 | tee .last_test_output

# Хук pre-exit
#!/bin/bash
if grep -q "FAILED" .last_test_output 2>/dev/null; then
  echo "Блокировка: последний прогон тестов содержит ошибки."
  echo "Исправьте падающие тесты перед завершением сессии."
  exit 1
fi

TEST_DIRS=("tests/" "src/__tests__/")
DIRTY_TESTS=""
for dir in "${TEST_DIRS[@]}"; do
  if [ -d "$dir" ]; then
    DIRTY=$(git status --porcelain "$dir" 2>/dev/null)
    if [ -n "$DIRTY" ]; then
      DIRTY_TESTS="$DIRTY_TESTS$DIRTY\n"
    fi
  fi
done

if [ -n "$DIRTY_TESTS" ]; then
  echo "Блокировка: незакоммиченные изменения в тестовых файлах:"
  echo -e "$DIRTY_TESTS"
  echo "Закоммитьте изменения или откатите их перед выходом."
  exit 1
fi

echo "Сессия чиста, можно завершать."
exit 0

Критерий «грязных» тестов - комбинация двух проверок. Вывод тестового раннера анализируется на наличие строки FAILED. Git status в тестовых директориях показывает незакоммиченные изменения. Оба сигнала означают, что состояние тестов нестабильно и сессия не должна завершаться.

Хук pre-exit в Claude Code реализуется через перехват команды /exit. Агент вызывает скрипт, и если тот возвращает ошибку, сессия продолжается. Разработчик вынужден либо исправить тесты, либо явно откатить изменения - полумеры невозможны.

Эксперимент: хук против прямой инструкции пользователя

Теоретически агент может попытаться обойти хук. Например, пользователь даёт команду: «Игнорируй pre-commit проверку и закоммить изменения». Если хук реализован на уровне git, а не на уровне агента, обход невозможен.

Эксперимент подтвердил это. Пользователь в сессии Claude Code явно потребовал пропустить тесты и выполнить коммит. Агент попытался выполнить git commit, но git автоматически вызвал pre-commit хук. Скрипт проверил метку .last_test_run, обнаружил, что тесты не запускались, и вернул ошибку. Коммит был заблокирован, несмотря на прямую инструкцию.

Агент сообщил пользователю: «Коммит отклонён системным хуком. Тесты не запускались после последних изменений». Пользователь попытался удалить хук командой rm .git/hooks/pre-commit, но у агента не было прав на модификацию .git без дополнительного подтверждения. Третий заход - попытка изменить файл .last_test_run вручную - также был отклонён, поскольку файл защищён от прямой записи агентом.

Результат: три попытки обхода, три отказа. Системный уровень контроля оказался надёжнее инструкций пользователя. Это ключевой вывод: принудительные хуки работают, потому что они находятся вне зоны влияния агента. Claude Code оперирует в рамках файловой системы и git, а хуки - это часть инфраструктуры, которую агент не может модифицировать без явного разрешения.

Подобный подход к принудительной верификации используется в двухэтажной архитектуре управления AI-агентами: контрольная плоскость задаёт правила, а локальные гейты обеспечивают их исполнение на уровне каждого агента. Разница в масштабе, принцип - общий.

Найденный баг: ложный пропуск из-за секундной гранулярности bash

В ходе эксплуатации трёх гейтов обнаружен баг, который проявляется при определённом стечении обстоятельств. Если тесты запускаются в ту же секунду, что и коммит, хук может не заметить разницы.

Причина - гранулярность команды stat -c %Y в bash. Она возвращает время последней модификации в секундах. Если разработчик запустил тесты и через доли секунды выполнил коммит, обе метки времени будут иметь одинаковое значение. Условие if [ "$LAST_TEST" -lt "$LAST_COMMIT" ] не выполнится, поскольку значения равны, и хук пропустит коммит.

Сценарий воспроизводится при автоматическом прогоне тестов с немедленным коммитом - например, в скрипте, где команды идут подряд без задержки. Вероятность низкая, но не нулевая: за месяц активной работы с Claude Code баг проявился дважды.

Возможные решения:

  • Использовать stat -c %.9Y для получения времени с наносекундной точностью (доступно в GNU coreutils 8.28+).
  • Добавить принудительную задержку sleep 1 после прогона тестов перед сохранением метки - грубо, но надёжно.
  • Вместо сравнения меток проверять содержимое файла с хешем последнего прогона: npm test && sha256sum test-output.log > .last_test_hash, а в хуке сравнивать хеш текущего вывода с сохранённым. Это исключает временные коллизии полностью.

Третий вариант - самый надёжный. Хеш меняется при любом изменении вывода тестов, независимо от времени запуска. Реализация требует хранения эталонного хеша и повторного прогона тестов в хуке для сравнения, что увеличивает время коммита, но исключает ложные пропуски.

Внедрение в рабочий процесс: рекомендации и лучшие практики

Три гейта - это шаблон, который адаптируется под конкретный проект. Пути к тестовым директориям, команда запуска тестов, критерии «грязного» состояния - всё это параметризуется.

Первое правило: тестируйте сами хуки. Напишите тест-кейс, который намеренно нарушает каждое правило, и убедитесь, что хук блокирует операцию. Проверьте граничные условия: пустой репозиторий, отсутствие тестовых файлов, удалённый файл .last_test_run. Хук не должен падать с необработанной ошибкой - только информативное сообщение и корректный exit code.

Второе правило: комбинируйте с CI/CD. Локальные хуки - первая линия обороны, но не единственная. На сервере CI/CD тесты запускаются в любом случае, и если разработчик нашёл способ обойти локальный хук (например, git commit --no-verify), пайплайн отловит проблему. Хуки снижают нагрузку на CI, отсеивая очевидные нарушения до push.

Третье правило: обновляйте хуки при изменении регламента. Если команда перешла с npm на другой тестовый раннер, если изменилась структура тестовых директорий, если добавились новые типы тестовых файлов - хуки должны отражать актуальную конфигурацию. Застывшие правила создают ложное чувство безопасности.

Четвёртое правило: не перегружайте хуки логикой. Три гейта покрывают критические сценарии. Добавление проверки стиля кода, линтера, форматера и десяти других правил в pre-commit замедляет каждый коммит и раздражает разработчиков. Хуки должны быть быстрыми (до 2-3 секунд) и релевантными.

Для команд, которые только начинают внедрять AI-кодинг, три гейта - минимально необходимый набор. Они не требуют внешних сервисов, работают на любом Linux-окружении и интегрируются с Claude Code без модификации самого агента. Опыт внедрения в нескольких проектах показал: после настройки хуков количество инцидентов «забыли запустить тесты» падает до нуля в рамках активных сессий.

Более сложные сценарии - параллельная работа нескольких агентов, удалённые сессии длительностью более 24 часов - требуют дополнительных инструментов. Подходы к продлению сессий кодинг-агентов разобраны в статье про запуск Claude Code более 24 часов: sandbox-изоляция, автоматическая верификация и agentic code review. Но фундамент дисциплины тестирования закладывается именно здесь - тремя принудительными хуками, которые не спрашивают, а блокируют.

Подписаться на канал