Перейти к содержимому

Хуки

Хук это команда, которая запускается на определённом событии агента. Хуки объявляются в 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" }]
}
]
}
}
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" }] }
]
}
}
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 хук работает без правок, включая блокировку: имена событий, формат конфигурации, коды возврата и поля 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.