---
title: "Claude Code хуки: как заставить AI-агента не ломать типы и линтер"
description: "Настраиваем автоматический typecheck и lint через хуки Claude Code: PostToolUse для feedback после каждого файла, TaskCompleted как quality gate с exit code 2"
url: "https://ifonin.ru/blog/claude-code-hooks-auto-typecheck-lint/"
date: 2026-03-09
tags: ["claude-code","ai-tools","typescript","developer-experience"]
---

# 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, и он сразу их видит.

```json
{
  "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 запускается молча - просто форматирует файл:

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

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

```json
{
  "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 не даёт агенту закончить, пока все проверки не пройдут.

```bash
#!/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.
