Hooks — события жизненного цикла

Если навыки и субагенты расширяют то, что умеет Claude, то хуки контролируют как он это делает — и делают это детерминированно. Хук — это обычная shell-команда (или HTTP-запрос, MCP-инструмент, даже вызов LLM), которую Claude Code запускает автоматически в определённые моменты жизненного цикла сессии. Модель не участвует в этом решении: хук срабатывает всегда, независимо от того, что думает Claude.

Это принципиальное отличие от инструкций в CLAUDE.md или навыках. Написать «всегда запускай prettier после редактирования файлов» — это просьба, которую Claude может проигнорировать в длинной цепочке инструментов. Хук с тем же правилом — это гарантия.


Анатомия хука

Хуки живут в "hooks" внутри любого settings.json (пользовательского, проектного или локального). Структура трёхуровневая:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/format.sh",
            "timeout": 30
          }
        ]
      }
    ]
  }
}
  • Ключ верхнего уровня — имя события (PostToolUse, PreToolUse, SessionStart и т.д.)
  • Матчер (matcher) — фильтр: к каким инструментам или ситуациям применять. "Edit|Write" — только для этих двух инструментов; "Bash(rm *)" — только для bash-команд с rm; "mcp__github__.*" — для всех инструментов GitHub MCP-сервера.
  • Массив hooks — список команд, выполняющихся последовательно при совпадении.

Хуки можно коммитить в репозиторий (.claude/settings.json) — команда получает их вместе с кодом. Персональные правила (пути к локальным скриптам, токены) — в .claude/settings.local.json, который попадает в .gitignore.

flowchart TD A[Открытие сессии] --> B[[SessionStart]] B --> C[Пользователь вводит промпт] C --> D[[UserPromptSubmit]] D --> E{Агентный цикл} E --> F[[PreToolUse]] F --> G{Заблокирован?} G -- нет --> H[Инструмент выполняется] G -- да --> E H --> I[[PostToolUse]] I --> E E --> J[[Stop]] J --> K{Остановиться?} K -- нет --> E K -- да --> L[Ответ готов] L --> C C --> M[[SessionEnd]]
flowchart TD
    A[Открытие сессии] --> B[[SessionStart]]
    B --> C[Пользователь вводит промпт]
    C --> D[[UserPromptSubmit]]
    D --> E{Агентный цикл}
    E --> F[[PreToolUse]]
    F --> G{Заблокирован?}
    G -- нет --> H[Инструмент выполняется]
    G -- да --> E
    H --> I[[PostToolUse]]
    I --> E
    E --> J[[Stop]]
    J --> K{Остановиться?}
    K -- нет --> E
    K -- да --> L[Ответ готов]
    L --> C
    C --> M[[SessionEnd]]
Порядок срабатывания хуков в жизненном цикле Claude Code

Типы событий

События делятся на три ритма:

Сессионные (один раз за сессию):

  • SessionStart — сразу после открытия или возобновления. Удобен для загрузки окружения и подтягивания актуальных данных.
  • SessionEnd — при закрытии. Для логирования, очистки временных файлов.
  • Setup — при init или maintenance.

На каждый промпт (один раз на пользовательский запрос):

  • UserPromptSubmit — сразу после отправки промпта, до обработки Claude. Можно блокировать или модифицировать.
  • Stop — когда Claude собирается завершить ответ. Можно заставить его продолжить.

Агентный цикл (на каждый вызов инструмента):

  • PreToolUse — до выполнения. Самый мощный: можно заблокировать вызов, изменить аргументы или залогировать.
  • PostToolUse — после выполнения. Инструмент уже отработал; можно добавить контекст или инициировать форматирование.
Проверь себя
PreToolUse-хук сработал на вызов инструмента Bash и вернул exit code 2. Bash должен был выполнить `git push`. Что произойдёт с командой?

Как хук общается с Claude Code

Хук-скрипт получает на stdin JSON с контекстом события. Для PreToolUse это выглядит примерно так:

{
  "session_id": "abc123",
  "hook_event_name": "PreToolUse",
  "tool_name": "Bash",
  "tool_input": {
    "command": "rm -rf ./dist"
  },
  "cwd": "/home/user/myproject",
  "permission_mode": "default"
}

Хук возвращает решение через код выхода и JSON в stdout:

Код выходаЗначение
0Успех. Stdout парсится как JSON с инструкциями.
2Блокирующая ошибка. Stderr передаётся Claude как контекст.
Любой другойНе блокирующая ошибка; сессия продолжается, в лог пишется предупреждение.

Критически важно: JSON в stdout обрабатывается только при exit 0. Если скрипт упал с кодом 1 — его вывод игнорируется.

Проверь себя
Хук вернул exit 0 и JSON с `permissionDecision: "deny"` в PostToolUse. Инструмент Edit уже записал файл. Будет ли он заблокирован?

Блокировка и модификация

Самый частый сценарий для PreToolUse — заблокировать опасный вызов:

#!/bin/bash
# .claude/hooks/block-destructive.sh
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')

if echo "$COMMAND" | grep -qE 'rm -rf|DROP TABLE|truncate'; then
  jq -n '{
    hookSpecificOutput: {
      hookEventName: "PreToolUse",
      permissionDecision: "deny",
      permissionDecisionReason: "Деструктивная команда заблокирована политикой проекта"
    }
  }'
else
  exit 0
fi

