Claude Code хуки: как заставить AI-агента не ломать типы и линтер

Настраиваем автоматический typecheck + lint после каждой задачи

Знакомо: Claude Code вносит правки в десяток файлов, ты радостно коммитишь - а потом CI падает с сотней ошибок типов? Агента можно заставить самому чинить всё, что он сломал, до того как он скажет “готово”.

Проблема: агент не видит, что сломал

Claude Code отлично генерирует код. Но он не запускает tsc --noEmit после каждого изменения. Не прогоняет ESLint. Не проверяет, что его правки в одном файле не сломали импорты в пяти других - а ведь именно это происходит чаще всего.

Можно написать в CLAUDE.md: “Всегда запускай typecheck после изменений”. Иногда это работает. Иногда нет. Промпт-инструкции - это рекомендации, не гарантии. Агент может проигнорировать их, особенно когда контекст разрастается и ранние инструкции вытесняются из “внимания”.

У меня фронтенд-проект на React/Next.js. Строгий TypeScript, большая кодовая база. Агент вносил изменения, ломал типы в соседних файлах, а я узнавал об этом только на этапе сборки. Или хуже - на CI после пуша. Цикл “сгенерировал - закоммитил - CI упал - откатил - объяснил агенту - повторил” отнимал больше времени, чем ручное написание кода. Я начал искать решение.

И нашёл его в hooks.

Что такое хуки Claude Code

Хуки - это shell-команды, которые Claude Code выполняет автоматически в определённые моменты своей работы. Не “может выполнить, если вспомнит”, а выполняет каждый раз. Гарантированно.

Ключевые events:

  • PostToolUse - срабатывает после того, как агент отредактировал файл (Write или Edit). Сюда вешаем typecheck и lint.
  • Stop - срабатывает каждый раз, когда Claude заканчивает ответ. Подходит для проверок после каждого шага.
  • TaskCompleted - срабатывает, когда задача помечается как завершённая. Сюда вешаем финальный quality gate.

Stop и TaskCompleted - разные events с разным payload. Stop ловит каждый ответ агента, TaskCompleted - только момент завершения задачи.

Exit codes управляют поведением:

  • 0 - всё хорошо, stdout попадает агенту как обратная связь
  • 2 - блокировка. Операция останавливается, текст из stderr отправляется агенту. Он обязан исправить проблему.
  • Любой другой - ошибка, но без блокировки

Для Stop hooks есть альтернативный подход: вернуть JSON через stdout ({"decision":"block","reason":"..."}) с exit code 0. Паттерн exit 2 + stderr тоже работает, но JSON-формат считается более актуальным.

Конфигурация лежит в .claude/settings.json (для проекта, коммитится в git) или в ~/.claude/settings.json (глобально). Есть ещё .claude/settings.local.json для личных настроек, которые не попадают в репозиторий.

Рецепт 1: Typecheck после каждого изменения файла

Агент отредактировал .ts или .tsx файл - мы тут же запускаем tsc --noEmit. Ошибки попадают агенту как feedback, и он сразу их видит.

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "bash -c 'FILE=$(jq -r \".tool_input.file_path\" <<< \"$(cat)\"); if [[ \"$FILE\" == *.ts || \"$FILE\" == *.tsx ]]; then npx tsc --noEmit 2>&1 | head -20; fi; exit 0'",
            "timeout": 30
          }
        ]
      }
    ]
  }
}

matcher: "Write|Edit" - хук срабатывает только на запись и редактирование файлов, не на Bash или другие инструменты. jq парсит JSON из stdin, чтобы достать путь к файлу. head -20 обрезает вывод - агенту хватит первых двадцати строк, чтобы понять масштаб проблемы.

exit 0 в конце означает, что мы не блокируем агента, а информируем. Ошибки типов попадают к нему как системное сообщение, и он сам начинает их исправлять. В моём опыте этого достаточно в подавляющем большинстве случаев - агент видит ошибку и тут же её чинит.

Для production-проекта имеет смысл вынести логику в отдельный скрипт (например, ~/.claude/hooks/typescript-check.sh), который ищет tsconfig.json вверх по дереву каталогов и корректно находит путь к tsc.

