
TL;DR
Hooks — это точки в жизненном цикле агента (Claude Code, GitHub Copilot CLI, VS Code Agent Mode), в которых выполняется ваш детерминированный код до, во время и после того, как модель что-либо делает. Большинство хуков полезны для наблюдения и инъекции контекста, но один — PreToolUse — особенный: он превращает недетерминированного агента в систему, которая обязана пройти через ваши gates (lint, typecheck, secrets-scan), прежде чем что-то записать.
В этой статье мы постараемся разобраться с нюансами, а именно, что такое контракт IO, паттерны применения и реальный кейс forced-lint gate, посмотрим, как заставить делать агента только то, что мы просим. Все примеры мы будем рассматривать для Claude Code (самый развитый набор: ~26 событий, типы хуков command/http/prompt/agent, matchers). Какие-то частности Copilot CLI и VS Code Agent Mode попробуем добавить в комментах. Кроме того надо учесть, что платформы развиваются — перед попыткой что-то внедрить проверяйте актуальный референс.
1. Зачем вообще хуки
LLM‑агенты недетерминированы, как говорится, by design. Модель может проигнорировать ошибку линтера, рационализировать сломанный тест, написать код с утечкой токена и уверенно сообщить, что всё хорошо. Как говорится, “AI lies pathologically”. Для одноразового скрипта это терпимо, для production‑кода — нет.
По сути, хуки — это механизм, который позволяет вклиниться в жизненный цикл агента своим кодом. Ваш shell-скрипт (или HTTP-эндпоинт, или вспомогательный sub-agent) исполняется в строго заданные моменты: старт сессии, отправка prompt, до tool call, после tool call, запуск sub-agent, остановка, перед компактификацией контекста, завершение сессии. В одних точках вы можете только наблюдать; в других — заблокировать действие агента до того, как оно произойдёт.
Хуки сейчас доступны в Claude Code, GitHub Copilot CLI и VS Code Agent Mode. Концепция везде одинаковая, но имена событий, формат конфига и набор полей различаются. В статье в основном будем рассматривать Claude Code.
Надо сразу оговориться, что мы сделаем акцент на модели жизненного цикла, каталоге событий с сигнатурами, а также разберем паттерн PreToolUse gate на ESLint и попробуем составить чек-лист для внедрения в команде.
2. Жизненный цикл агента
Чтобы говорить о хуках предметно надо понимать, что чат с агентом — это не “вопрос-ответ”, а цикл:

Ключевые свойства:
-
Агент — это loop. Он исполняется, пока есть tool calls. Нет больше tool calls — агент считает работу завершённой и отдаёт response.
-
Sub-agent — рекурсия того же типа. Внутри у него свои reasoning и свои tool calls, с таким же условием выхода.
-
Сессия может содержать много prompt’ов. Каждый новый prompt перезапускает agent loop, но контекст предыдущих итераций переносится.
-
Контекст ограничен. При приближении к лимиту платформа делает компактификацию (compaction) — это тоже точка хука.
Теперь наложим на цикл точки срабатывания хуков (Claude Code):

