0.4.11
08 · Техническая документация

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 aliases Write и Edit;
  • write_file: имя write_file, matcher alias Write;
  • 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-команд:

toml
[[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 тот же контракт имеет верхний уровень:

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-форма:

json
{
  "hookSpecificOutput": {
    "hookEventName": "SessionStart",
    "additionalContext": "Read the repository policy before editing."
  }
}

{"continue":false,"stopReason":"..."} не позволяет начать текущий turn.

UserPromptSubmit

Plain stdout и hookSpecificOutput.additionalContext добавляют контекст перед запросом к модели. Prompt можно отклонить:

json
{"decision":"block","reason":"Prompt rejected by local policy"}

continue: false также останавливает обработку текущего prompt.

PreToolUse

Поддерживаются две блокирующие JSON-формы. Короткая compatibility-форма:

json
{"decision":"block","reason":"Do not run destructive commands"}

И hook-specific форма:

json
{
  "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:

json
{
  "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

Дополнительный контекст возвращается через:

json
{
  "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

json
{"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:

  1. managed hooks из requirements;
  2. активные config layers от низкого приоритета к высокому;
  3. внутри слоя сначала hooks.json, затем [hooks] из config.toml;
  4. 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-механизм и не относится к шести событиям выше:

toml
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/HookCompleted rows со статусом, 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