При exit 0 без JSON-вывода Claude Code просто продолжает работу. Хук обязан что-то писать в stdout только тогда, когда хочет повлиять на поведение.

Второй вариант — не блокировать, а изменить аргументы инструмента. Поле updatedInput подменяет то, что получит инструмент:

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "allow",
    "updatedInput": { "command": "npm run build -- --no-cache" }
  }
}

Для PostToolUse блокировка недоступна (инструмент уже отработал), зато можно добавить дополнительный контекст, который Claude увидит рядом с результатом:

{
  "hookSpecificOutput": {
    "hookEventName": "PostToolUse",
    "additionalContext": "Этот файл генерируется автоматически. Редактируй src/schema.ts и запусти npm run generate."
  }
}

Три практических паттерна

1. Автоформатирование после редактирования

Классический случай: Claude написал код, хук тут же форматирует файл.

#!/bin/bash
# .claude/hooks/auto-format.sh
FILE=$(cat | jq -r '.tool_input.file_path // empty')
if [[ "$FILE" =~ \.(ts|tsx|js|jsx)$ ]]; then
  prettier --write "$FILE" 2>/dev/null
fi
exit 0
{
  "hooks": {
    "PostToolUse": [{
      "matcher": "Edit|Write",
      "hooks": [{ "type": "command", "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/auto-format.sh", "timeout": 15 }]
    }]
  }
}

2. Линт-гейт: заставить Claude починить то, что сломано

PostToolUse не может отменить уже выполненный инструмент, но exit code 2 передаёт ошибку обратно Claude:

#!/bin/bash
# .claude/hooks/lint-gate.sh
INPUT=$(cat)
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')

if [[ "$FILE" =~ \.ts$ ]]; then
  if ! npx tsc --noEmit --skipLibCheck "$FILE" 2>/dev/null; then
    npx tsc --noEmit --skipLibCheck "$FILE" >&2
    exit 2
  fi
fi
exit 0

При exit 2 Claude Code передаёт stderr обратно модели. Claude видит ошибки TypeScript и обычно сразу их исправляет.

3. Аудит: лог всех действий агента

#!/bin/bash
# ~/.claude/hooks/audit.sh
echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) | $(cat | jq -c '.')" \
  >> ~/.claude/audit.jsonl
exit 0
{
  "hooks": {
    "PreToolUse": [{
      "hooks": [{
        "type": "command",
        "command": "${HOME}/.claude/hooks/audit.sh",
        "async": true
      }]
    }]
  }
}

Поле "async": true запускает скрипт фоново, не блокируя агентный цикл — идеально для логирования, где задержка не нужна.

Проверь себя
Хук с `async: true` завершился с exit 2. Его stderr содержит ошибки линтера. Увидит ли Claude эти ошибки?

SessionStart: подготовка окружения

SessionStart — единственное событие, которое может внедрить контекст до первого промпта. Это делает его незаменимым для подгрузки актуальных данных:

#!/bin/bash
# .claude/hooks/session-start.sh
BRANCH=$(git branch --show-current 2>/dev/null || echo "неизвестна")
UNCOMMITTED=$(git status --short 2>/dev/null | wc -l | tr -d ' ')
LAST_COMMIT=$(git log --oneline -1 2>/dev/null || echo "нет коммитов")

jq -n \
  --arg branch "$BRANCH" \
  --arg uncommitted "$UNCOMMITTED" \
  --arg last "$LAST_COMMIT" '{
  hookSpecificOutput: {
    hookEventName: "SessionStart",
    additionalContext: ("Ветка: " + $branch + "\nНезакоммиченных файлов: " + $uncommitted + "\nПоследний коммит: " + $last),
    sessionTitle: $branch
  }
}'

Claude начинает сессию уже зная контекст репозитория — без лишних вопросов и git status в первом запросе.

Кроме additionalContext, SessionStart умеет:

  • Устанавливать переменные окружения через CLAUDE_ENV_FILE
  • Задавать заголовок сессии (sessionTitle)
  • Перезагружать навыки (reloadSkills: true) — полезно если навыки хранятся в git и обновляются

Типы хуков помимо command

Документация описывает пять типов, command — лишь самый распространённый:

ТипЧто делает
commandShell-скрипт. Читает stdin, пишет stdout.
httpPOST-запрос на endpoint. Тело — тот же JSON контекста.
mcp_toolВызывает инструмент на подключённом MCP-сервере.
promptОднотурновый LLM-запрос, возвращает yes/no решение.
agentЗапускает субагент с доступом к инструментам для сложной проверки.

http хук особенно полезен для команд: централизованный сервер получает все события, логирует их, применяет политики — и всё это без установки скриптов на каждой машине.


Просмотр всех хуков

Команда /hooks в Claude Code открывает read-only браузер хуков — показывает все настроенные правила, их события, матчеры, типы и источники (User / Project / Local / Plugin). Незаменимо при отладке: сразу видно, какой хук сработает на каком событии и нет ли конфликтующих правил из разных уровней иерархии.



Быстрое повторение
Чем exit код 2 в хуке отличается от других ошибочных кодов (1, 3, ...)?
Быстрое повторение
В чём главное различие между PreToolUse и PostToolUse?
Быстрое повторение
Почему хук более надёжен, чем инструкция «всегда запускай prettier» в CLAUDE.md?

See also

Источники

  1. Claude Code Hooks — официальная документация