Claude Code hooks: как превратить правила агента в гарантии
Хуки Claude Code — это shell-команды, которые сам Claude Code запускает на определённых событиях: до и после вызова инструмента, при отправке промпта, при старте сессии и перед остановкой агента. Их настраивают в settings.json, а код выхода 2 блокирует действие и возвращает агенту сообщение об ошибке, поэтому хук превращает правило из CLAUDE.md в гарантию, которую модель не может забыть.
Что такое хуки Claude Code
Хук — это команда, которую запускает не модель, а сама программа Claude Code в заданный момент работы агента. Агент собирается выполнить rm -rf — срабатывает хук и решает, пропустить ли команду. Агент отредактировал файл — хук запускает форматтер. Агент хочет закончить работу — хук прогоняет тесты и не даёт остановиться, пока они красные.
Главное отличие от инструкций в CLAUDE.md: инструкция — это текст, который модель учитывает с некоторой вероятностью, а хук — детерминированный код. Он сработает всегда, независимо от того, насколько длинный контекст, что агент «решил» и не забыл ли он правило на сотой минуте сессии. Поэтому claude code hooks — основной способ превратить пожелание в гарантию.
Почему правил в CLAUDE.md недостаточно
Типичная картина: агент дописал фичу, юнит-тесты зелёные, в чате уверенное «Готово». А сквозной сценарий никто не запускал, линтер не прогонялся, миграция упала на середине. В CLAUDE.md при этом написано «перед завершением запусти тесты». Модель не обманывает намеренно — она просто оценивает себя сама, и эта самооценка ненадёжна.
Отсюда принцип харнеса: всё, что можно проверить механически, проверяет механика, а не модель. Правило «запускай линтер» становится хуком, который запускает линтер. Правило «не трогай прод-конфиг» становится хуком, который блокирует запись в этот файл. Текстовая инструкция остаётся, но лишь объясняет агенту, почему его остановили.
События: когда срабатывают хуки
Каждый хук привязан к событию жизненного цикла сессии. Основные события:
| Событие | Когда срабатывает | Типичное применение |
|---|---|---|
PreToolUse | перед вызовом инструмента (Bash, Edit, Write и др.) | запрет опасных команд, защита файлов |
PostToolUse | после успешного вызова инструмента | форматтер, линтер, быстрые тесты после правки |
UserPromptSubmit | когда пользователь отправил промпт, до его обработки | добавить контекст, отсечь промпт с секретом |
Stop | когда агент собирается закончить ответ | не дать остановиться, пока проверки не пройдены |
SubagentStop | когда завершает работу субагент | проверка результата субагента |
SessionStart | при старте или возобновлении сессии | подгрузить состояние: git status, план, журнал прогресса |
SessionEnd | при завершении сессии | очистка, запись логов |
Notification | когда Claude Code отправляет уведомление (например, ждёт разрешения) | звук или сообщение на телефон |
PreCompact | перед сжатием контекста | сохранить важное перед компактизацией |
Самые полезные для надёжности — PreToolUse, PostToolUse и Stop: они закрывают три слабых места агента — опасные действия, непроверенные правки и преждевременное «готово».
Где настраиваются хуки: settings.json
Хуки описываются в файлах настроек Claude Code:
~/.claude/settings.json— ваши личные, для всех проектов;.claude/settings.jsonв репозитории — общие для команды, коммитятся в git;.claude/settings.local.json— локальные для проекта, в git не попадают.
Структура такая: событие → список групп с matcher → список команд.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/lint.sh",
"timeout": 60
}
]
}
]
}
}
matcher — имя инструмента или регулярное выражение по нему: Bash, Edit|Write, .*. Он нужен для событий, связанных с инструментами; для Stop или UserPromptSubmit его можно не указывать. Переменная CLAUDE_PROJECT_DIR указывает на корень проекта, поэтому скрипты удобно держать в .claude/hooks/ и вызывать через неё. Посмотреть текущие хуки можно командой /hooks внутри Claude Code.
Как хук получает данные и отвечает
Вход: JSON в stdin
Claude Code передаёт хуку JSON со сведениями о событии: идентификатор сессии, рабочий каталог, имя события, а для инструментов — tool_name и tool_input. Для Bash в tool_input.command лежит команда, для Edit и Write в tool_input.file_path — путь к файлу. Читать удобно через jq.
{
"session_id": "abc123",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": { "command": "rm -rf build/" },
"cwd": "/home/dev/project"
}
Выход: код возврата
| Код | Что происходит |
|---|---|
0 | успех, работа продолжается; для UserPromptSubmit и SessionStart stdout добавляется в контекст агента |
2 | блокирующая ошибка: stderr передаётся агенту, а действие останавливается (для PreToolUse — вызов инструмента отменяется, для Stop — агент продолжает работу) |
| другой | неблокирующая ошибка: stderr показывается пользователю, работа идёт дальше |
Exit code 2 — ключевая механика. Агент получает текст из stderr как обратную связь и может исправиться: переписать команду, починить тест, не трогать файл. Поэтому пишите в stderr не «ошибка», а понятную причину и что сделать вместо этого.
Кроме кода возврата хук может вывести в stdout JSON с решением, например "decision": "block" и "reason". Это нужно для тонкой настройки, но для большинства задач хватает кода 2 и сообщения в stderr.
Пример 1: линтер и тесты после каждой правки
Хук на PostToolUse с matcher Edit|Write срабатывает после изменения файла. Инструмент уже выполнен, отменить правку нельзя, но код 2 вернёт агенту вывод линтера, и он исправит ошибку сразу, а не через полчаса.
#!/usr/bin/env bash
# .claude/hooks/lint.sh
file=$(jq -r '.tool_input.file_path // empty')
case "$file" in
*.py)
if ! out=$(ruff check "$file" 2>&1); then
echo "Линтер нашёл ошибки в $file:" >&2
echo "$out" >&2
exit 2
fi ;;
*.php)
if ! out=$(php -l "$file" 2>&1); then
echo "$out" >&2
exit 2
fi ;;
esac
exit 0
Не вешайте сюда полный прогон тестов на десять минут: хук срабатывает на каждую правку. После правки — быстрые проверки (синтаксис, линтер, тесты одного модуля), а тяжёлые — в хуке Stop.
Пример 2: запрет опасных команд
Хук на PreToolUse с matcher Bash видит команду до выполнения. Код 2 отменяет вызов, и агент получает объяснение.
#!/usr/bin/env bash
# .claude/hooks/guard.sh
cmd=$(jq -r '.tool_input.command // empty')
if echo "$cmd" | grep -Eq 'rm -rf /|git push (-f|--force)|DROP (TABLE|DATABASE)'; then
echo "Команда заблокирована политикой проекта: $cmd. Опасные операции выполняет человек." >&2
exit 2
fi
exit 0
Так же защищают файлы: хук на Edit|Write проверяет file_path и блокирует запись в .env, прод-конфиги, сгенерированный код или файлы миграций, которые уже применены. Учтите, что поиск по регулярному выражению обходится — агент может написать команду иначе. Хук — сильная страховка, но не замена правам доступа и песочнице.
Пример 3: агент не может сказать «готово», пока тесты красные
Хук Stop срабатывает, когда агент собирается закончить. Если тесты не проходят, код 2 не даёт остановиться, а вывод тестов уходит агенту как следующая задача. Это механическая версия Definition of Done.
#!/usr/bin/env bash
# .claude/hooks/done.sh
input=$(cat)
# защита от бесконечного цикла: хук уже один раз продлил работу
if [ "$(echo "$input" | jq -r '.stop_hook_active')" = "true" ]; then
exit 0
fi
if ! out=$(npm test --silent 2>&1); then
echo "Тесты не прошли, работа не закончена:" >&2
echo "$out" | tail -n 40 >&2
exit 2
fi
exit 0
Поле stop_hook_active равно true, если агент уже продолжает работу из-за stop-хука. Без этой проверки хук может гонять агента по кругу без конца — именно та ситуация, когда агент «трудится всю ночь не туда». В автономных циклах добавьте ещё лимит попыток и условие, при котором зовут человека.
Пример 4: подгрузить состояние в начале сессии
На SessionStart stdout хука попадает в контекст. Удобно выводить текущую ветку, незакоммиченные изменения и следующую задачу из плана, чтобы агент не начинал с нуля.
#!/usr/bin/env bash
echo "Ветка: $(git branch --show-current)"
git status --short | head -n 20
[ -f PROGRESS.md ] && tail -n 15 PROGRESS.md
exit 0
Как хуки превращают правила в гарантии
Удобно делить правила харнеса на три уровня. Первый — текст в CLAUDE.md: объясняет, как работать, но соблюдается вероятностно. Второй — исполняемые проверки: тесты, E2E, архитектурные правила в виде скрипта, который падает при нарушении. Третий — хуки, которые запускают эти проверки автоматически в нужный момент, без участия модели.
Практическое правило: если агент нарушил одно и то же правило дважды, не добавляйте в CLAUDE.md ещё одно предупреждение капслоком. Спросите, можно ли проверить нарушение скриптом. Если можно — сделайте скрипт и повесьте его на хук. Тогда текстовая инструкция становится короче, а поведение — стабильнее.
| Правило в тексте | Гарантия через хук |
|---|---|
| «Форматируй код по стандарту» | PostToolUse запускает форматтер на изменённый файл |
| «Не пушь с force» | PreToolUse на Bash блокирует команду, exit 2 |
| «Не говори готово, пока тесты красные» | Stop запускает тесты и не даёт закончить |
| «Не редактируй .env» | PreToolUse на Edit|Write проверяет путь |
| «Помни, на чём остановился» | SessionStart выводит план и git status |
Типичные ошибки
- Код 1 вместо 2. Скрипт падает с кодом 1, агент ничего не узнаёт, а действие проходит. Блокирует только код 2.
- Сообщение в stdout. При коде 2 агенту уходит stderr. Пишите через
>&2. - Медленный хук на частом событии. Полный прогон тестов в PostToolUse делает каждую правку минутной. Тяжёлое — в Stop.
- Бесконечный Stop. Нет проверки
stop_hook_activeили лимита — агент крутится без конца. - Относительные пути. Хук запускается в рабочем каталоге сессии; используйте
$CLAUDE_PROJECT_DIR. - Непроверенный чужой хук. Хук выполняется с вашими правами без подтверждения. Читайте скрипты из чужих репозиториев, прежде чем им доверять.
Итог
Claude Code hooks переносят самые важные правила из текста в код: PreToolUse останавливает опасные действия, PostToolUse проверяет каждую правку, Stop не даёт рапортовать о готовности раньше времени, SessionStart возвращает агенту память о проекте. Начните с двух-трёх хуков на правила, которые агент уже нарушал, и держите сообщения в stderr понятными — тогда агент сам исправляет ошибки, а вы проверяете результат, а не процесс. Валидацию, проверку готовности и автономные циклы мы разбираем на практике в курсе.
Частые вопросы
Нужно ли перезапускать Claude Code после изменения хуков?
Изменения в settings.json надёжнее всего применяются в новой сессии; текущую конфигурацию хуков можно посмотреть командой /hooks.
Можно ли писать хуки не на bash?
Да, хук — это любая команда: скрипт на Python, Node.js или бинарник. Важно только читать JSON из stdin и возвращать правильный код выхода.
Видит ли агент вывод успешного хука?
Обычно нет: при коде 0 вывод в контекст не попадает. Исключения — UserPromptSubmit и SessionStart, где stdout добавляется в контекст агента.
Работают ли хуки для субагентов?
Да, вызовы инструментов субагентов проходят через те же хуки PreToolUse и PostToolUse, а для их завершения есть отдельное событие SubagentStop.
Безопасно ли коммитить хуки в репозиторий?
Да, .claude/settings.json и скрипты в .claude/hooks/ — обычная практика для команды; личные хуки держите в settings.local.json или ~/.claude/settings.json.
Эта тема подробно разобрана с практикой в курсе «Harness Engineering: инженерия надёжности AI-агентов».
- Агент написал «готово», а фича не работает: verification gap и Definition of Done
- Все тесты зелёные, а фича сломана: E2E-тесты и архитектурные правила для AI-агента
- Автономный AI-агент часами без присмотра: Loop Engineering и условия остановки