Stop Hook в Claude Code: как проверить тесты до сообщения «готово»

Настройте короткую проверку завершения Claude Code: тесты, Git-статус или ожидаемый файл, понятный exit code и безопасные логи.

Сообщение «готово» в Claude Code означает, что агент решил завершить ход. Оно не доказывает, что тесты действительно запускались, рабочее дерево чистое или ожидаемый файл появился. Stop Hook позволяет запускать короткую локальную команду перед завершением и вернуть агенту понятный код ошибки.

Хороший hook отвечает на один проверяемый вопрос: «после этого хода есть ли файл dist/app.js и проходит ли быстрая проверка?». Он не заменяет CI, полноценный набор тестов или ручное решение о публикации.

Чем Stop Hook отличается от CLAUDE.md и CI

  • Stop Hook запускает команду в момент завершения ответа. Он подходит для быстрой проверки факта: git diff --check, существования файла или короткого тестового набора.
  • CLAUDE.md описывает правило для работы агента: что менять нельзя, какую команду запускать и какой результат считать успешным. Сам файл ничего не проверяет.
  • CI запускается в отдельной среде после push или pull request и остаётся обязательной границей для команды. Hook не должен заменять эту проверку.

Если проверка меняет внешнюю систему, делает deploy или отправляет сообщение, вынесите её из Stop Hook. При каждом завершении такой side effect будет трудно повторять и откатывать.

Опишите один проверяемый результат

Перед настройкой hook запишите четыре строки:

  1. Claim: что агент должен утверждать после завершения, например «сборка создана».
  2. Проверка: команда или файл, которыми это можно подтвердить.
  3. Успех: exit code 0.
  4. Блокировка Stop: exit code 2 и короткая причина в stderr без секретов и полного лога.

Для небольшого Node-проекта контракт может выглядеть так:

Claim: сборка готова Проверка: npm test -- --runInBand и test -s dist/app.js Успех: обе команды завершились с кодом 0 Сбой: показать имя проверки в stderr и завершить hook с кодом 2

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

Минимальный Stop Hook

Claude Code хранит проектные настройки в .claude/settings.json. Добавьте один Stop hook с ограничением времени и отдельным скриптом:

{ "hooks": { "Stop": [ { "hooks": [ { "type": "command", "command": "./scripts/check-before-stop.sh", "timeout": 30 } ] } ] } }

Скрипт должен быть коротким и предсказуемым:

#!/usr/bin/env sh set -eu if [ -t 0 ]; then input='{"stop_hook_active":false}' else input=$(cat) fi stop_hook_active=$(printf '%s' "$input" | node -e ' let raw = ""; process.stdin.on("data", chunk => raw += chunk); process.stdin.on("end", () => { try { process.stdout.write(String(Boolean(JSON.parse(raw).stop_hook_active))); } catch { process.stdout.write("false"); } });') block_stop() { if [ "$stop_hook_active" = "true" ]; then printf '%s\n' "{\"systemMessage\":\"Stop check still fails: $block_reason. Run the check manually; CI remains required.\"}" exit 0 fi printf '%s\n' "Stop check blocked: $block_reason" >&2 exit 2 } if ! npm test -- --runInBand >/dev/null 2>&1; then block_reason='run npm test and fix the failing test' block_stop fi if ! test -s dist/app.js; then block_reason='rebuild dist/app.js' block_stop fi printf '%s\n' 'Stop check passed: tests and dist/app.js' exit 0

Сделайте файл исполняемым (chmod +x scripts/check-before-stop.sh) и проверьте его отдельно. В реальном проекте замените npm test и путь к файлу на команды из репозитория. Не добавляйте в вывод переменные окружения, API Key, содержимое .env или полный ответ тестов.

Здесь важен именно код 2. По актуальной документации Claude Code, exit 2 для события Stop запрещает завершение хода, а строка из stderr объясняет Claude, что нужно исправить. Обычный exit 1 считается неблокирующей ошибкой: интерфейс покажет ошибку hook, но Claude сможет остановиться. Timeout тоже не блокирует Stop, поэтому короткая проверка должна укладываться в указанный лимит.

После первой блокировки следующий Stop получает во входном JSON поле stop_hook_active: true. Пример выше даёт Claude одну попытку исправления. Если та же проверка снова не проходит, hook возвращает 0 с systemMessage для оператора и завершает цикл; CI остаётся обязательной границей. Это сознательный fail-open после одной попытки, а не гарантия качества. Для строгого проекта вместо него можно снова вернуть 2, но тогда нужно следить за повтором: Claude Code принудительно завершает ход после восьми последовательных блокировок.

Проверьте полный цикл, а не только скрипт

  1. Временно замените dist/app.js на dist/missing.js и выполните printf '%s\n' '{"stop_hook_active":false}' | ./scripts/check-before-stop.sh; echo $?. Ожидаются строка Stop check blocked в stderr и код 2.
  2. Верните правильный путь, запустите сборку и повторите ту же команду. Ожидаемый код — 0.
  3. Снова укажите отсутствующий файл, запустите Claude Code и попросите выполнить маленькое обратимое изменение. Когда Claude попробует закончить, Stop Hook должен оставить разговор активным и показать ему причину из stderr.
  4. Не исправляя путь, выполните printf '%s\n' '{"stop_hook_active":true}' | ./scripts/check-before-stop.sh; echo $?. Скрипт должен вывести JSON с systemMessage и вернуть 0: это проверка fail-open ветки.
  5. Верните правильный путь или создайте актуальную сборку. После следующего завершения hook должен вернуть 0, и ход закончится без повторной блокировки.

Такой тест отличает работающий Stop Hook от скрипта, который падает в терминале, но не влияет на завершение Claude Code.

Как читать сбой

Если hook вернул ненулевой код, не исправляйте проблему повторной попыткой вслепую:

  1. Откройте строку Stop check blocked и запустите только названную команду вручную.
  2. Исправьте код или тест, затем снова запустите проверку в чистом состоянии.
  3. Убедитесь, что ожидаемый файл создаётся именно текущей командой, а не остался от старой сборки.
  4. Повторите исходную задачу Claude Code в новой попытке и сохраните новый результат.

Hook может обнаружить только то, что вы в него явно поместили. Прошедший git diff --check не говорит, что бизнес-логика верна; наличие файла не доказывает, что он содержит актуальную сборку.

Где находится API-часть workflow

Если Claude Code работает через API, используйте собственный API Key и сверяйте текущие параметры Anthropic-compatible подключения с документацией BetterToken для Claude Code. Hook остаётся локальной проверкой: ему не нужен доступ к ключу, полному prompt или журналу Dashboard. BetterToken — отдельный API-доступ, а не подписка Claude; ключ создаётся в вашем аккаунте и не должен попадать в репозиторий.

Готовы оптимизировать LLM workflow?

Подключите единый API, управляйте ключами и контролируйте расходы на AI-модели в BetterToken.