← Claude Code in Action · урок 5 из 9

Урок 5. Hooks

Конспект урока «Hooks» из бесплатного курса Anthropic Claude Code in Action. Оригинал урока: https://anthropic.skilljar.com/claude-code-in-action/486933

Это пересказ на русском, а не перевод. Права на курс принадлежат Anthropic.

Проблема, которую они решают

Написать правило в CLAUDE.md — значит попросить, а не гарантировать. «Всегда форматируй после правки» Клод обычно выполнит. Обычно. Но в длинном прогоне, за которым ты не следишь, «обычно» не годится.

Hook — это детерминированный код, который выполняется в фиксированной точке цикла. Он превращает правило из «Клод обычно слушается» в «Клод не может это пропустить».

События

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

Событие Когда срабатывает и зачем
PreToolUse до вызова инструмента. Основной инструмент принуждения — единственный, кто может остановить действие до того, как оно случилось
PostToolUse после успешного вызова. Сюда обычно вешают автоформат и автолинт
Stop когда Клод хочет завершить ход. Можно отказать: «нет, ты ещё не закончил», если условие не выполнено. Для субагентов есть парное SubagentStop
PreCompact / PostCompact до и после сжатия контекста
InstructionsLoaded при загрузке CLAUDE.md или файла правил. Удобно, чтобы проверять, что реально попало в контекст
SessionStart на старте, подготавливает окружение. Есть источник startup, если нужно только на свежих запусках

Место, где спотыкаются: чтобы вернуть контекст после сжатия, PostCompact не подходит. Нужен SessionStart с матчером compact — только его вывод реально попадает обратно в разговор.

PreToolUse: решение возвращается в JSON

Здесь вся сила, потому что можно заблокировать вызов до исполнения. Ответ Клоду — это JSON в stdout и выход с нулевым кодом. Ключевое поле permissionDecision, у него три значения:

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

Структура ответа:

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "...",
    "updatedInput": { "command": "..." }
  }
}

Обрати внимание на updatedInput: вызов можно не блокировать, а переписать. Именно так вычищают секрет из bash-команды, при этом позволяя ей выполниться. Подвох: updatedInput заменяет объект входных данных целиком, так что поля, которые ты не меняешь, надо вернуть обратно — иначе они потеряются.

Коды возврата — для хуков попроще

Не всякому хуку нужен JSON. Значимых кодов три:

Главная ловушка — код 1. Выглядит как ошибка, но не блокирует: команда всё равно выполнится. Хочешь остановить — возвращай 2, а не 1.

Ещё пара тонкостей. Код 2 умеет блокировать даже Stop — так Клоду сообщают, что он не закончил. А PostToolUse срабатывает уже после выполнения инструмента, так что блокировать там поздно, хотя текст Клоду вернуть всё ещё можно. Некоторые события блокировку игнорируют вовсе (Notification, SessionStart) — покажут твой stderr и поедут дальше.

Приём: не блокировать, а вычищать

Практический пример. Ставим на инструмент Bash защиту через PreToolUse. Матчер выбирает инструмент, необязательное условие сужает до конкретной команды.

Очевидный ход — вернуть deny и остановить опасный вызов. Это правильно. Но менее известный и более интересный ход — вернуть updatedInput и переписать вызов. Так из команды вырезают секрет, не отказывая ей в исполнении.

На практике: Клоду поручают выполнить команду с боевым на вид секретом. Хук перехватывает её, видит шаблон sk_live_ и подменяет его заглушкой ещё до запуска.

Команда выполнилась. Работа сделана. Секрет наружу не ушёл. Вот разница между блокировкой и вычисткой — и хук обеспечивает это каждый раз, без исключений.

Сохранить состояние через сжатие

Ещё один приём, который стоит настроить. При сжатии длинного разговора теряется много деталей. Хук SessionStart с матчером compact срабатывает сразу после сжатия — пусть он печатает короткую сводку о файлах, над которыми ты работал. Сводка возвращается в контекст, и Клод продолжает с того же места, а не с чистого листа.

Итог

Хуки превращают правило, которому Клод обычно следует, в правило, которому он следует всегда. Не ограничивайся автоформатом: закрывай инструменты через PreToolUse, ставь ворота на завершение хода через Stop, сохраняй состояние через сжатие. Настройка стоит некоторых усилий, но окупается в первый же раз, когда хук поймает что-то в прогоне, за которым ты даже не наблюдал.