Еще нужно учесть, что разные хуки срабатывают разное количество раз: SessionStart срабатывает единожды, UserPromptSubmit — на каждый ваш prompt, PreToolUse/PostToolUse — на каждый tool call (а их за один prompt могут быть десятки).
3. Каталог событий
В Claude Code сейчас порядка 26 событий. Ниже — самые часто используемые; полный актуальный список — в референсе (en, ru).
|
Событие |
Когда |
Частота |
Может блокировать (exit 2)? |
Типичный use case |
|---|---|---|---|---|
|
|
Старт или resume сессии |
1 раз |
Нет (только |
Инъекция версий/стека/конвенций |
|
|
Каждая отправка prompt |
N раз |
Да |
Обогащение prompt, guardrails на ввод |
|
|
Перед tool call |
На каждый tool call |
Да |
Gates: lint, secrets, policy |
|
|
После успешного tool call |
На каждый tool call |
Нет (но поддерживает top-level |
Аудит, автотесты, телеметрия |
|
|
После упавшего tool call |
По факту |
Нет |
Доп. диагностика, ретрай-хинты |
|
|
Показан диалог разрешения |
По факту |
Да |
Авто-allow/deny политики без участия пользователя |
|
|
Tool call отклонён auto-mode классификатором |
По факту |
Нет (но можно |
Подсказать агенту, что попытка валидна и можно переформулировать |
|
|
Перед запуском sub-agent |
На каждый sub-agent |
Нет |
Логирование запуска sub-agent’а |
|
|
По завершении sub-agent |
На каждый sub-agent |
Да (заставить продолжить) |
Сбор результатов, гейт «не останавливайся, пока тесты красные» |
|
|
Агент собирается завершить ход |
1 раз на ход |
Да (заставить продолжить) |
Финальные проверки, не дать остановиться раньше времени |
|
|
Ход завершился с API-ошибкой |
По факту |
Нет |
Алерты, аналитика rate-limit/billing |
|
|
Перед компактификацией контекста |
По факту |
Да |
Сохранить артефакты до сжатия |
|
|
После компактификации |
По факту |
Нет |
Восстановить состояние, дозаписать контекст |
|
|
Загружен |
По факту |
Нет |
Аудит, compliance-логирование загрузок |
|
|
Сменилась рабочая директория |
По факту |
Нет |
Реактивная настройка окружения (direnv-style) |
|
|
Изменился отслеживаемый файл на диске |
По факту |
Нет |
Реакция на внешние правки ( |
|
|
Платформа просит подтверждение / idle |
По факту |
Нет |
Кастомные уведомления (Slack, звук) |
|
|
Закрытие сессии |
1 раз |
Нет |
Отчёт в трекер, cleanup |
Остальные события (TaskCreated/TaskCompleted, TeammateIdle, ConfigChange, WorktreeCreate/WorktreeRemove, Elicitation/ElicitationResult) — более узкие; см. референс.
Тут надо обратить внимание, что некоторые хуки “могут блокировать”. Например, PreToolUse — точка детерминированного контроля, где мы запрещаем действие модели до его выполнения и при этом получаем естественный механизм ретрая (агент видит причину и пробует снова). Stop/SubagentStop блокируют завершение хода (могут заставить продолжить), но не tool call. SessionStart/SessionEnd принимают только additionalContext и не умеют отказывать.
Copilot CLI: другой набор и другая нотация — всего 6 событий, имена в camelCase:
sessionStart,sessionEnd,userPromptSubmitted,preToolUse,postToolUse,errorOccurred. Блокирующий — толькоpreToolUse. Маппинг:UserPromptSubmit↔userPromptSubmitted, остальные — по аналогии.errorOccurredотдельного аналога в Claude Code не имеет (у Claude —PostToolUseFailure,StopFailure,Notification).VS Code Agent Mode: «chat hooks» — preview-фича, текущий набор близок к Copilot CLI; названия и поля сверяйте с актуальной документацией VS Code.
4. Модель конфигурации
В Claude Code хуки описываются под ключом hooks в .claude/settings.json (project-level, едет в git) или ~/.claude/settings.json (global). Минимальный конфиг:
{ "hooks": { "UserPromptSubmit": [ { "hooks": [ { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/log-prompt.sh" } ] } ] }}
Разберём структуру:
-
Внешний массив для каждого события — это группы хуков, объединённые общим
matcher. -
matcher— фильтр (см. ниже), необязателен; если пропущен — срабатывает на всё. -
Внутренний массив
hooks— собственно handlers; на одну группу можно навесить несколько. В Claude Code все подходящие под событие handlers запускаются параллельно, а идентичные (по команде — дляcommand-хуков, по URL — дляhttp) автоматически дедуплицируются. -
type: "command"— самый частый тип (есть ещёhttp,prompt,agent, см. §6). -
command— shell-команда. На Windows работает PowerShell ("shell": "powershell") или bash под WSL. -
timeout— таймаут в секундах. Дефолты зависят от типа хука:command— 600 с,prompt— 30 с,agent— 60 с. ДляPreToolUsegates лучше держать таймаут максимально коротким: один медленный хук растягивает каждый tool call. -
$CLAUDE_PROJECT_DIR— переменная, указывающая на корень проекта. Стандартный способ ссылаться на скрипты без относительных путей.
Поле matcher (далее по тексту — «фильтр») — ключевая часть конфига Claude Code. Что именно фильтруется — зависит от события:
|
Событие |
matcher фильтрует на |
Пример |
|---|---|---|
|
|
имя tool |
|
|
|
источник |
|
|
|
тип уведомления |
|
|
|
тип агента |
|
|
|
matcher не поддерживается |
срабатывает всегда |
Простые имена (буквы, цифры, _, |) трактуются как точное совпадение или список; более сложные — как regex. Без matcher хук на PreToolUse сработает на каждый tool call (Read, Glob, Grep, Bash, …) — это часто не то, что нужно.
Где лежат конфиги:
Claude Code: Project: .claude/settings.json (едет в git) .claude/settings.local.json (личные оверрайды, не в git) Global: ~/.claude/settings.json Plugin: ${CLAUDE_PLUGIN_ROOT}/.claude-plugin/plugin.json
Copilot CLI: конфиг живёт в
.github/hooks/(project) или соответствующих глобальных директориях. Структура — отдельный JSON-файл (например,.github/hooks/copilot-cli-policy.json) с полемversion: 1и блокомhooksпод camelCase-имена событий. Поле команды —bash(илиpowershell), плюсcwd,timeoutSec. Это другая схема, конфиги не взаимозаменяемы с Claude Code.
5. Контракт ввода-вывода
Каждый command-хук — это внешний процесс, который получает вход через stdin как JSON и может вернуть JSON в stdout (или сигнализировать через exit code).
Типичный вход (Claude Code, общие поля):
{ "session_id": "abc123", "transcript_path": "/path/to/chat/transcript.jsonl", "cwd": "/Users/me/projects/my-app", "permission_mode": "default", "hook_event_name": "UserPromptSubmit", "prompt": "Сделай рефакторинг модуля auth"}
Поля, специфичные для события, добавляются сверху:
-
UserPromptSubmit→prompt -
PreToolUse→tool_name,tool_input,tool_use_id -
PostToolUse→tool_name,tool_input,tool_response,tool_use_id -
SessionStart→source,model
transcript_path — отдельный бонус: в нём лежит весь текущий чат построчным JSONL, и его можно анализировать — оценивать расход токенов, считать частоту tool calls, искать паттерны в reasoning.
Выход. Есть два эквивалентных пути:
Путь A — exit code:
|
Exit code |
Поведение |
|---|---|
|
|
Успех. Stdout как JSON парсится; для |
|
|
Блокирующая ошибка. Stderr передаётся агенту (или пользователю — для не-блокирующих событий). Действие предотвращено |
|
другое |
Не-блокирующая ошибка. Stderr показывается с пометкой об ошибке хука |
⚠️ Важно: в Claude Code exit code 1 — не блокирует (в отличие от Unix-конвенции). Чтобы заставить агента переделать tool call, используйте exit code 2.
Путь B — JSON в stdout:
{ "hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "deny", "permissionDecisionReason": "ESLint errors must be fixed first", "additionalContext": "..." }}
Какое поле работает в каком событии:
-
additionalContext—SessionStart,UserPromptSubmit,PostToolUse(вливается в контекст). -
permissionDecision: "allow"|"deny"|"ask"|"defer"+permissionDecisionReason—PreToolUse. -
updatedInput—PreToolUseпозволяет переписать аргументы tool call. -
decision: "block"+reason—Stop,SubagentStop,PostToolUse(заставить продолжить или передать сообщение).
В bash де-факто стандарт для работы с JSON — jq. Минимальный хук, который читает prompt и пишет в лог:
#!/usr/bin/env bashset -euo pipefailINPUT=$(cat)PROMPT=$(echo "$INPUT" | jq -r '.prompt')echo "[$(date -Iseconds)] $PROMPT" >> "$CLAUDE_PROJECT_DIR/.claude/session.log"
Почему именно так:
-
Входные данные приходят на stdin, не в аргументах — читаем через
cat. -
jq -rснимает обрамляющие кавычки со строковых значений. -
set -euo pipefailловит ошибки сразу, а не молча.
Формировать output лучше тоже через jq, чтобы не заморачиваться с экранированием:
jq -n --arg ctx "$VALUE" \ '{hookSpecificOutput: {hookEventName: "SessionStart", additionalContext: $ctx}}'
Флаг --arg прокидывает значение безопасно — никакие кавычки или переводы строк внутри $VALUE не сломают JSON.
Copilot CLI: payload —
{timestamp, cwd, ...}(timestamp числом, не ISO), дляpreToolUse—toolNameиtoolArgs(JSON-строка, а не объект). Вывод для блокировки — на верхнем уровне:{"permissionDecision": "deny", "permissionDecisionReason": "..."}, без обёрткиhookSpecificOutput.
6. От inline-команды к внешнему скрипту (и к http/prompt/agent)
Простейший "command": "echo hello" прямо в JSON удобен только для Hello World. Для чего-то посерьезнее лучше, конечно, вынести в скрипт.
Рабочая структура проекта (Claude Code):
.claude/ settings.json hooks/ common/ ← общие утилиты (jq helpers, parse_input) session-start/ pre-tool-use/ post-tool-use/
Конфиг:
{ "hooks": { "UserPromptSubmit": [ { "hooks": [ { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/log-prompt.sh", "timeout": 10 } ] } ] }}
Использование $CLAUDE_PROJECT_DIR делает путь стабильным независимо от того, откуда запущен Claude Code.
И, конечно, лучше сразу проставить права на исполнение, чтобы не получить permission denied при запуске:
chmod +x .claude/hooks/**/*.sh
Где смотреть вывод хуков.
-
Claude Code CLI: запускайте с
claude --debug, ошибки и stderr хуков попадут в debug-лог. Для не-блокирующих событий и для хуков, упавших с не-2 exit code, первая строка stderr дополнительно показывается в транскрипте какhook error— это помогает замечать проблемы без--debug. Изменения вsettings.jsonобычно подхватываются автоматически file watcher’ом; команда/hooksоткрывает read-only просмотр текущих хуков (удобно убедиться, что ваш конфиг реально подхватился). Если что-то ведёт себя странно — перезапустите сессию. -
VS Code Agent Mode: Output panel → канал, связанный с Claude Code / Copilot. Туда падают stdout/stderr хуков.
-
Copilot CLI: в UI ничего не отображается; пишите лог в файл сами.
Типы хуков (Claude Code)
type: "command" — это самый частый, но не единственный вариант. В Claude Code есть ещё три типа.
type: "http" — POST-запрос на эндпоинт. Полезно, когда логика хука уже живёт как сервис (внутренний policy-сервер, security-gateway):
{ "type": "http", "url": "http://localhost:8080/hooks/pre-tool-use", "timeout": 30, "headers": { "Authorization": "Bearer $MY_TOKEN" }, "allowedEnvVars": ["MY_TOKEN"]}
allowedEnvVars — белый список переменных, доступных для интерполяции в headers. Без этого поля $MY_TOKEN не подставится — защита от утечки случайных env.
type: "prompt" — отдаёт решение второму LLM-вызову (быстрая модель):
{ "type": "prompt", "prompt": "Это безопасная команда? $ARGUMENTS", "timeout": 30}
$ARGUMENTS — placeholder, в который подставляется payload хука. Удобно для нечётких политик («можно ли это писать в production-код?»), где regex недостаточно.
type: "agent" — то же, но с полноценным sub-agent (есть Read/Grep/Glob), который может посмотреть код перед решением. Самый дорогой и самый умный вариант — для ситуаций, где статический скрипт не справится.
Командные хуки остаются основой; http/prompt/agent появляются, когда у политики есть собственный жизненный цикл или ей нужна семантика.
7. Пример №1: инъекция контекста через SessionStart
Первое по-настоящему полезное применение — избавить агента от tool calls за тривиальной информацией.
Без хука вы спрашиваете «какая версия Node у пользователя?» — агент запустит bash tool call (node --version), дождётся ответа, потратит токены и время. С хуком версия уже лежит в контексте с первого сообщения.
Скрипт get-env.sh:
#!/usr/bin/env bashset -euo pipefailNODE_VERSION=$(node --version 2>/dev/null || echo "not installed")GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "not a repo")CONTEXT="Node.js: ${NODE_VERSION}Git branch: ${GIT_BRANCH}OS: $(uname -s)"jq -n --arg ctx "$CONTEXT" \ '{hookSpecificOutput: {hookEventName: "SessionStart", additionalContext: $ctx}}'
Подключение (только для нового запуска, без resume — отфильтровано matcher’ом):
{ "hooks": { "SessionStart": [ { "matcher": "startup", "hooks": [ { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/get-env.sh" } ] } ] }}
На что нужно обратить внимание:
-
hookEventNameв output — именноSessionStart, а не имя другого события. -
permissionDecisionздесь не работает —SessionStartблокировку игнорирует. -
Экранирование — через
jq --arg, а не конкатенацию строк. Иначе перевод строки или кавычка в выводеgit rev-parseсломает JSON.
С хуком vs без. Без: агент видит вопрос, планирует tool call, запускает bash, получает вывод, формирует ответ — минимум один round-trip и несколько сотен токенов. С хуком: отвечает мгновенно из уже влитого контекста. На сессии с десятками запросов экономия кумулятивна.
Что ещё стоит инжектить через SessionStart:
-
Project rules (stack, запрещённые библиотеки, style guide).
-
Текущий sprint goal и активный тикет.
-
Ссылки на ADR и релевантную документацию.
-
Список feature flags, активных в окружении.
Это дешёвая, одноразовая инъекция с огромным эффектом на качество первых tool calls.
Бонус: экспорт ENV через CLAUDE_ENV_FILE. В SessionStart-хуке доступна специальная переменная CLAUDE_ENV_FILE — путь до файла, куда можно записать export-строки. Всё, что туда попадёт, будет подхвачено последующими Bash tool calls в этой сессии. Удобно «пробросить» агенту окружение (путь к node_modules/.bin, активный Node через nvm, credentials-профиль), не зашивая их в скрипты:
#!/usr/bin/env bashset -euo pipefailif [ -n "${CLAUDE_ENV_FILE:-}" ]; then echo 'export PATH="$PATH:./node_modules/.bin"' >> "$CLAUDE_ENV_FILE" echo 'export AWS_PROFILE=dev' >> "$CLAUDE_ENV_FILE"fi
Важно: >>, а не > — в том же файле могут писать другие SessionStart-хуки.
8. Пример №2: детерминированные gates через PreToolUse
Это самый показательный пример. Если из всего текста вы запомните одно — пусть это будет отсюда.
Проблема. Агент пишет код. Линтер подчёркивает ошибку. Человек не двинется дальше, пока не уберёт red squiggly — это норма. Агент потопает дальше: тупо рационализирует ошибку, пометит как «не критично», допишет соседний файл, отрапортует об успехе. Вы обнаружите это через полчаса, когда CI упадёт.
Решение. PreToolUse — единственная точка, где можно отклонить tool call до исполнения. Превращаем запись файла в детерминированный gate: если содержимое не линтится — tool call отклоняется, агент обязан исправить и попытаться снова.
Схема:

Цикл крутится, пока агент не напишет код, который пройдёт lint.
Конфиг с matcher — фильтрация на уровне settings, скрипт отвечает только за линт:
{ "hooks": { "PreToolUse": [ { "matcher": "Write|Edit", "hooks": [ { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/pre-tool-lint.sh", "timeout": 30 } ] } ] }}
matcher: "Write|Edit" означает, что хук срабатывает только на запись/редактирование файла — не на каждый Bash, Read или Glob. Без этого скрипт получал бы каждый tool call и ему пришлось бы фильтровать самому.
Скрипт pre-tool-lint.sh (Claude Code, через JSON-вывод):
#!/usr/bin/env bashset -euo pipefailINPUT=$(cat)FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')CONTENT=$(echo "$INPUT" | jq -r '.tool_input.content // .tool_input.new_string // empty')# Линтим только JS/TS — остальное пропускаемcase "$FILE_PATH" in *.js|*.jsx|*.ts|*.tsx) ;; *) jq -n '{hookSpecificOutput: {hookEventName: "PreToolUse", permissionDecision: "allow"}}' exit 0 ;;esac# ESLint через stdin — файла на диске ещё нет.# printf (а не echo) чтобы не интерпретировать \n/\t внутри контента.# Exit code ESLint = источник истины (0 — чисто, 1 — errors, 2 — конфиг сломан).LINT_OUTPUT=$(printf '%s' "$CONTENT" | npx --no-install eslint --stdin --stdin-filename "$FILE_PATH" --format compact 2>&1) && LINT_STATUS=0 || LINT_STATUS=$?if [ "$LINT_STATUS" -ne 0 ]; then REASON="ESLint errors must be fixed before writing ${FILE_PATH}:${LINT_OUTPUT}" jq -n --arg reason "$REASON" \ '{hookSpecificOutput: {hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: $reason}}' exit 0fijq -n '{hookSpecificOutput: {hookEventName: "PreToolUse", permissionDecision: "allow"}}'
На что тут стоит обратить внимание:
-
tool_input.file_pathиtool_input.content— стандартные поля payload Claude Code. ДляEdit«новая» часть лежит вnew_string(неcontent), поэтому пробуем оба. Важно: дляEditмы получаем только фрагмент (replacement text), он сам по себе может не быть валидным модулем — ESLint на нём будет спотыкаться по синтаксису. На практике строгий gate имеет смысл вешать наWrite(целый файл), а дляEdit— делатьPostToolUseс линтом уже записанного файла на диске. -
ESLint запускается через
--stdin, потому что файла на диске ещё нет: tool call заблокирован, мы линтим содержимое, которое агент собирался записать.printf '%s'вместоecho— чтобы\n/\tвнутри кода не были интерпретированы shell. -
Источник истины — exit code ESLint:
0— чисто,1— errors,2— сломанная конфигурация. Парсить--format compactрегуляркой («есть ли слово Error») ненадёжно: слово может встречаться в тексте правил и в сообщениях про warnings. -
permissionDecision: "deny"+permissionDecisionReason— это та форма ответа, которую Claude Code понимает. ТекстpermissionDecisionReasonпойдёт прямо в reasoning агента, и он попробует исправить. -
Exit code 0 — потому что мы возвращаем валидный JSON. Хук отработал успешно, решение «deny» — это часть его нормального вывода.
Альтернатива через exit 2 — короче, без JSON:
LINT_OUTPUT=$(printf '%s' "$CONTENT" | npx --no-install eslint --stdin --stdin-filename "$FILE_PATH" --format compact 2>&1) && LINT_STATUS=0 || LINT_STATUS=$?if [ "$LINT_STATUS" -ne 0 ]; then printf 'ESLint errors must be fixed before writing %s:\n%s\n' "$FILE_PATH" "$LINT_OUTPUT" >&2 exit 2fi
Exit code 2 + stderr → агент получит то же сообщение и повторит попытку. Удобно, когда не нужны updatedInput или additionalContext — только заблокировать.
Что происходит в реальности: агент пытается написать firebase-admin.ts, хук отклоняет со списком lint-ошибок, агент читает сообщение, идёт изучать правила, правит, пробует снова, снова отклоняется по другой ошибке, опять правит — и так до прохождения. Например, в логе видно 4–6 итераций на один файл, но результат: файл, который линтится при записи, а не через полчаса на CI.
Тезис. PreToolUse заставляет AI работать как разработчик: не уходим от red squiggly. Нельзя двигаться к следующему файлу, пока текущий не чист. Это перевод агента из режима «старается» в режим «обязан».
Бонус: updatedInput — тихая правка аргументов. Кроме allow/deny/ask PreToolUse умеет ещё и переписывать tool_input перед исполнением — через поле updatedInput в hookSpecificOutput. Пример: нормализовать путь к файлу, добавить --dry-run к опасной команде, прогнать контент через prettier и записать уже отформатированное:
FORMATTED=$(printf '%s' "$CONTENT" | npx --no-install prettier --stdin-filepath "$FILE_PATH")jq -n --arg file "$FILE_PATH" --arg content "$FORMATTED" '{ hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput: { file_path: $file, content: $content } }}'
updatedInput заменяет объект аргументов целиком, поэтому неизменённые поля нужно включать вручную. С allow это работает как auto-approve, с ask — покажет пользователю уже модифицированный вариант.
Цепочки gates. На одну группу matcher можно повесить несколько handlers — каждый исполняется независимо и любой может вернуть deny. Промышленный набор для typescript-проекта:
-
ESLint — синтаксис и style.
-
tsc --noEmit— типы. -
Secrets scan (regex на токены).
-
Targeted unit tests для затронутого модуля (через
tsc/jest --findRelatedTests).
Каждый — отдельный handler в массиве hooks под одним matcher’ом. Все они запустятся параллельно, результаты объединяются по precedence deny > defer > ask > allow — то есть достаточно, чтобы любой gate вернул deny, и tool call блокируется. Это делает цепочку gates дешевле по wall-time: общий таймаут примерно равен самому медленному из них, а не сумме.
Copilot CLI: идея та же, но JSON-вывод другой — поля
permissionDecision/permissionDecisionReasonна верхнем уровне, безhookSpecificOutput. И поля payload —toolName/toolArgs(гдеtoolArgs— JSON-строка, нужно ещё разjq fromjson). Ещё нюанс: из трёх значенийpermissionDecision("allow","deny","ask") сейчас обрабатывается только"deny"— остальные игнорируются, поэтому хуком можно только запретить, но не авто-одобрить.
9. Другие продвинутые паттерны
Короткий каталог поверх описанных двух.
Policy enforcement (PreToolUse + matcher на Bash). Запрет опасных команд (rm -rf, curl … | sh, chmod 777). Алгоритм: достаём tool_input.command, сверяем с blacklist, возвращаем deny. Поле if в конфиге Claude Code позволяет дополнительно фильтровать без запуска скрипта: "if": "Bash(rm *)".
Secrets guard (PreToolUse на Write|Edit). Regex по tool_input.content (AKIA[0-9A-Z]{16}, sk-[A-Za-z0-9]{32,}, -----BEGIN PRIVATE KEY----- и т. п.). Блокируем запись, возвращаем агенту подсказку: «токен обнаружен, вынеси в env».
Аудит и телеметрия (PostToolUse). Пишем в JSONL лог: {tool_name, duration_ms, success, files_touched}. Прогоняем через обычную аналитику — выясняем, какие tool calls чаще всего не удаются, где агент буксует.
Автотестирование (PostToolUse). После Write/Edit файла src/auth/login.ts запускаем jest --findRelatedTests src/auth/login.ts. Результат через additionalContext подаётся обратно в reasoning агента.
«Не останавливайся, пока тесты красные» (Stop). Хук на Stop запускает npm test; если упало — возвращает {"decision": "block", "reason": "Tests are failing: ..."} или, эквивалентно, завершается с exit 2 и текстом ошибки в stderr. Агент не сможет завершить ход, пока не починит. То же для SubagentStop — гарант, что delegated работа доведена до конца.
Сохранить контекст до компактификации (PreCompact). Перед сжатием контекста выгружаем важное (текущее состояние таска, ключевые решения) в файл, а после — SessionStart (на source: "compact") подгружает обратно. Преемственность работы через границу компактификации.
Интеграции (SessionEnd). В конце сессии шлём в Slack сводку: «сделано X tool calls, Y denied, Z ошибок». Или пишем запись в Linear/Jira о том, что агент закрыл задачу.
Sub-agent control (SubagentStart). Логирование запускаемых подагентов; matcher по типу даёт фильтрацию ("matcher": "Plan|Explore").
Для всех этих паттернов подход одинаков: парсим payload события, применяем правило, возвращаем JSON или exit 2.
10. Ограничения и edge cases
Что нужно держать в голове, чтобы не удивляться.
Что нельзя блокировать. SessionStart, SessionEnd, Notification, SubagentStart, PostToolUseFailure — exit 2 не даёт блокировки, stderr попадёт только пользователю. Полная таблица — в референсе hooks.
Производительность. PreToolUse — на каждый tool call (после фильтра matcher). Полноценный запуск ESLint/tsc может занять секунду-две, и если таких gates несколько, каждый write оплачивается задержкой. Решения: кэш зависимостей, запуск только релевантных проверок (matcher + проверка путей внутри), вынос тяжёлых проверок в PostToolUse там, где блокирующая семантика не нужна.
Async и timeout. Поле timeout (секунды) ограничивает выполнение хука. Дополнительно есть async: true (запустить в фоне, не дожидаться) и asyncRewake: true (запустить в фоне, пробудить агента, если хук вышел с exit 2). Удобно для длинных проверок: запускаем тесты в фоне, агент работает; тесты упали — будим и просим починить.
Кросс-платформенность. На Windows без WSL нужен "shell": "powershell" или соответствующее поле powershell. Либо держать два handler’а с условием по платформе, либо писать скрипты на Python/Node.
Различия CLI и IDE. В Claude Code CLI правки в settings.json обычно подхватываются автоматически file watcher’ом; /hooks — это read-only browser для проверки текущей конфигурации, а не команда перезагрузки. Если хук явно не срабатывает — быстрее всего убедиться, что файл валиден, и перезапустить сессию. В IDE-интеграциях поведение зависит от расширения.
Race conditions между handlers. Несколько handlers на одно событие в Claude Code запускаются параллельно, без явной синхронизации. Если два пишут в один файл — race. Либо разделяйте артефакты, либо делайте операции атомарными (>> + flock на Linux).
Версионирование. Платформы развиваются, поля переименовываются. Прикрепляйте конфиги хуков к конкретной версии CLI/IDE, тестируйте на обновлении отдельной CI-job’ой.
11. Безопасность
Напомним, хук — это произвольный код, запускаемый автоматически каждый раз, когда срабатывает соответствующее событие. Если конфиг хука лежит в репозитории (.claude/settings.json, .github/hooks/), то клонирование репозитория и первый же prompt запускает этот код у каждого, кто открыл проект в Claude Code или Copilot CLI. Прямая аналогия — .git/hooks/, только автоматически и без явного действия пользователя.
Supply chain-риски:
-
Форк популярного репо с добавленным хуком, который читает
~/.ssh/id_rsa. -
PR в open-source проект с «безобидной» правкой в
.claude/hooks/scripts/. -
Скомпрометированная зависимость, которая прописывает хук в глобальный
~/.claude/settings.json. -
HTTP-хук, который тихо отправляет содержимое каждого
tool_inputна внешний сервер.
Минимальный набор мер:
-
Code review хуков как отдельный класс изменений. Любое изменение в
.claude/,.github/hooks/— обязательный ревьюер из security. -
CI-проверка. Pipeline-шаг, который падает при изменениях в этих путях без соответствующего label’а.
-
Разграничение project vs global. Project-уровень — потенциально hostile, global — ваше доверенное окружение. Не смешивайте.
-
Белый список команд. Внутренний helper-скрипт, который принимает только команды из allow-list; все хуки вызывают его, а не произвольный bash.
-
Аудит первого запуска незнакомого репо. Перед первым
claudeв чужом проекте выставьте"disableAllHooks": trueв своих user- или local-settings, прочитайте, что лежит в.claude/settings.jsonпроекта, и только после этого включайте хуки обратно. Обратите внимание:disableAllHooksуважает managed settings hierarchy — корпоративные managed-хуки отключить только на уровне managed policy. -
Безопасные HTTP-хуки. Для
type: "http"всегда указывайтеallowedEnvVars(белый список переменных, которые могут попасть в headers/url). Без этого$VARв headers интерполироваться не будет — это защита от случайной утечки токенов. -
permission_modeв payload. Хук получает текущий режим (default,bypassPermissions,acceptEdits, …). Можно строить поведение: например, вbypassPermissionsусиливать gates вместо ослабления. -
Корпоративный контроль через managed settings. На уровне организации Claude Code умеет флаг
allowManagedHooksOnly— он блокирует user/project/plugin-хуки, оставляя только те, что пришли из managed policy. Плагины из принудительно включённогоenabledPlugins— исключение: через него IT может раздавать прошедшие ревью «доверенные» хуки.
Это та же гигиена, что с .git/hooks, только ставки выше: хуки запускаются чаще и получают на вход ваш prompt и transcript.
12. Hooks vs альтернативы
Разработчик, впервые увидевший хуки, путает их с инструкциями в CLAUDE.md/AGENTS.md и с MCP-серверами. Это разные механизмы.
Hooks vs CLAUDE.md / AGENTS.md / system prompt. Инструкция — это декларативное правило, например, «всегда линтуй код перед записью». Агент её читает и старается следовать. Но модель недетерминирована — иногда забывает, иногда обходит. Хук — это детерминированный gate: физически не пропустит tool call, пока условие не выполнено. Правило: если вам жизненно важно, чтобы условие соблюдалось — это хук. Если хватает «как правило, делай так» — CLAUDE.md.
Hooks vs MCP-серверы. MCP расширяет набор tools, которые доступны агенту. «Дай агенту возможность запрашивать данные из Jira» — MCP. Hooks контролируют жизненный цикл. «Не пропусти tool call без lint» — хук. Они комплементарны: MCP-tool тоже можно обернуть в PreToolUse gate (matcher "mcp__jira__.*"), и сам MCP-сервер может реализовывать политики на своей стороне.
Hooks vs кастомные tools. Tool вызывается агентом по его решению. Хук срабатывает детерминированно, вне решения агента. Если нужно, чтобы агент сам мог запускать ESLint как tool — это tool. Если нужно, чтобы ESLint запускался всегда перед записью, без участия агента — это хук.
Матрица выбора:
Нужно «агент обязан» ─► hooks (PreToolUse)Нужно «агент может» ─► custom tool / MCPНужно «агент знает» ─► system prompt / CLAUDE.md / SessionStart injectНужно «внешний источник данных» ─► MCP
13. Чек-лист внедрения
Практический минимум, с которого стоит начать в новом проекте.
Базовый набор хуков (Claude Code):
-
SessionStart(matcher"startup") → инъекция stack, версий, конвенций, активных feature flags. -
PreToolUse(matcher"Write|Edit") → lint + typecheck gate (главный, по важности — как все остальные вместе). -
PreToolUse(matcher"Bash",if: "Bash(rm *)") → защита от опасных команд. -
PostToolUse(matcher"Write|Edit") → targeted unit tests на затронутый модуль (async: true, чтобы не блокировать). -
Stop→ финальная проверка тестов (не дать остановиться, пока CI красный). -
SessionEnd→ запись сводки в трекер команды.
Конвенции структуры:
.claude/ settings.json hooks/ common/ ← общие утилиты (jq helpers, parse_input) session-start/ pre-tool-use/ post-tool-use/ stop/ README.md ← как добавлять хуки, как отлаживать
Onboarding: короткий документ в репо — что такое хуки в вашем проекте, какие gates работают, как их отключить локально ("disableAllHooks": true в .claude/settings.local.json), куда смотреть при срабатывании.
Мониторинг. Счётчики: сколько tool calls в день, сколько denied, по каким причинам. Если denied-rate растёт — проверьте, не деградировала ли модель или не устарели ли правила.
План миграции. Платформы развиваются быстро. Тестируйте хуки на обновлении CLI/IDE отдельной CI-job’ой; следите за переименованиями полей в release notes.
Чек-лист перед мержем:
-
[ ] Хук написан и протестирован (Claude Code:
claude --debug). -
[ ] Использован
$CLAUDE_PROJECT_DIR, а не относительные пути. -
[ ] Подобран корректный
matcher(хук не срабатывает на лишних tool calls). -
[ ] Указан
timeout(дляPreToolUse— желательно ≤ 5 секунд от хука, ≤ 500 ms от лёгких проверок). -
[ ] Есть сценарий «что делает, когда проверка падает».
-
[ ] Добавлен в README.
-
[ ] Security-ревьюер посмотрел.
14. Заключение
На самом деле всё не так уж и сложно — основное, что нужно запомнить, это простая эвристика из трёх пунктов:
-
Жизненный цикл агента — это loop reasoning+tool calls, с возможным вложенным sub-agent loop и точкой компактификации. Хуки подключаются в заранее определённые точки.
-
Контракт IO — stdin JSON на вход, stdout JSON на выход (или exit 2 + stderr),
jqкак лингва-франка. Какие поля какой хук понимает — сверяйтесь с таблицей. -
PreToolUse— самая полезная точка детерминированного контроля.permissionDecision: "deny"(илиexit 2) позволяют заставить агента переделывать работу, пока она не дойдёт до нужного вам threshold качества.
Нужно помнить, что production-ценность агентов зависит не только от «умности» модели, но и от gates, которые модель не может обойти. Начните с одного — PreToolUse с ESLint, тестами или другими проверками на Write|Edit. Когда увидите, как агент реально проходит через несколько итераций исправлений вместо того, чтобы просто забить — вы поймёте, почему этот хук считается важнейшим инструментом в AI-тулчейне. И почему трудно представить себе серьёзный проект с агентом без него.
Что еще стоит посмотреть
-
Completely understand hooks in less than 20 minutes — https://www.youtube.com/watch?v=03CfGf9iw_U
-
Claude Code Hooks Reference (en) — https://docs.claude.com/en/docs/claude-code/hooks
-
Практическое введение — Claude Code Hooks Guide — https://docs.claude.com/en/hooks-guide
-
Различия GitHub Copilot CLI отмечены во врезках; референс — https://docs.github.com/en/copilot/reference/hooks-configuration
ссылка на оригинал статьи https://habr.com/ru/articles/1028570/