Рецепт 2: ESLint auto-fix + Prettier

Форматирование и lint вешаем на тот же PostToolUse event. Prettier запускается молча - просто форматирует файл:

{
  "type": "command",
  "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write 2>/dev/null; exit 0"
}

ESLint с --fix исправляет то, что может, а оставшиеся ошибки отправляет агенту:

{
  "type": "command",
  "command": "bash -c 'FILE=$(jq -r \".tool_input.file_path\" <<< \"$(cat)\"); if [[ \"$FILE\" == *.ts || \"$FILE\" == *.tsx || \"$FILE\" == *.js || \"$FILE\" == *.jsx ]]; then npx eslint --fix \"$FILE\" 2>/dev/null; fi; exit 0'"
}

Оба хука можно повесить в один массив hooks внутри PostToolUse. Они выполнятся последовательно: сначала Prettier отформатирует, потом ESLint проверит и починит что может.

Отдельно про ESLint-правила для AI-кода. Alex Brohshtut предложил набор strict-правил, которые хорошо работают именно с агентами: запрет комментариев (код должен быть самодокументируемым), максимум два параметра в функции, ограничение на длину функций и файлов, запрет magic numbers. Агент, встретив такие ограничения, автоматически рефакторит код - разбивает длинные функции, выносит параметры в объекты. Забавный момент: некоторые модели пытаются обойти правила через eslint-disable-next-line, но запрет комментариев блокирует и это.

Рецепт 3: Quality Gate - не дать агенту завершиться без зелёных проверок

PostToolUse даёт feedback после каждого файла. Полезно, но недостаточно. Агент может увидеть ошибки, решить, что разберётся позже, и сказать “готово”, забыв про них.

TaskCompleted hook решает эту проблему. Он срабатывает при завершении задачи. Exit code 2 не даёт агенту закончить, пока все проверки не пройдут.

#!/bin/bash
INPUT=$(cat)
# Typecheck
TSC_RESULT=$(npx tsc --noEmit 2>&1)
if [ $? -ne 0 ]; then
  echo "TypeScript errors found. Fix before completing." >&2
  exit 2
fi
# Lint
LINT_RESULT=$(npx eslint src/ 2>&1)
if [ $? -ne 0 ]; then
  echo "ESLint errors found. Fix before completing." >&2
  exit 2
fi
exit 0

Если вы используете Stop hook вместо TaskCompleted, добавьте защиту от бесконечного цикла: проверяйте поле stop_hook_active из входного JSON. Без этого агент зацикливается - пытается завершиться, хук блокирует, агент исправляет и снова пытается завершиться, хук снова блокирует.

Я использую TaskCompleted с похожим скриптом. Это полностью убрало “сюрпризы на CI” - до хуков я регулярно ловил type errors после пуша, после внедрения quality gate такое перестало происходить. Агент не говорит “готово”, пока typecheck и lint не зелёные.

Можно пойти дальше и добавить prompt hook - LLM-проверку, которая оценивает, соответствует ли реализация исходному запросу. Но command hooks покрывают большинство потребностей. Детерминированные проверки надёжнее LLM-оценок для типовых операционных задач.

Подводные камни

jq должен быть установлен. Хуки получают данные через JSON на stdin. Без jq их не распарсить удобно (можно через Python или sed, но jq проще). brew install jq на маке, apt install jq на Linux.

Скрипты должны быть исполняемыми. chmod +x для каждого .sh файла.

Exit 2 передаёт только stderr. Всё, что вы пишете в stdout при exit code 2, теряется. Сообщения об ошибках отправляйте через >&2.

Matchers чувствительны к регистру. "Bash", не "bash". Имена инструментов в PascalCase: Write, Edit, Bash.

~/.zshrc может всё сломать. Если ваш shell profile печатает что-то при запуске (приветствие, информацию об окружении), это попадает в stdout и ломает JSON-парсинг. Оберните такие команды в проверку if [[ $- == *i* ]]; then ... fi.

Один хук на старт

Добавьте PostToolUse хук с tsc --noEmit сегодня. Через неделю посмотрите, сколько ошибок он поймал - и решите, нужен ли вам quality gate на TaskCompleted.