08. Хуки (Hooks)
Lifecycle hooks запускают внешние команды в определённых точках работы
AstraCode. Основная реализация находится в крейте hooks/, а вызовы из agent
loop и tool runtime — в core/src/hook_runtime.rs, core/src/session/turn.rs и
core/src/tools/registry.rs.
В текущей версии исполняются только синхронные относительно lifecycle
обработчики command: AstraCode ждёт их завершения до продолжения события.
Варианты prompt, agent и async = true распознаются конфигом, но
пропускаются с warning как неподдерживаемые.
Основной lifecycle engine включён stable feature flag
[features].astracode_hooks = true, а загрузка hooks из плагинов дополнительно
зависит от [features].plugin_hooks = true. Оба флага по умолчанию включены.
1. События и реальные точки вызова
| Событие | Когда срабатывает |
|---|---|
SessionStart |
В начале первого следующего turn после создания, resume или clear треда. Поле source: startup, resume или clear |
UserPromptSubmit |
После получения user message, но до записи сообщения в history и до запроса к модели; hook может отклонить prompt |
PreToolUse |
Перед выполнением поддерживаемого tool, если его handler сформировал pre-tool payload |
PermissionRequest |
В approval path до штатного решения guardian/user; hook может вернуть allow или deny |
PostToolUse |
Только после успешного выполнения поддерживаемого tool, если его handler сформировал post-tool payload |
Stop |
Когда модель естественно завершила текущий turn (needs_follow_up == false); hook может запросить ещё один model cycle |
Stop не является событием закрытия сессии и не гарантируется при interrupt,
ошибке или аварийном завершении. Аналогично PostToolUse не запускается после
неуспешного tool result.
Покрытие tool hooks
PreToolUse и PostToolUse вызываются не для каждого зарегистрированного
tool. Сейчас hook-payload формируют:
- shell-like handlers, включая unified exec: каноническое hook-имя
Bash; apply_patch: имяapply_patch, matcher aliasesWriteиEdit;write_file: имяwrite_file, matcher aliasWrite;- MCP tools: каноническое имя вида
mcp__server__tool.
Остальные внутренние tools проходят без PreToolUse/PostToolUse, пока их
ToolHandler не реализует соответствующий payload builder. Matcher aliases
используются только при выборе handlers; в stdin всегда записывается
каноническое tool_name.
2. Конфигурация
Хуки задаются секцией [hooks] в активном слое config.toml или файлом
hooks.json в config directory этого слоя. Если в одном слое непустые hooks
есть в обоих представлениях, AstraCode загрузит сначала hooks.json, затем
TOML и покажет warning. Также поддерживаются managed hooks из requirements и
hooks активных плагинов.
Рабочий пример для shell-команд:
[[hooks.PreToolUse]]
matcher = "Bash"
hooks = [
{ type = "command", command = "jq -r '.tool_name' >> /tmp/astracode-tools.log", timeout = 5, statusMessage = "Logging tool call..." },
]
[[hooks.SessionStart]]
matcher = "startup|resume|clear"
hooks = [
{ type = "command", command = "jq -r '.cwd' >> /tmp/astracode-sessions.log", timeout = 5 },
]
В hooks.json тот же контракт имеет верхний уровень:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_name' >> /tmp/astracode-tools.log",
"timeout": 5
}
]
}
]
}
}
Поля command-handler:
| Поле | Значение |
|---|---|
type |
Обязательное значение "command" |
command |
Обязательная shell-команда; пустая команда пропускается с warning |
timeout |
Таймаут в секундах; default 600, значения 0 и 1 дают эффективный минимум 1 |
statusMessage |
Опциональный текст, показываемый в hook status UI |
async |
Пока должен отсутствовать или быть false; true пропускается с warning |
Пользовательских полей args, workdir и env нет. Команда запускается через
пользовательский shell, наследует environment процесса AstraCode и получает
текущую рабочую директорию события как cwd.
Plugin hooks дополнительно получают PLUGIN_ROOT, PLUGIN_DATA и aliases
CLAUDE_PLUGIN_ROOT, CLAUDE_PLUGIN_DATA. Эти же имена можно использовать как
${PLUGIN_ROOT} и т.п. внутри команды: discovery подставит значения до запуска.
3. Matcher
Matcher сопоставляется с одной или несколькими строками события:
- отсутствие matcher, пустая строка или
*— все события этого типа; - строка из букв, цифр,
_и|, напримерEdit|Write, — точное совпадение с одной из альтернатив; - строка с другими regex-символами, например
^mcp__.*__write, — Rust regex.
Для PreToolUse, PermissionRequest и PostToolUse проверяются каноническое
имя tool и его matcher aliases. Один handler запускается не более одного раза,
даже если совпали и имя, и alias. Для SessionStart matcher проверяет startup,
resume или clear. Matcher у UserPromptSubmit и Stop игнорируется.
Это не expression DSL: переменные success, duration_ms, permission_type
и операторы &&, ==, ~= не поддерживаются.
4. Ввод command-hook
AstraCode сериализует событие в один JSON object и передаёт его через stdin.
Это не набор event-specific переменных окружения. transcript_path может быть
строкой или null.
| Событие | Поля stdin сверх session_id, transcript_path, cwd, hook_event_name, model, permission_mode |
|---|---|
SessionStart |
source; это единственный lifecycle input без turn_id |
UserPromptSubmit |
turn_id, prompt |
PreToolUse |
turn_id, tool_name, tool_input, tool_use_id |
PermissionRequest |
turn_id, tool_name, tool_input; tool_use_id отсутствует |
PostToolUse |
turn_id, tool_name, tool_input, tool_response, tool_use_id |
Stop |
turn_id, stop_hook_active, last_assistant_message |
Для shell-like tools tool_input имеет форму {"command":"..."}. Для MCP
там находятся разрешённые JSON-аргументы вызова. Полные схемы лежат в
hooks/schema/generated/*command.input.schema.json.
5. Stdout, stderr и решения
При exit code 0 пустой stdout означает отсутствие результата. JSON-output
должен быть одним объектом и соответствовать event-specific schema; неизвестные
или неподдерживаемые поля дают failed hook. Поведение обычного текста и exit
code 2 различается по событиям:
| Событие | Plain stdout при exit 0 |
Exit 2 с непустым stderr |
|---|---|---|
SessionStart |
Добавляется в model context | Failed hook, turn не блокируется |
UserPromptSubmit |
Добавляется в model context | Блокирует обработку prompt |
PreToolUse |
Игнорируется | Блокирует tool |
PermissionRequest |
Игнорируется | Возвращает deny |
PostToolUse |
Игнорируется | Передаёт stderr как feedback модели; выполненный tool не откатывается |
Stop |
Считается ошибкой | Отменяет остановку и передаёт stderr как continuation prompt |
Exit code 2 без требуемого текста в stderr считается failed hook и ничего не
блокирует. Любой другой ненулевой exit code, ошибка запуска или timeout также
помечаются как Failed и сами по себе не останавливают lifecycle.
SessionStart
Plain stdout является сокращённой формой дополнительного контекста. JSON-форма:
{
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": "Read the repository policy before editing."
}
}
{"continue":false,"stopReason":"..."} не позволяет начать текущий turn.
UserPromptSubmit
Plain stdout и hookSpecificOutput.additionalContext добавляют контекст перед
запросом к модели. Prompt можно отклонить:
{"decision":"block","reason":"Prompt rejected by local policy"}
continue: false также останавливает обработку текущего prompt.
PreToolUse
Поддерживаются две блокирующие JSON-формы. Короткая compatibility-форма:
{"decision":"block","reason":"Do not run destructive commands"}
И hook-specific форма:
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Do not run destructive commands"
}
}
permissionDecision: "allow"/"ask", updatedInput, additionalContext,
continue: false, stopReason и suppressOutput пока не поддерживаются и дают
failed hook без блокировки tool.
PermissionRequest
Hook может принять решение до штатного approval flow:
{
"hookSpecificOutput": {
"hookEventName": "PermissionRequest",
"decision": {
"behavior": "deny",
"message": "Network access is disabled by project policy"
}
}
}
behavior принимает allow или deny. Если совпало несколько handlers,
любой deny побеждает все allow; если решений нет, продолжается обычный approval
flow. Поля updatedInput, updatedPermissions и interrupt: true зарезервированы,
но сейчас считаются неподдерживаемыми.
PostToolUse
Дополнительный контекст возвращается через:
{
"hookSpecificOutput": {
"hookEventName": "PostToolUse",
"additionalContext": "Remember to remove the temporary file."
}
}
{"decision":"block","reason":"..."} не отменяет уже выполненный tool.
Reason становится feedback и заменяет tool response, который увидит модель.
continue: false аналогично формирует остановленный hook-result и replacement
feedback; это не rollback внешних эффектов tool. updatedMCPToolOutput пока не
поддерживается.
Stop
{"decision":"block","reason":"Run the tests before finishing"}
Такой результат не даёт turn завершиться: reason записывается как специальный
continuation prompt, после чего запускается следующий model cycle.
stop_hook_active во входе следующего вызова будет true. continue: false
имеет приоритет над decision:block и разрешает окончательно завершить turn.
Общие JSON-поля
Output schemas также содержат systemMessage, который отображается как warning
entry, и совместимые поля continue, stopReason, suppressOutput. Их реальная
поддержка различается по событиям: suppressOutput сейчас не даёт полезного
эффекта, а для PreToolUse, PermissionRequest и PostToolUse некоторые
universal-поля явно отклоняются. Поэтому источником истины являются
event-specific generated schemas вместе с parser в
hooks/src/engine/output_parser.rs, а не только общая wire-структура.
6. Порядок и объединение результатов
Discovery назначает handlers стабильный display order:
- managed hooks из requirements;
- активные config layers от низкого приоритета к высокому;
- внутри слоя сначала
hooks.json, затем[hooks]изconfig.toml; - plugin hooks.
Порядок групп и handlers внутри одного события сохраняется. Однако все
совпавшие command-handlers одного события запускаются конкурентно через
join_all. AstraCode ждёт завершения всей группы, после чего собирает результаты
в display order и объединяет решения по правилам события. Поэтому handlers не
должны зависеть от последовательных побочных эффектов друг друга.
7. Безопасность
Hook-команды — доверенный локальный код. Они запускаются с правами процесса
AstraCode и не помещаются в обычный tool sandbox. Они наследуют environment
процесса и могут читать stdin с prompt, tool_input и tool_response, где
возможны секреты.
Для hooks активных плагинов пока нет отдельного per-plugin trust prompt. Не подключайте недоверенные hook-конфиги и не сохраняйте полные payload в логи без фильтрации чувствительных данных.
8. Legacy notify
Top-level параметр notify — отдельный legacy-механизм и не относится к шести
событиям выше:
notify = ["/path/to/notifier", "--finished"]
При обычном завершении agent turn lifecycle-событие Stop выполняется первым.
Если оно не запросило продолжение и не вернуло continue:false, AstraCode
запускает указанный argv через отдельный legacy after-agent registry, добавляя
последним аргументом JSON agent-turn-complete. Stdin, stdout и stderr
отключены; процесс spawn-ится без ожидания завершения. У legacy notify нет
matcher, timeout и command-hook JSON через stdin.
9. Диагностика
Отдельной slash-команды /hooks и списка resolved handlers нет.
/debug-configпоказывает активные/отключённые config layers и requirements, но не разворачивает итоговый список handlers.- Ошибки чтения, invalid matcher, пустые/unsupported handlers и одновременная конфигурация JSON+TOML показываются как startup warnings.
- Во время работы TUI отображает
HookStarted/HookCompletedrows со статусом, duration и output entries; app-server передаёт те же события какhook/startedиhook/completed. - Для command hooks в крейте
hooksсейчас нет отдельного debug tracing, поэтомуRUST_LOG=astracode_hooks=debugне является рабочим способом получить дополнительный hook log.
10. Файлы кода
- Конфигурационный контракт:
astracode-rs/config/src/hook_config.rs - Discovery и порядок sources:
astracode-rs/hooks/src/engine/discovery.rs - Запуск процесса и timeout:
astracode-rs/hooks/src/engine/command_runner.rs - Конкурентный dispatch:
astracode-rs/hooks/src/engine/dispatcher.rs - Matcher:
astracode-rs/hooks/src/events/common.rs - Stdin/stdout wire types:
astracode-rs/hooks/src/schema.rs - Парсинг JSON-output:
astracode-rs/hooks/src/engine/output_parser.rs - Event-specific решения:
astracode-rs/hooks/src/events/ - Точки вызова из core:
astracode-rs/core/src/hook_runtime.rs,astracode-rs/core/src/session/turn.rs,astracode-rs/core/src/tools/registry.rs - Legacy notify:
astracode-rs/hooks/src/legacy_notify.rs