Hook - это способ вставить в жизненный цикл Claude Code детерминированную реакцию: он получает JSON-событие, выполняет обработчик и может вернуть решение или контекст. В отличие от текстовой инструкции, которую модель может учесть или нет, hook - это программный контроль: он срабатывает всегда и одинаково. Отсюда и ответственность: ошибка в hook способна заблокировать работу или выполнить нежелательную команду, поэтому hooks ревьюят и тестируют как production-код, а не как заметку.
Обработчик бывает пяти типов, и выбор зависит от задачи. command запускает локальный процесс, получает событие на stdin и возвращает результат кодом выхода и stdout. http шлёт POST с JSON события на URL. mcp_tool вызывает уже подключённый MCP-инструмент. prompt даёт одноходовое решение модели. agent - это agentic-верификатор с инструментами; он экспериментальный и дороже остальных. Большинство детерминированных проверок делают именно command-обработчиком - он самый предсказуемый.
Устройство hook проще всего понять на PreToolUse. matcher сначала фильтрует группу событий (например, Bash), опциональное условие if применяет одно permission-rule-условие к входу инструмента, а совпавшие обработчики выполняются параллельно. Скрипт полностью читает stdin и возвращает JSON с решением. Полезно один раз увидеть такой обработчик: ниже - скрипт, который на попытке рекурсивного удаления возвращает permissionDecision deny с понятной причиной.
У возвращаемого значения своя семантика, которую надо знать. Пустой успешный вывод означает "hook не принял решение, продолжаем обычный flow" - молчание это не запрет. И важная оговорка: наивная проверка подстроки не должна быть единственной защитой от всех форм shell-команды, потому что её легко обойти. Hook - дополнительный детерминированный слой, а первичными остаются permission deny и sandbox; забыть об этом - значит принять хрупкую строковую проверку за настоящую границу.
Событий много, и практически они группируются по назначению. Есть события сессии и конфигурации, промпта и отображения, жизненного цикла инструмента, агентов и задач, памяти и контекста, файловой системы и worktree, MCP и завершения хода. Полный текущий список вынесен в справочник; полезно один раз увидеть карту групп, чтобы понимать, к какому событию цеплять проверку - блокировку опасной команды к PreToolUse, авто-lint к PostToolUse, аудит к SessionStart.
| Группа событий | Примеры | Типичная задача |
|---|---|---|
| session/config | SessionStart, ConfigChange, SessionEnd | Настройка окружения, аудит |
| prompt/display | UserPromptSubmit, Notification | Валидация, UX |
| tool lifecycle | PreToolUse, PostToolUse, PermissionDenied | Блокировка, формат, тест, аудит |
| agent/task | SubagentStart/Stop, TaskCompleted | Контроль оркестрации |
| memory/context | InstructionsLoaded, PreCompact | Происхождение, непрерывность |
| turn end | Stop, StopFailure | Проверка, уведомления |
Коды возврата и формат вывода тоже подчиняются правилам. Код 0 сообщает об успехе; для command-обработчиков поведение ненулевого кода зависит от события, а exit 2 часто используется как блокирующая ошибка с сообщением в stderr. Но предпочитать стоит структурированный JSON-вывод там, где событие поддерживает решение: он явно задаёт причину и меньше зависит от терминального текста, который легко разойдётся с намерением. Явное решение надёжнее угаданного по коду выхода.
Отлаживают hooks методично, а не наугад. claude --debug показывает совпавшие hooks, их коды выхода и вывод. Сначала тестируют скрипт отдельно, подав ему образец JSON на stdin, затем прогоняют одно реальное событие. Ставят короткий timeout, не печатают секреты, используют абсолютные или project-root пути и не зависят от интерактивного shell-профиля - иначе hook, работавший локально, молча сломается в CI или у коллеги с другим окружением.
Типичные провалы hooks предсказуемы и дороги. Наивная подстрочная проверка как единственная защита, которую обходит первая же нестандартная форма команды. Долгий hook без timeout, вешающий работу. Секрет, распечатанный в stdout или stderr. Зависимость от shell-профиля, ломающая hook вне вашей машины. И вывод текстом там, где событие поддерживает structured decision. Ревьюйте и тестируйте hooks как код, держите permission и sandbox первичными, а решение возвращайте явным JSON.
#!/usr/bin/env node
// PreToolUse: событие на stdin -> решение в JSON
let raw = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', c => (raw += c));
process.stdin.on('end', () => {
const event = JSON.parse(raw || '{}');
const command = event.tool_input?.command || '';
if (/\brm\s+-[^\n]*r[^\n]*f\b/.test(command)) {
process.stdout.write(JSON.stringify({
hookSpecificOutput: {
hookEventName: 'PreToolUse',
permissionDecision: 'deny',
permissionDecisionReason: 'Recursive force deletion is blocked'
}
}));
}
// пустой вывод = решение не принято, продолжаем обычный flow
});