Глава 09 · Расширяемость

Hooks — точки расширения между шагами цикла

Hook — это callback на событие внутри агентского цикла. Пользователь, плагин или skill вешает свой код на одно из именованных событий harness — и тот выполняется между шагами модели и инструментов, может заблокировать действие, добавить контекст или просто записать лог.

EVENT TIMELINE SessionStart сессия запущена snapshot загружен UserPromptSubmit prompt получен до вызова модели PreToolUse tool_use получен до выполнения PostToolUse tool_result готов до отправки модели Stop модель сказала end_turn SessionEnd сессия закрыта cleanup EXECUTORS shell bash-команда prompt одиночный LLM-зов agent мини-агент с tools http POST на endpoint function in-process callback

Зачем hooks

В чистом виде агентский цикл — это «модель решает, harness выполняет, история накапливается». Но между шагами есть моменты, когда у harness уже есть весь нужный контекст — текст prompt, имя инструмента, его аргументы, результат вызова — и при этом ещё не поздно вмешаться. Hooks — это и есть способ вставить пользовательский код именно в эти моменты, не форкая harness и не переписывая loop. Hook видит payload события, может его прочитать, дополнить или вернуть решение, которое изменит дальнейшее поведение цикла.

Канонических применений три. Первое — policy enforcement: запретить запись вне рабочей директории, заблокировать Bash с rm -rf, потребовать подтверждения для npm publish. Это происходит на PreToolUse: hook видит имя инструмента и его JSON-аргументы и возвращает решение «разрешить» или «запретить». Второе — инъекция контекста: на UserPromptSubmit прикрепить к запросу метаданные, текущий бранч git, недавние коммиты, выписку из issue-трекера — чтобы модель уже на старте видела окружение. Третье — наблюдаемость: на PostToolUse и Stop писать структурированные логи, считать метрики, отправлять события в SIEM. Все три задачи решаются одним и тем же механизмом — событием с payload и пользовательским callback.

6 типов событий

SessionStart. Срабатывает один раз при старте сессии — после загрузки конфигурации и до первого user prompt. Идеальное место, чтобы подгрузить системные инструкции, прочитать состояние из внешнего хранилища, объявить алерт в Slack «такой-то стартовал агента». Payload минимальный: id сессии, рабочая директория, профиль пользователя.

UserPromptSubmit. Срабатывает после того, как пользователь отправил сообщение, но до того, как harness вызовет модель. Payload содержит сам текст prompt. Hook может его прочитать, может добавить дополнительный контекст в messages (имя ветки git, открытый файл, краткую выжимку из CRM), может вообще заблокировать запрос, если он матчит запрещённый шаблон.

PreToolUse. Самое нагруженное событие. Срабатывает, когда модель попросила инструмент, но harness ещё не начал его выполнять. Payload — имя инструмента и его JSON-вход. Hook может разрешить (allow), заблокировать (deny со stderr-сообщением, которое увидит модель и решит, что делать дальше), спросить пользователя (ask) или мутировать вход (например, заменить путь на абсолютный). Hooks матчатся по tool_name: один хук может слушать «Edit|Write», другой — «Bash», третий — все подряд.

PostToolUse. Срабатывает после того, как harness выполнил инструмент, но до того, как результат был отправлен обратно модели. Payload — имя инструмента, его вход и его результат (stdout/stderr/exit-code или текст ответа). Здесь живут аудиторы и логгеры: «записать каждое изменение файла в audit.log», «посчитать длину stdout и предупредить, если он больше 50 KB», «отметить в Grafana, что прошёл бенчмарк».

Stop. Срабатывает, когда модель сообщила end_turn и цикл собирается остановиться. Hook может прочитать финальный ответ, отправить уведомление, сохранить артефакт в S3. В режиме субагента то же событие называется SubagentStop — потому что субагент завершается отдельно от основного цикла.

SessionEnd. Зеркало SessionStart: срабатывает при закрытии сессии. Используется для cleanup — удалить временные файлы, сбросить буфер метрик, закрыть соединение с внешним сервисом.

5 типов executor

shell — самый простой и распространённый. Hook — это bash-команда, harness её запускает, передаёт payload через stdin как JSON, читает stdout/stderr и интерпретирует exit-code: 0 — успех, 2 — блокирующая ошибка (stderr увидит модель, действие отменяется), всё остальное — non-blocking error (видит только пользователь). Подходит для всего, что уже написано как CLI-утилита: linter, security-scanner, logger.

prompt — внутри hook делается одиночный вызов LLM. Удобно, когда нужно умное решение, но запускать целого субагента избыточно: «оцени, безопасна ли эта команда, и верни yes/no». Дорого по latency и токенам, поэтому используют точечно.

