Хуки
Хук это команда, которая запускается на определённом событии агента. Хуки объявляются в settings.json под ключом hooks.
Правило в WAIBEE.md это просьба: агент обычно её выполняет, но может и упустить, особенно когда контекст заполнен. Хук срабатывает всегда, независимо от того, что агент решил. Поэтому в инструкции идёт то, что желательно, а в хуки то, что обязано случиться каждый раз.
События
Заголовок раздела «События»| Событие | Когда срабатывает |
|---|---|
SessionStart |
Старт, возобновление или сброс сессии |
SessionEnd |
Завершение сессии: закрытие, сброс, выход |
UserPromptSubmit |
Ты отправил запрос, до обращения к модели |
PreToolUse |
Перед вызовом инструмента. Единственное место, где вызов можно запретить или переписать |
PostToolUse |
После того как инструмент вернул результат, успешно или с ошибкой |
PreCompact, PostCompact |
До и после сжатия контекста |
Stop |
Модель закончила ход |
SubagentStart, SubagentStop |
Запуск и завершение субагента |
Notification |
Запрос подтверждения и другие уведомления |
Неизвестное имя события молча игнорируется, поэтому опечатку в ключе видно только по тому, что хук не срабатывает.
Конфигурация
Заголовок раздела «Конфигурация»{ "hooks": { "PreToolUse": [ { "matcher": "Bash", "hooks": [ { "command": "./scripts/audit.sh" } ] } ] }}matcher(необязательно): регулярное выражение по имени инструмента,"*"или пустая строка означают «любой». Учитывается только для событий с инструментом (PreToolUse,PostToolUse,Notification).command: shell-команда.async(по умолчаниюfalse):trueпревращает хук в «запустить и забыть». По умолчанию хук блокирующий, то есть агент ждёт его и читает результат.
Поле blocking тоже понимается и задаёт то же самое напрямую: "blocking": false равнозначно "async": true.
Как выполняются
Заголовок раздела «Как выполняются»Команда запускается через shell в рабочей папке проекта, получает JSON события на stdin и переменные окружения: TOOL_NAME, TOOL_INPUT, SESSION_ID, CWD, PROJECT_DIR, CLAUDE_PROJECT_DIR, HOOK_EVENT, TIMESTAMP, TRANSCRIPT_PATH. Таймаут 60 секунд. Команда /hooks показывает настроенные хуки, добавляет новый по форме (событие, шаблон инструмента, команда, блокирующий или нет) и выключает любой, не удаляя запись, чтобы вернуть его потом одним нажатием; d удаляет запись совсем. То же самое доступно на странице расширений в браузере.
Блокирующий хук отвечает кодом возврата:
| Код | Что значит |
|---|---|
0 |
Разрешить. Если на stdout лежит JSON, он разбирается, см. ниже |
2 |
Запретить. Текст из stderr становится причиной, которую видит модель |
126, 127 |
Хук не найден или не исполняемый: это не решение, а поломка конфигурации |
| прочее | Разрешить, в лог уходит предупреждение |
Асинхронный хук ("async": true) запускается и забывается: ни код возврата, ни вывод не читаются. Для уведомлений и логирования этого достаточно, и он ничего не задерживает.
Поломанный хук
Заголовок раздела «Поломанный хук»Хук с опечаткой в пути, недоступный на исполнение или зависший до таймаута не считается решением:
- на
PreToolUseэто отказ. Это контур безопасности, и сломанная проверка не должна тихо пропускать вызовы; - на всех остальных событиях агент показывает предупреждение и продолжает работу. Неверный путь в хуке
UserPromptSubmitне заблокирует тебе все запросы.
Что может блокировать и менять
Заголовок раздела «Что может блокировать и менять»Код возврата 2 запрещает действие на любом событии. Всё остальное задаётся объектом JSON на stdout блокирующего хука. Формат тот же, что у Claude Code:
| Поле | Событие | Действие |
|---|---|---|
hookSpecificOutput.permissionDecision |
PreToolUse |
allow, deny, ask или defer, причина в reason. Перебивает режим прав; если хуков несколько, побеждает самый строгий |
hookSpecificOutput.updatedInput |
PreToolUse |
Заменяет аргументы вызова. Побеждает последний хук, поэтому цепочка санитайзеров складывается |
hookSpecificOutput.updatedToolOutput |
PostToolUse |
Заменяет результат инструмента до того, как его увидят модель и интерфейс |
hookSpecificOutput.additionalContext |
любое | Текст подмешивается модели следующим ходом |
continue: false плюс stopReason |
любое | Останавливает агента |
Ключевая деталь: обычный вывод на stdout модели не достаётся. Хук, который печатает текст и надеется дополнить контекст, ничего не изменит: нужен JSON с additionalContext. Не-JSON на stdout трактуется как «разрешить».
Примеры
Заголовок раздела «Примеры»Форматирование после каждой правки
Заголовок раздела «Форматирование после каждой правки»Самый частый случай. Агент не тратит ход на запуск форматтера, а дифф остаётся чистым.
{ "hooks": { "PostToolUse": [ { "matcher": "Write|Edit", "hooks": [{ "command": "make fmt", "async": true }] } ] }}async здесь по делу: результат форматтера агенту не нужен, а без этого флага каждая правка ждёт, пока make fmt закончится.
Защита путей от правок
Заголовок раздела «Защита путей от правок»Миграции, lock-файлы, сгенерированный код, чужой модуль: то, что агент не должен трогать без твоего участия. Список deny в правах закрывает команды, а этот хук закрывает файлы.
{ "hooks": { "PreToolUse": [ { "matcher": "Write|Edit", "hooks": [{ "command": "./scripts/guard-paths.sh" }] } ] }}#!/usr/bin/env bash# Exit 2 blocks the tool call; stderr becomes the reason shown to the agent.case "$TOOL_INPUT" in *migrations/*|*package-lock.json*) echo "этот путь правится только вручную" >&2 exit 2 ;;esacФайл обязан быть исполняемым (chmod +x) и лежать там, где указано: на PreToolUse ненайденный хук трактуется как отказ, и все правки перестанут проходить.
Подмешать контекст в каждый запрос
Заголовок раздела «Подмешать контекст в каждый запрос»Хук на UserPromptSubmit может дополнить твой ввод. Удобно, когда одно и то же приходится повторять: текущая ветка, номер задачи, состояние рабочей копии. Контекст передаётся полем additionalContext, обычный вывод команды агент не читает.
{ "hooks": { "UserPromptSubmit": [ { "hooks": [{ "command": "./scripts/git-context.sh" }] } ] }}#!/usr/bin/env bash# Plain stdout is ignored; only additionalContext reaches the model.jq -n --arg ctx "$(git status --short --branch)" \ '{hookSpecificOutput: {hookEventName: "UserPromptSubmit", additionalContext: $ctx}}'Уведомление, когда агент закончил
Заголовок раздела «Уведомление, когда агент закончил»Долгая задача перестаёт требовать, чтобы ты смотрел в терминал.
{ "hooks": { "Stop": [ { "hooks": [{ "command": "osascript -e 'display notification \"Готово\" with title \"Waibee Code\"'", "async": true }] } ] }}На Linux вместо этого подойдёт notify-send, а вместо уведомления можно отправить сообщение в мессенджер через curl.
Журнал команд
Заголовок раздела «Журнал команд»Все запуски Bash в один файл: удобно, когда работаешь в trust или разбираешь, что происходило в длинной сессии.
{ "hooks": { "PostToolUse": [ { "matcher": "Bash", "hooks": [{ "command": "echo \"$TOOL_INPUT\" >> .waibee/commands.log", "async": true }] } ] }}Проверка на секреты перед записью
Заголовок раздела «Проверка на секреты перед записью»Хук на PreToolUse останавливает запись, если в содержимом похоже на ключ или токен. Дешевле, чем вычищать это из истории git.
{ "hooks": { "PreToolUse": [ { "matcher": "Write|Edit", "hooks": [{ "command": "./scripts/no-secrets.sh" }] } ] }}Совместимость с Claude Code
Заголовок раздела «Совместимость с Claude Code»Перенесённый из Claude Code хук работает без правок, включая блокировку: имена событий, формат конфигурации, коды возврата и поля hookSpecificOutput те же. Читается и ~/.claude/settings.json, экспортируются CLAUDE_PROJECT_DIR и CLAUDE_PLUGIN_ROOT, понимается старый плоский формат decision: "block".
Отличий три:
- Обычный вывод на
stdoutне становится контекстом. В Claude Code хук наUserPromptSubmitилиSessionStartможет просто напечатать текст; здесь нужен JSON сadditionalContext. - Есть
async. Хук, чей результат агенту не нужен, помечается"async": trueи не задерживает ход. - Сломанный хук на
PreToolUseзапрещает вызов, а не пропускает его с предупреждением.
Плюс два события, которых в Claude Code нет: SubagentStart и PostCompact.