Хуки Claude Code блокируют только на коде выхода 2

Хук Claude Code, который нашёл rm -rf и завершился с кодом 1, команду не остановит. Claude Code сочтёт это неблокирующей ошибкой, покажет в транскрипте пометку и продолжит работу. Блокирует только exit 2, и то лишь на событиях, где блокировка вообще предусмотрена. Разбор контракта хуков, опубликованный 23 сентября, не пересказывает документацию целиком. Его тема - места, где рабочий на вид хук молча перестаёт что-либо гарантировать.
Автор разбора в блоге Blakecrosley сверял каждую деталь API со справочником и руководством Anthropic по хукам. Основная проверка прошла 8 августа, а часть про события и SDK он перепроверил 6 сентября. Он сам предупреждает: система меняется быстро, и если пост расходится со справочником, верить нужно справочнику. Его схема такая. Разрешения определяют, что агенту можно делать, CLAUDE.md описывает, что ему следует делать, а хуки составляют третий слой, и гарантию даёт только он. Prettier после каждой правки модель запустит в большинстве случаев, но может и пропустить, если правка выглядит мелкой или контекст разросся. Хук сработает на каждой правке.
JSON на входе, код выхода на выходе
Командный хук получает JSON на stdin. В нём общий конверт (session_id, transcript_path, cwd, hook_event_name, на большинстве событий ещё и permission_mode) и поля конкретного события. PreToolUse для Bash получает tool_name и tool_input.command, UserPromptSubmit получает prompt, Stop получает stop_hook_active и last_assistant_message. Хуки внутри субагентов дополнительно видят agent_id и agent_type. HTTP-хук получает тот же JSON в теле POST-запроса и отвечает телом ответа.
Исходов три. Exit 0 означает успех: Claude Code ищет в stdout JSON с решением. На большинстве событий обычный stdout уходит только в отладочный лог, но у UserPromptSubmit, UserPromptExpansion и SessionStart он добавляется в контекст, который видит Claude. Exit 2 означает блокирующую ошибку: stdout, включая любой JSON, выбрасывается, а stderr уходит модели как сообщение об ошибке. Любой другой код считается неблокирующей ошибкой, и выполнение продолжается. Документация прямо предупреждает, что exit 1 ничего не блокирует, хотя в Unix это стандартный код неудачи. Хуки с политиками должны выходить с кодом 2.
Что значит «заблокировать», зависит от события. На PreToolUse отменяется вызов инструмента, на PermissionRequest отклоняется разрешение. UserPromptSubmit останавливает обработку и стирает промпт. Stop и SubagentStop не дают агенту закончить, и разговор продолжается. На PreCompact отменяется компакция (сжатие контекста), а PostToolBatch останавливает агентный цикл до следующего вызова модели. У WorktreeCreate создание worktree обрывает любой ненулевой код. Остальные события блокировать не умеют вовсе. PostToolUse показывает stderr модели, но инструмент к этому моменту уже отработал. StopFailure, InstructionsLoaded, MessageDisplay и PermissionDenied код выхода игнорируют, и у PermissionDenied единственный рычаг - JSON с retry: true.
Тоньше управлять можно через JSON в stdout при exit 0, и правило тут жёсткое: либо код выхода, либо JSON. Скрипт, который печатает {"decision": "block"} и выходит с кодом 2, свой JSON потеряет. У PreToolUse своя схема. Верхнеуровневые decision/reason для него устарели, решение теперь кладут в hookSpecificOutput.permissionDecision со значениями allow, deny, ask или defer, а через updatedInput можно переписать аргументы до выполнения. Если несколько хуков PreToolUse не согласны между собой, побеждает самый строгий ответ: deny, затем defer, ask и allow. Автор всё равно советует закреплять каждое решение за одним хуком и на этот порядок не полагаться. Переписывание аргументов параллельными хуками ненадёжно по построению: когда несколько хуков правят аргументы одного инструмента, сохраняется только одна правка, и какая именно, вы не контролируете.
Сильнее разрешений, но в одну сторону
PreToolUse срабатывает раньше любой проверки режима разрешений. Поэтому permissionDecision: "deny" останавливает инструмент даже в режиме bypassPermissions и под --dangerously-skip-permissions. Наоборот это не работает: "allow" из хука не ослабит запрет из settings. Хуки умеют ужесточить политику сверх того, что дают разрешения, но ослабить её не могут.
Отсюда и разделение труда. Правила разрешений декларативны: это статические шаблоны allow/deny/ask, которые Claude Code проверяет сам. Хук программируемый, ваш код видит весь ввод инструмента и решает сам. Всё, что выражается шаблоном, автор советует держать в правилах, а за хук браться, когда решению нужна логика, внешнее состояние или переписывание ввода. В headless-режиме (-p) у PreToolUse есть дополнительная опция defer, которую интерактивные сессии игнорируют. Она ставит вызов на паузу, чтобы внешний процесс, например приложение на Agent SDK или собственный интерфейс, собрал ввод и потом возобновил сессию.
Где хуки ломаются без ошибок
Матчеры, фильтры по имени инструмента или другому полю события, разбираются по символам. Значение из одних букв, цифр, _, -, пробелов, запятых и | считается точной строкой или списком (Bash, Edit|Write). Всё остальное превращается в регулярное выражение JavaScript без якорей, поэтому Edit.* поймает и Edit, и NotebookEdit, а для одного инструмента нужно писать ^Edit$. Регистр важен: bash никогда не совпадёт с Bash.
Поле if у обработчика принимает правило вроде "Bash(git *)", но работает в режиме best-effort и на неразборчивых командах пропускает вызов. Для жёстких гарантий автор отправляет к правилам разрешений.
Два изменения в v2.1.214 тихо ломают старые конфиги. Первое касается if: однокомпонентный путь вроде Edit(src/**) теперь совпадает только с src верхнего уровня в рабочем каталоге. Правило, написанное раньше, перестаёт ловить вложенные пути вроде packages/app/src/, и для прежнего поведения нужен Edit(**/src/**). Второе: у SessionStart поле source получило пятое значение fork вместе с форками сессий. Хук, скопированный со старого списка из четырёх значений (startup, resume, clear, compact), форки молча пропустит. Автор разбора пишет, что API хуков существенно менялся на протяжении релизов v2.1.x, и предлагает считать детали, зависящие от версии, верными на дату проверки.
Из 33 событий нужны пять
На 6 сентября справочник документировал 33 события. По оценке автора, почти любая рабочая настройка собирается из пяти: PreToolUse, PostToolUse, UserPromptSubmit, SessionStart и Stop. Остальные 28 лежат на тот день, когда понадобятся: смена модели, компакция, субагенты, worktree, запросы ввода от MCP-серверов. Например, PreModelSwitch может отказать в переключении на неодобренную модель. MessageDisplay правит текст только на экране, транскрипт не меняется. DirectoryAdded срабатывает, когда каталог подключают через /add-dir или SDK-вызов register_repo_root, в v2.1.219 и новее.
Из пяти паттернов разбора подробнее всего расписаны ворота на Stop. Хук запускает npm test и при падении возвращает decision: "block" с причиной, так что агент продолжает работу. Сначала скрипт проверяет stop_hook_active: если агент уже продолжает из-за этого же хука, тот выходит с нулём. Без проверки ворота упрутся в лимит. Claude Code по умолчанию разрешает Stop-хуку не больше 8 блокировок подряд, порог поднимается переменной CLAUDE_CODE_STOP_HOOK_BLOCK_CAP. Мягче работает hookSpecificOutput.additionalContext: продолжение то же, но это помеченная обратная связь, а не ошибка хука. Для разовых условий есть встроенная /goal, по сути промптовый Stop-хук на одну сессию без настройки. Stop при этом срабатывает на каждом конце ответа, а не только когда задача готова, и не срабатывает при прерывании пользователем. Логика ворот должна переживать остановки посреди задачи.
Пятый паттерн решает проблему конфигов, которые расползаются по машинам. Один диспетчер на событие перебирает исполняемые скрипты в .claude/hooks/<событие>/, и первый exit 2 пробрасывается наверх. Новый guard добавляется через chmod +x, а settings.json не меняется. Цена в том, что диспетчер выполняет последовательно то, что Claude Code запустил бы параллельно. И хукам с JSON-выводом лучше остаться отдельными, потому что в stdout должен быть ровно один JSON-объект.
SDK и моды
В Agent SDK отдельной системы хуков нет: срабатывают те же события, хуки регистрируются колбэками в поле hooks опций с тем же синтаксисом матчеров, и колбэк возвращает тот же JSON, что shell-хук печатает в stdout. Различается покрытие. PreToolUse, PostToolUse, PostToolUseFailure, UserPromptSubmit, Stop, SubagentStart, SubagentStop, PreCompact, PermissionRequest и Notification есть в обоих SDK. SessionStart, SessionEnd, Setup, PostToolBatch, PermissionDenied, а также события компакции, смены модели, задач, worktree, запросов ввода и слежения за файлами на момент проверки доступны только в TypeScript. Shell-хуки из settings продолжают работать внутри SDK-приложения, если включён соответствующий settingSources (setting_sources в Python), а в опциях query() по умолчанию он включён. Приложение наследует хуки проекта и добавляет колбэки поверх.
Документация Anthropic описывает и ещё один путь. Плагин может регистрировать хуки как JavaScript-функции, которые Claude Code вызывает в своём процессе и которые умеют рисовать в интерфейсе. Такой плагин называется модом: так Function Hooks превратились в Claude Mods. Shell- и HTTP-хуки со страницы справочника продолжают работать рядом с модами.
Чего хуки не умеют
PostToolUse ничего не откатывает: инструмент уже отработал, предотвращать нужно в PreToolUse. Хуки не вызывают инструменты и слеш-команды, а контекст из additionalContext вставляется простым текстом. Файлы, подтянутые через @ в промпте, PreToolUse не видит, потому что вызова инструмента там нет; такие пути защищают правилами deny для Read. PermissionRequest не срабатывает в обычных прогонах -p, только когда промпт разрешения поставляет колбэк canUseTool из Agent SDK, и для вызовов фоновых субагентов. Таймаут по умолчанию 600 секунд для command-хуков, но всего 30 для UserPromptSubmit и 10 для MessageDisplay, и ворота, упавшие по таймауту, считай, не сработали. Вывод режется на 10 000 символах, остаток пишется в файл и заменяется превью.
И хуки работают с полными правами вашего пользователя. Справочник предупреждает, что они «могут изменить, удалить или прочитать любой файл, к которому есть доступ у вашей учётной записи», и советует проверять и тестировать каждую команду до добавления в конфиг. Классическая ловушка при отладке - shell-профиль, который что-то печатает при старте и портит JSON хука. Отлаживают через транскрипт (Ctrl+O), claude --debug-file /tmp/claude.log или /debug прямо в сессии, а всё разом отключает "disableAllHooks": true.
Если хук запускается, но ничего не блокирует, автор советует две проверки. Сначала /hooks: там видно, что к какому событию подключено и из какого файла settings оно пришло. Потом подать в скрипт пример JSON через пайп и посмотреть echo $?.
Дальше
Ответ Opus 5.5 стоил в 3,4 раза больше, чем у GPT-6 Sol
49 задач из 49 у обеих моделей, но верный ответ Claude Opus 5.5 стоил 0,05422 доллара против 0,015922 у GPT-6 Sol. И это при том, что по прайсу Opus дороже всего вдвое.
Читайте также
- Function Hooks стали Claude Mods, релиз обещают за недели
- Function Hooks для Claude Code показали до релиза
- Claude Code 2.1.318: срок входа, логи раннера и шлюз AWS
- intent-lens объясняет команды Claude Code до их запуска
- Codedict срезал токены Claude Code на 63%, а счёт на 26%
- Дерево файлов покажет, что читает и правит Claude Code
Комментарии
Пока никто не написал. Будьте первым.
Присоединяйтесь к разговору
Войдите, чтобы оставить комментарий. Он появится после модерации.