agent — внутри hook поднимается полноценный субагент с собственным набором tools, который может многоходово работать над задачей. Например, верификатор изменений: запускается после Write, читает изменённый файл, прогоняет тесты, возвращает вердикт. Дороже, чем prompt, ограничен жёстким cap по числу turns, чтобы не зациклиться.

http — hook делает POST на указанный URL с payload в теле. Подходит для интеграции с внешними сервисами: webhook в Slack, событие в SIEM, проверка политики на корпоративном endpoint. Защищён SSRF-гардом: запросы на private / link-local / cloud-metadata адреса блокируются до подключения сокета.

function — in-process callback на языке самого SDK (TS или Python). Самый быстрый, но доступен только из SDK-режима: пользователь не может объявить function-hook в settings.json, его регистрирует код, который импортирует @anthropic-ai/claude-agent-sdk или claude_agent_sdk. Это та форма, в которой hooks показаны в примере из документации SDK.

Hooks и permissions

Глава «Permissions» описывает четырёхслойный гейт: hooks → deny-rules → mode → canUseTool. Hooks — первый слой, и именно PreToolUse — самый частый usecase для всей подсистемы. Документация Agent SDK формулирует это так: «hooks run first; a hook can deny outright but allow from a hook does not skip the rest of the chain». То есть hook имеет право заблокировать действие сразу — но если он скажет «разрешить», harness всё равно прогонит запрос через deny-rules, режим и canUseTool.

Конкретный пример. У вас в команде договорённость: Write разрешён только внутри рабочей директории проекта. Вы пишете shell-hook на PreToolUse с матчером Write: bash-скрипт читает JSON-payload, достаёт tool_input.file_path, сравнивает с $PWD и возвращает exit 0 (разрешить) или exit 2 со stderr вроде «file outside workspace, rewrite to a relative path» (запретить). На exit 2 модель увидит stderr как tool error и попробует переписать запрос — это и есть «детерминированный поведенческий guardrail» поверх стохастической модели.

Кто пишет hooks

Источников три, и они имеют разную семантику. Пользователь объявляет hooks в settings.json — глобальном или per-project. Это типичные «private rules» одного человека или одной команды: запрет на push в main, лог в свой Notion, интеграция с локальным linter. Управляются через /hooks UI и хранятся в обычном dotfile.

Плагины распространяются как пакеты с собственным манифестом и могут поставлять hooks в комплекте: например, плагин «company-security» приходит с PreToolUse-хуком, который сверяет команды с корпоративным policy-сервером. Hooks плагинов лежат в отдельном namespace и переживают политику strictPluginOnlyCustomization, которая блокирует пользовательские hooks.

Skills — это контент-первый формат: markdown с frontmatter, который описывает «как делать X». Skill может объявить hooks во frontmatter — и они зарегистрируются в момент установки skill в сессию. Скажем, skill «commit-with-conventional-commits» поставляет hook на PreToolUse, который проверяет формат коммит-сообщения. Если skill используется в субагенте, hook Stop автоматически превращается в SubagentStop, чтобы срабатывать на завершении субагента, а не основного цикла.

Async vs sync

Sync-хук блокирует loop: harness ждёт, пока hook отработает, и только потом продолжает. Это нужно для всего, что влияет на permissions: на PreToolUse вы обязаны получить ответ «разрешить или нет» до того, как инструмент пойдёт выполняться. Дефолтные таймауты для sync-хуков короткие — prompt-hook 30 секунд, agent-hook 60 секунд, http-hook 10 минут, shell — настраиваемо. Если hook не уложился — он трактуется как blocking error.

Async-хук — это shell-hook, который при запуске сразу возвращает {"async": true, "asyncTimeout": 60000} и продолжает работать в фоне. Harness регистрирует его в реестре pending-hooks, не блокирует loop и идёт дальше. На следующем повороте REPL harness проверяет реестр и, если фоновая задача завершилась, прикрепляет её stdout как pending-attachment к следующему сообщению пользователя. Это инструмент для side-effects — длинных проверок, отправки логов, медленных HTTP-запросов, — где результат полезен, но ждать его на каждом повороте нельзя.

"

Hooks are claude-cli's user-extensibility surface: a declarative way to plug commands, prompts, agent-queries, HTTP endpoints, or in-memory callbacks into well-defined points of the agent's lifecycle. They are the answer to «how does a user, a plugin, or a skill add behavior to claude-cli without forking the codebase?»

— claude-cli concepts, «06-hooks-extensibility»

Источники главы