МОДУЛЬ 06/УРОК

Хуки

Hooks

Hooks - это автоматические скрипты, которые запускаются в ответ на определённые события во время сессий Claude Code. Они позволяют реализовать автоматизацию, валидацию, управление разрешениями и пользовательские сценарии работы.

Обзор

Hooks - это автоматические действия (shell-команды, HTTP webhooks, промпты к LLM, вызовы MCP-инструментов или обращения к subagent), которые выполняются при возникновении определённых событий в Claude Code. Они принимают на вход JSON и возвращают результаты через exit codes и JSON на выходе.

Ключевые возможности:

  • Автоматизация, управляемая событиями
  • Ввод/вывод в формате JSON
  • Поддержка hook-типов command, http, mcp_tool, prompt и agent
  • Сопоставление по шаблонам для hooks, привязанных к конкретным инструментам

Конфигурация

Hooks настраиваются в файлах settings со строго заданной структурой:

  • ~/.claude/settings.json - пользовательские настройки (для всех проектов)
  • .claude/settings.json - настройки проекта (доступны для совместной работы, попадают в commit)
  • .claude/settings.local.json - локальные настройки проекта (не попадают в commit)
  • Managed policy - настройки на уровне организации
  • hooks/hooks.json в plugin - hooks в области видимости plugin
  • Frontmatter у Skill/Agent - hooks жизненного цикла компонента

Базовая структура конфигурации

json
{
  "hooks": {
    "EventName": [
      {
        "matcher": "ToolPattern",
        "hooks": [
          {
            "type": "command",
            "command": "your-command-here",
            "timeout": 60
          }
        ]
      }
    ]
  }
}

Ключевые поля:

FieldDescriptionExample
matcherPattern to match tool names (case-sensitive)"Write", "Edit|Write", "*"
hooksArray of hook definitions[{ "type": "command", ... }]
typeHook type: "command" (bash), "prompt" (LLM), "http" (webhook), "mcp_tool" (MCP tool invocation, v2.1.118+), or "agent" (subagent)"command"
commandShell command to execute"$CLAUDE_PROJECT_DIR/.claude/hooks/format.sh"
timeoutOptional timeout in seconds. Defaults: 600 for command/http/mcp_tool, 30 for prompt, 60 for agent.30
onceIf true, run the hook only once per sessiontrue
asyncIf true, runs in the background without blockingtrue
asyncRewakeIf true, runs in the background and wakes Claude on exit code 2. Implies async.true
shellAccepts "bash" or "powershell". Defaults to "bash", or to "powershell" on Windows when Git Bash isn't installed."bash"
statusMessageCustom spinner message displayed while the hook runs"Formatting…"

Примечание: Некоторые события уменьшают timeout по умолчанию. UserPromptSubmit снижает значение по умолчанию для command, http и mcp_tool до 30 секунд, а MessageDisplay - до 10 секунд. Hook-и SessionEnd используют общий бюджет в 1,5 секунды; если в ваших настройках задан больший timeout для отдельного hook, Claude Code увеличивает общий бюджет до соответствующего значения, но не более 60 секунд.

Шаблоны matcher

PatternDescriptionExample
Exact stringMatches specific tool"Write"
Regex patternMatches multiple tools"Edit|Write"
Comma-separatedMatches any listed tool (v2.1.191+)"Write,Edit"
WildcardMatches all tools"*" or ""
MCP toolsServer and tool pattern"mcp__memory__.*"

Матчеры сопоставляются строго (v2.1.195+). Идентификатор с дефисом (например, имя MCP-инструмента, содержащее дефис) больше не срабатывает случайно как подстрока для другого инструмента. Матчеры со списком через запятую, такие как "Write,Edit", теперь срабатывают на любом инструменте из списка - в более ранних сборках они молча не срабатывали никогда.

Значения матчера InstructionsLoaded:

Matcher ValueDescription
session_startInstructions loaded at session startup
nested_traversalInstructions loaded during nested directory traversal
path_glob_matchInstructions loaded via path glob pattern matching

Сужение через условия if (пути в аргументах инструмента)

Поле matcher выбирает hook по имени инструмента ("Write", "Edit|Write", "*"). Чтобы фильтровать точнее - по аргументам инструмента, например запускать hook только когда правка затрагивает src/, или блокировать чтение секретных файлов, - добавьте условие if к конкретному обработчику hook. Это не то же самое, что matcher по имени инструмента: matcher определяет, какой инструмент, а if - какой именно вызов.

В if используется синтаксис правил разрешений (ToolName(pattern)), который проверяется одновременно по имени инструмента и его аргументам. Для Read/Edit/Write шаблон пути подчиняется семантике gitignore с теми же якорями, что и в правилах разрешений: голое имя вроде .env совпадает на любой глубине, src/** отсчитывается от текущей директории, /src/** - от корня проекта, ~/... - от домашней директории, а //... - абсолютный путь в файловой системе.

Поле if располагается на уровне обработчика hook - рядом с type и command внутри массива hooks, - а не на уровне matcher:

json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "if": "Edit(src/**)",
            "command": "./hooks/lint-src.sh"
          }
        ]
      },
      {
        "matcher": "Read",
        "hooks": [
          {
            "type": "command",
            "if": "Read(.env)",
            "command": "./hooks/block-secret-read.sh"
          }
        ]
      }
    ]
  }
}

Примеры допустимых шаблонов if: Edit(src/**) (правки в src/), Read(~/.ssh/**) (чтение любых SSH-ключей), Read(.env) (любой .env в текущем каталоге или ниже по дереву), Bash(git push *) (только подкоманды git push).

Обновление v2.1.214: односегментный шаблон dir/** в условии if у hook (например, Edit(src/**)) теперь соответствует только <cwd>/dir - а не одноимённому каталогу на любой глубине дерева. Ранее src/** также совпадал с foo/src/**. Если нужно сопоставление на любой глубине, используйте **/dir/**. Важно: это сужение действует только для условий if: у hook и для автоодобрения по allow-правилам - правила разрешений deny/ask по-прежнему сопоставляют dir/** на любой глубине.

Типы hook

Claude Code поддерживает пять типов hook:

Command Hooks

Тип hook по умолчанию. Выполняет shell-команду и обменивается данными через stdin/stdout в формате JSON и коды завершения.

json
{
  "type": "command",
  "command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/validate.py\"",
  "timeout": 60
}

Exec-форма (args)

Добавлено в v2.1.139.

Вместо shell-формы "command": "..." command hook может запускать бинарный файл напрямую через execve(), используя массив args. Парсинг shell при этом не выполняется, поэтому плейсхолдеры путей не требуется экранировать кавычками, а сама конфигурация защищена от уязвимостей типа shell injection.

json
{
  "type": "command",
  "args": ["python3", "$CLAUDE_PROJECT_DIR/.claude/hooks/validate.py", "--strict"],
  "timeout": 60
}

Эти две формы взаимоисключающие - hook с одновременно заданными command и args отклоняется при загрузке конфигурации. Используйте command, когда нужны конвейеры, перенаправления, цепочки через &amp;&amp; или раскрытие шелла; используйте args, когда вызываете один бинарник с аргументами.

HTTP Hooks

> Добавлено в v2.1.63.

Удалённые webhook-эндпоинты, принимающие тот же JSON, что и command hooks. HTTP hooks отправляют POST с JSON на URL и получают JSON в ответ. При включённом sandboxing HTTP hooks маршрутизируются через sandbox. Для подстановки переменных окружения в URL в целях безопасности требуется явно заданный список allowedEnvVars.

json
{
  "hooks": {
    "PostToolUse": [{
      "type": "http",
      "url": "https://my-webhook.example.com/hook",
      "matcher": "Write"
    }]
  }
}

Ключевые свойства:

  • "type": "http" - обозначает, что это HTTP-hook
  • "url" - URL эндпоинта webhook
  • Маршрутизируется через sandbox, если тот включён
  • Требует явного указания списка allowedEnvVars для подстановки любых переменных окружения в URL

Prompt Hooks

Промпты, вычисляемые LLM: содержимое hook - это промпт, который оценивает Claude. Применяются главным образом с событиями Stop и SubagentStop для интеллектуальной проверки завершения задачи.

json
{
  "type": "prompt",
  "prompt": "Evaluate if Claude completed all requested tasks.",
  "timeout": 30
}

LLM оценивает prompt и возвращает структурированное решение (подробности см. в Prompt-Based Hooks).

MCP Tool Hooks

Добавлено в v2.1.118.

Тип mcp_tool напрямую вызывает настроенный MCP-инструмент; в конфигурации указываются MCP-сервер и имя инструмента, а не shell-команда или URL. Это удобно, когда логика валидации или реакции уже реализована в одном из настроенных вами MCP-серверов.

json
{
  "matcher": "Edit",
  "hooks": [{
    "type": "mcp_tool",
    "server": "my-mcp-server",
    "tool": "validate_edit"
  }]
}

Ключевые свойства:

  • "type": "mcp_tool" - указывает, что это MCP tool hook
  • "server" - имя настроенного MCP-сервера
  • "tool" - имя вызываемого tool на этом сервере

Входные данные hook (имя tool, входные данные tool, контекст сессии) передаются в качестве аргументов MCP tool. О настройке MCP-серверов см. MCP server setup.

Agent Hooks

Верификационные hooks на основе subagent: они запускают отдельный agent для проверки условий или выполнения сложных проверок. В отличие от prompt hooks (одноходовая LLM-оценка), agent hooks могут использовать tools и выполнять многошаговые рассуждения.

Примечание: agent hooks являются экспериментальными и могут измениться.

json
{
  "type": "agent",
  "prompt": "Verify the code changes follow our architecture guidelines. Check the relevant design docs and compare.",
  "timeout": 120
}

Ключевые свойства:

  • "type": "agent" - обозначает, что это agent hook
  • "prompt" - описание задачи для субагента
  • Агент может использовать инструменты (Read, Grep, Bash и т. д.) для выполнения проверки
  • Возвращает структурированное решение, аналогично prompt hooks

События hook

Claude Code поддерживает 33 события hook:

EventWhen TriggeredMatcher InputCan BlockCommon Use
SessionStartSession begins/resumes/clear/compactstartup/resume/clear/compact/forkNoEnvironment setup
SetupInitial environment setup (one-time per session)(none)NoProvision tooling, install deps
InstructionsLoadedAfter CLAUDE.md or rules file loaded(none)NoModify/filter instructions
UserPromptSubmitUser submits prompt(none)YesValidate prompts
UserPromptExpansionUser prompt is expanded (e.g., @ mentions, slash commands resolved)(none)YesTransform or inspect expanded prompt
PreToolUseBefore tool executionTool nameYes (allow/deny/ask/defer)Validate, modify inputs
PermissionRequestPermission dialog shownTool nameYesAuto-approve/deny
PermissionDeniedUser denies a permission promptTool nameNoLogging, analytics, policy enforcement
PostToolUseAfter tool succeedsTool nameNoAdd context, feedback
PostToolUseFailureTool execution failsTool nameNoError handling, logging
PostToolBatchAfter a batch of tool uses completes(none)NoAggregate reporting, batched validation
NotificationNotification sentNotification typeNoCustom notifications
MessageDisplayWhile assistant message text is displayed(none)NoTransform or hide displayed message text (v2.1.152)
SubagentStartSubagent spawnedAgent type nameNoSubagent setup
SubagentStopSubagent finishesAgent type nameYesSubagent validation
StopClaude finishes responding(none)YesTask completion check
StopFailureAPI error ends turn(none)NoError recovery, logging
TeammateIdleAgent team teammate idle(none)YesTeammate coordination
TaskCompletedTask marked complete(none)YesPost-task actions
TaskCreatedTask created via TaskCreate(none)NoTask tracking, logging
ConfigChangeConfig file changes(none)Yes (except policy)React to config updates
CwdChangedWorking directory changes(none)NoDirectory-specific setup
DirectoryAddedNew working directory registered mid-session via /add-dir or the SDK register_repo_root control request (v2.1.219)(none)NoSet up tooling for a newly added directory
FileChangedWatched file changes(none)NoFile monitoring, rebuild
PreCompactBefore context compactionmanual/autoNoPre-compact actions
PostCompactAfter compaction completes(none)NoPost-compact actions
PreModelSwitchBefore Claude Code applies a requested model switchCanonical name of the model being switched to (from to_model)YesGate or veto model changes
PostModelSwitchAfter the session's model changes, including changes Claude Code makes itself (such as restoring the model on resume)Canonical name of the model switched to (from to_model)NoLog or react to model changes
WorktreeCreateWorktree being created(none)Yes (path return)Worktree initialization
WorktreeRemoveWorktree being removed(none)NoWorktree cleanup
ElicitationMCP server requests user input(none)YesInput validation
ElicitationResultUser responds to elicitation(none)YesResponse processing
SessionEndSession terminates(none)NoCleanup, final logging
PreModelSwitch и PostModelSwitch требуют версии v2.1.251 или новее. Оба получают from_model и to_model; matcher применяется к каноническому имени, полученному из to_model (например, claude-opus-5, .*opus.*). Для них таймаут по умолчанию у command, http и mcp_tool снижен до 30 секунд.

Для TaskCreated и TaskCompleted нужны включённые todo-инструменты (v2.1.233). Эти два события возникают в инструментах отслеживания todo/задач (TaskCreate/Get/Update/List, TodoWrite), которые больше недоступны в Opus 4.8, Sonnet 5, Fable 5, Mythos 5 и более новых моделях. На таких моделях hooks остаются валидной конфигурацией, но просто никогда не срабатывают - ни вывода, ни ошибки вы не получите. Установите CLAUDE_CODE_ENABLE_TODO_TOOLS=1, чтобы вернуть эти инструменты, а вместе с ними и события.

Длительность PostToolUse (v2.1.119): входные данные hooks PostToolUse и PostToolUseFailure теперь содержат duration_ms - подробности см. в разделе PostToolUse.

PreToolUse

Выполняется после того, как Claude сформировал параметры инструмента, но до его вызова. Используйте для валидации или изменения входных параметров инструмента.

Конфигурация:

json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/validate-bash.py"
          }
        ]
      }
    ]
  }
}

Типичные matchers: Task, Bash, Glob, Grep, Read, Edit, Write, WebFetch, WebSearch

Управление выводом:

  • permissionDecision: "allow", "deny", "ask" или "defer"
    • "allow" пропускает запрос разрешения (кроме инструментов, требующих взаимодействия с пользователем, и connector-инструментов, для которых ваша организация задала ask)
    • "deny" блокирует вызов инструмента
    • "ask" запрашивает подтверждение у пользователя
    • "defer" корректно завершает работу, чтобы инструмент можно было возобновить позже; при этом значении permissionDecisionReason, updatedInput и additionalContext игнорируются
    • Правила deny и ask всё равно применяются независимо от того, что вернул hook. Если несколько hooks PreToolUse дают противоречивые решения, приоритет следующий: deny > defer > ask > allow
  • permissionDecisionReason: обоснование решения. Показывается пользователю (а не Claude) для "allow" и "ask"; показывается Claude для "deny"; игнорируется для "defer"
  • updatedInput: изменённые входные параметры инструмента

PostToolUse

Запускается сразу после завершения работы инструмента. Используйте для проверки, логирования или передачи контекста обратно в Claude.

Конфигурация:

json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/security-scan.py"
          }
        ]
      }
    ]
  }
}

Управление выводом:

  • решение "block" передаёт Claude сообщение с обратной связью
  • additionalContext: контекст, добавляемый для Claude

Дополнительные поля ввода (v2.1.119):

FieldTypeDescription
duration_msnumberTool execution time in milliseconds. Excludes time spent in permission prompts and PreToolUse hook execution. Available on both PostToolUse and PostToolUseFailure hooks.

Восстанавливаемые блокировки (continueOnBlock, v2.1.139)

По умолчанию hook PostToolUse, возвращающий "decision": "block", прерывает текущий ход. Укажите у hook "continueOnBlock": true, чтобы вместо прерывания отказ был передан Claude в виде tool_result - тогда модель сможет прочитать это сообщение и повторить вызов или скорректировать действия.

json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/policy-check.py",
            "continueOnBlock": true
          }
        ]
      }
    ]
  }
}

Используйте это, когда reason у hook - это то, на что Claude может отреагировать (например, «этот файл доступен только для чтения; запишите в другое место»); не указывайте, если блокировка должна полностью прервать ход.

UserPromptSubmit

Запускается, когда пользователь отправляет prompt, до того как Claude приступит к его обработке.

Конфигурация:

json
{
  "hooks": {
    "UserPromptSubmit": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/validate-prompt.py"
          }
        ]
      }
    ]
  }
}

Управление выводом:

  • decision: "block" - заблокировать обработку
  • reason: пояснение причины блокировки
  • additionalContext: контекст, добавляемый в prompt

Stop и SubagentStop

Срабатывают, когда Claude завершает ответ (Stop) или когда завершает работу субагент (SubagentStop). Поддерживают оценку на основе prompt для интеллектуальной проверки завершённости задачи.

Дополнительное входное поле: hook'и Stop и SubagentStop получают в JSON-входе поле last_assistant_message с последним сообщением от Claude или субагента перед остановкой. Это удобно для оценки завершённости задачи.

Конфигурация:

json
{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "prompt",
            "prompt": "Evaluate if Claude completed all requested tasks.",
            "timeout": 30
          }
        ]
      }
    ]
  }
}

Защита от последовательных блокировок (v2.1.143): если Stop hook возвращает "decision": "block" (или устанавливает continue: false) 8 раз подряд в рамках одного хода, Claude Code прерывает цикл и завершает сессию с предупреждением. Порог можно переопределить переменной окружения CLAUDE_CODE_STOP_HOOK_BLOCK_CAP=<integer> (значение 0 полностью отключает ограничение). Это защищает от бесконечного зацикливания сессии из-за некорректно работающего Stop hook.

Новое поле возврата (v2.1.163): hook Stop или SubagentStop может вернуть hookSpecificOutput.additionalContext, чтобы передать Claude обратную связь и продолжить ход, не показывая пометку об ошибке. Раньше повлиять на модель из Stop hook было неудобно; теперь hook может аккуратно подмешивать контекст, минуя поведение с пометкой об ошибке, свойственное прежним способам передачи обратной связи (например, "decision": "block").

json
{
  "hookSpecificOutput": {
    "hookEventName": "Stop",
    "additionalContext": "Reminder: run the test suite before declaring done."
  }
}

SubagentStart

Срабатывает при запуске subagent. На вход matcher подаётся имя типа агента, что позволяет hooks нацеливаться на конкретные типы subagent.

Конфигурация:

json
{
  "hooks": {
    "SubagentStart": [
      {
        "matcher": "code-review",
        "hooks": [
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/subagent-init.sh"
          }
        ]
      }
    ]
  }
}

SessionStart

Запускается при старте или возобновлении сессии. Позволяет сохранять переменные окружения.

Matchers: startup, resume, clear, compact, fork

Обновление в v2.1.214: форкнутая сессия теперь возвращает source "fork" - ранее возвращалось "resume".

Особенность: используйте CLAUDE_ENV_FILE для сохранения переменных окружения (также доступно в hooks CwdChanged и FileChanged):

bash
#!/bin/bash
if [ -n "$CLAUDE_ENV_FILE" ]; then
  echo 'export NODE_ENV=development' >> "$CLAUDE_ENV_FILE"
fi
exit 0

Вывод на уровне сессии (v2.1.152): hook SessionStart может возвращать JSON для повторного сканирования skills и установки заголовка сессии:

json
{
  "reloadSkills": true,
  "hookSpecificOutput": {
    "sessionTitle": "Payments migration"
  }
}

Параметр верхнего уровня reloadSkills: true запускает повторное сканирование skills в текущей сессии (аналогично команде /reload-skills), благодаря чему skills, только что установленные hook'ом, сразу становятся доступными. Поле hookSpecificOutput.sessionTitle задаёт отображаемое название сессии при её запуске и возобновлении.

SessionEnd

Выполняется при завершении сессии для очистки или финального логирования. Не может предотвратить завершение.

Значения поля Reason:

  • clear - пользователь очистил сессию
  • logout - пользователь вышел из аккаунта
  • prompt_input_exit - пользователь вышел из строки ввода
  • other - иная причина

Конфигурация:

json
{
  "hooks": {
    "SessionEnd": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/session-cleanup.sh\""
          }
        ]
      }
    ]
  }
}

Событие Notification

Обновлённые matchers для событий уведомлений:

  • permission_prompt - уведомление о запросе разрешения
  • idle_prompt - уведомление о простое
  • auth_success - успешная аутентификация
  • elicitation_dialog - диалог, показанный пользователю
  • agent_needs_input - фоновому агенту требуется ввод (v2.1.198)
  • agent_completed - фоновый агент завершил работу (v2.1.198)

PreModelSwitch

Срабатывает перед тем, как Claude Code применит запрошенную смену модели - например, при выполнении /model или когда какой-либо компонент запрашивает другую модель. Требуется v2.1.251 или новее.

Matchers: каноническое имя модели, на которую выполняется переключение, берётся из to_model. Можно сопоставлять конкретную модель (claude-opus-5) или целое семейство через regex (.*opus.*).

Входные поля: помимо общих полей, hook получает from_model (модель, использовавшаяся до переключения) и to_model (запрошенная модель).

Может блокировать: да. Код выхода 2 отменяет переключение и выводит stderr как ошибку, так что сессия продолжает работу с текущей моделью. Используйте это для контроля или запрета смены модели - например, чтобы в проекте, чувствительном к расходам, нельзя было переключиться на самую дорогую модель.

Timeout: это событие уменьшает timeout по умолчанию для command, http и mcp_tool до 30 секунд.

Конфигурация:

json
{
  "hooks": {
    "PreModelSwitch": [
      {
        "matcher": ".*opus.*",
        "hooks": [
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/gate-model-switch.sh"
          }
        ]
      }
    ]
  }
}
bash
#!/bin/bash
# gate-model-switch.sh - refuse a switch to Opus on this project
input=$(cat)
to_model=$(echo "$input" | jq -r '.to_model')

if [[ "$to_model" == *opus* ]]; then
  echo "This project is budgeted for Sonnet; staying on the current model." >&2
  exit 2
fi

exit 0

PostModelSwitch

Срабатывает после смены модели в сессии. Также вызывается при изменениях, которые Claude Code инициирует сам, - например, при восстановлении ранее выбранной модели во время возобновления сессии, - а не только при переключениях, запрошенных вами. Требуется версия v2.1.251 или новее.

Matchers: те же, что и у PreModelSwitch, - каноническое имя, полученное из to_model.

Входные поля: from_model и to_model, а также общие поля.

Может блокировать: нет. Переключение уже произошло; hook может только наблюдать и реагировать.

Тайм-аут: это событие уменьшает тайм-аут по умолчанию для command, http и mcp_tool до 30 секунд.

Конфигурация:

json
{
  "hooks": {
    "PostModelSwitch": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/log-model-switch.sh"
          }
        ]
      }
    ]
  }
}
bash
#!/bin/bash
# log-model-switch.sh - append every model change to a session log
input=$(cat)
from=$(echo "$input" | jq -r '.from_model')
to=$(echo "$input" | jq -r '.to_model')

echo "$(date -Iseconds) $from -> $to" >> ~/.claude/model-switches.log
exit 0

Hooks на уровне компонента

Hooks можно привязать к конкретным компонентам (skills, agents, commands) через их frontmatter:

В SKILL.md, agent.md или command.md:

yaml
---
name: secure-operations
description: Perform operations with security checks
hooks:
  PreToolUse:
    - matcher: "Bash"
      hooks:
        - type: command
          command: "./scripts/check.sh"
          once: true  # Only run once per session
---

Поддерживаемые события для hooks компонентов: PreToolUse, PostToolUse, Stop

Это позволяет определять hooks непосредственно в том компоненте, который их использует, благодаря чему связанный код хранится вместе.

Hooks во frontmatter субагента

Если hook Stop определён во frontmatter субагента, он автоматически преобразуется в hook SubagentStop, привязанный к этому субагенту. Таким образом, stop hook срабатывает только по завершении именно этого субагента, а не при остановке основной сессии.

yaml
---
name: code-review-agent
description: Automated code review subagent
hooks:
  Stop:
    - hooks:
        - type: prompt
          prompt: "Verify the code review is thorough and complete."
  # The above Stop hook auto-converts to SubagentStop for this subagent
---

Требуется доверие к workspace (v2.1.218): hooks во frontmatter проектного subagent теперь требуют подтверждённого доверия к workspace для папки, из которой был загружен файл агента, - иначе они не будут запущены. До версии v2.1.218 эти hooks могли выполняться из папок, которым вы не выдавали доверие. Список scopes, на которые это требование не распространяется, см. в документации по subagents.

Событие PermissionRequest

Обрабатывает запросы разрешений с пользовательским форматом вывода:

json
{
  "hookSpecificOutput": {
    "hookEventName": "PermissionRequest",
    "decision": {
      "behavior": "allow|deny",
      "updatedInput": {},
      "message": "Custom message",
      "interrupt": false
    }
  }
}

Входные и выходные данные hook

JSON на входе (через stdin)

Все hook получают на вход JSON через stdin:

json
{
  "session_id": "abc123",
  "transcript_path": "/path/to/transcript.jsonl",
  "cwd": "/current/working/directory",
  "permission_mode": "default",
  "hook_event_name": "PreToolUse",
  "tool_name": "Write",
  "tool_input": {
    "file_path": "/path/to/file.js",
    "content": "..."
  },
  "tool_use_id": "toolu_01ABC123...",
  "agent_id": "agent-abc123",
  "agent_type": "main",
  "worktree": "/path/to/worktree",
  "effort": { "level": "medium" }
}

Общие поля:

FieldDescription
session_idUnique session identifier
transcript_pathPath to the conversation transcript file
cwdCurrent working directory
prompt_idUUID of the prompt being processed; correlates with the OpenTelemetry prompt.id attribute (v2.1.196)
hook_event_nameName of the event that triggered the hook
agent_idIdentifier of the agent running this hook
agent_typeType of agent ("main", subagent type name, etc.)
worktreePath to the git worktree, if the agent is running in one
effort.level(v2.1.133+) Active effort level: low, medium, high, xhigh, or max

Коды возврата

Exit CodeMeaningBehavior
0SuccessContinue, parse JSON stdout
2Blocking errorBlock operation, stderr shown as error
OtherNon-blocking errorContinue, stderr shown in verbose mode

JSON-вывод (stdout, exit code 0)

json
{
  "continue": true,
  "stopReason": "Optional message if stopping",
  "suppressOutput": false,
  "systemMessage": "Optional warning message",
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "allow",
    "permissionDecisionReason": "File is in allowed directory",
    "updatedInput": {
      "file_path": "/modified/path.js"
    }
  }
}

Область действия (v2.1.121+): hookSpecificOutput.updatedToolOutput теперь учитывается для всех инструментов, а не только для MCP. Hook PostToolUse на Bash, Edit, Read и т. п. может переписать вывод инструмента до того, как его увидит Claude - это удобно для скрытия секретов, нормализации diff-ов или фильтрации шумного вывода команд. Пример (удаление ANSI-кодов цвета из вывода Bash):

json
{
  "hookSpecificOutput": {
    "hookEventName": "PostToolUse",
    "updatedToolOutput": "<plain-text output with ANSI escapes removed>"
  }
}

retry (PermissionDenied): используйте в JSON hookSpecificOutput.retry: true, чтобы сообщить модели, что она может повторить отклонённый вызов инструмента.

Устаревшая форма decision для PreToolUse: для PreToolUse поля верхнего уровня decision и reason устарели - вместо них используйте hookSpecificOutput.permissionDecision (allow / deny / ask / defer) и permissionDecisionReason. Приоритет решений: deny > defer > ask > allow. Учтите также, что suppressOutput принимается, но не даёт никакого эффекта.

terminalSequence (v2.1.141)

Hook-и могут выводить сырые escape-последовательности OSC (operating system command), задавая поле terminalSequence в JSON-выводе. Когда hook завершает работу, host пишет эту последовательность в свой управляющий терминал - это удобно для desktop-уведомлений, обновления заголовка окна и звуковых сигналов терминала, причём без необходимости иметь собственный TTY.

FieldTypeDescription
terminalSequencestringRaw escape sequence (typically OSC 9 / OSC 0 / OSC 777). Written to the host terminal verbatim.
Пример - отправить desktop-уведомление через OSC 9 по завершении длительной задачи:
json
{
  "terminalSequence": "]9;Task complete"
}

Настройте это на hook Stop, чтобы уведомление срабатывало, когда Claude завершает ход. Поддержка escape-последовательностей зависит от терминала; Kitty/iTerm2/Windows Terminal поддерживают OSC 9.

Переменные окружения

VariableAvailabilityDescription
CLAUDE_PROJECT_DIRAll hooksAbsolute path to project root
CLAUDE_ENV_FILESessionStart, CwdChanged, FileChangedFile path for persisting env vars
CLAUDE_CODE_REMOTEAll hooks"true" if running in remote environments
${CLAUDE_PLUGIN_ROOT}Plugin hooksPath to plugin directory
${CLAUDE_PLUGIN_DATA}Plugin hooksPath to plugin data directory
CLAUDE_CODE_SESSIONEND_HOOKS_TIMEOUT_MSSessionEnd hooksConfigurable timeout in milliseconds for SessionEnd hooks (overrides default)
CLAUDE_CODE_SESSION_IDBash tool subprocesses (v2.1.132+)Session UUID; matches the session_id field in hook input JSON. Use to correlate bash logs with hook telemetry.
CLAUDE_EFFORTBash tool subprocesses (v2.1.133+)Active effort level (low/medium/high/xhigh/max); matches effort.level in hook input JSON.
CLAUDE_CODE_STOP_HOOK_BLOCK_CAPProcess-wide (v2.1.143+)Max consecutive Stop-hook blocks before the session ends with a warning (default 8). Set to 0 to disable the cap.

Hooks на основе промптов

Для событий Stop и SubagentStop можно использовать оценку с помощью LLM:

json
{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "prompt",
            "prompt": "Review if all tasks are complete. Return your decision.",
            "timeout": 30
          }
        ]
      }
    ]
  }
}

Схема ответа LLM:

json
{
  "decision": "approve",
  "reason": "All tasks completed successfully",
  "continue": false,
  "stopReason": "Task complete"
}

Примеры

Пример 1: Валидатор Bash-команд (PreToolUse)

Файл: .claude/hooks/validate-bash.py

python
#!/usr/bin/env python3
import json
import sys
import re

BLOCKED_PATTERNS = [
    (r"\brm\s+-rf\s+/", "Blocking dangerous rm -rf / command"),
    (r"\bsudo\s+rm", "Blocking sudo rm command"),
]

def main():
    input_data = json.load(sys.stdin)

    tool_name = input_data.get("tool_name", "")
    if tool_name != "Bash":
        sys.exit(0)

    command = input_data.get("tool_input", {}).get("command", "")

    for pattern, message in BLOCKED_PATTERNS:
        if re.search(pattern, command):
            print(message, file=sys.stderr)
            sys.exit(2)  # Exit 2 = blocking error

    sys.exit(0)

if __name__ == "__main__":
    main()

Текущая дата: среда, 2 сентября 2026 г.

<query>

Конфигурация: </query>

json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/validate-bash.py\""
          }
        ]
      }
    ]
  }
}

Пример 2: Сканер безопасности (PostToolUse)

Файл: .claude/hooks/security-scan.py

python
#!/usr/bin/env python3
import json
import sys
import re

SECRET_PATTERNS = [
    (r"password\s*=\s*['\"][^'\"]+['\"]", "Potential hardcoded password"),
    (r"api[_-]?key\s*=\s*['\"][^'\"]+['\"]", "Potential hardcoded API key"),
]

def main():
    input_data = json.load(sys.stdin)

    tool_name = input_data.get("tool_name", "")
    if tool_name not in ["Write", "Edit"]:
        sys.exit(0)

    tool_input = input_data.get("tool_input", {})
    content = tool_input.get("content", "") or tool_input.get("new_string", "")
    file_path = tool_input.get("file_path", "")

    warnings = []
    for pattern, message in SECRET_PATTERNS:
        if re.search(pattern, content, re.IGNORECASE):
            warnings.append(message)

    if warnings:
        output = {
            "hookSpecificOutput": {
                "hookEventName": "PostToolUse",
                "additionalContext": f"Security warnings for {file_path}: " + "; ".join(warnings)
            }
        }
        print(json.dumps(output))

    sys.exit(0)

if __name__ == "__main__":
    main()

Пример 3: Автоформатирование кода (PostToolUse)

Файл: .claude/hooks/format-code.sh

bash
#!/bin/bash

# Read JSON from stdin
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | python3 -c "import sys, json; print(json.load(sys.stdin).get('tool_name', ''))")
FILE_PATH=$(echo "$INPUT" | python3 -c "import sys, json; print(json.load(sys.stdin).get('tool_input', {}).get('file_path', ''))")

if [ "$TOOL_NAME" != "Write" ] && [ "$TOOL_NAME" != "Edit" ]; then
    exit 0
fi

# Format based on file extension
case "$FILE_PATH" in
    *.js|*.jsx|*.ts|*.tsx|*.json)
        command -v prettier &>/dev/null && prettier --write "$FILE_PATH" 2>/dev/null
        ;;
    *.py)
        command -v black &>/dev/null && black "$FILE_PATH" 2>/dev/null
        ;;
    *.go)
        command -v gofmt &>/dev/null && gofmt -w "$FILE_PATH" 2>/dev/null
        ;;
esac

exit 0

Пример 4: Валидатор промптов (UserPromptSubmit)

Файл: .claude/hooks/validate-prompt.py

python
#!/usr/bin/env python3
import json
import sys
import re

BLOCKED_PATTERNS = [
    (r"delete\s+(all\s+)?database", "Dangerous: database deletion"),
    (r"rm\s+-rf\s+/", "Dangerous: root deletion"),
]

def main():
    input_data = json.load(sys.stdin)
    prompt = input_data.get("user_prompt", "") or input_data.get("prompt", "")

    for pattern, message in BLOCKED_PATTERNS:
        if re.search(pattern, prompt, re.IGNORECASE):
            output = {
                "decision": "block",
                "reason": f"Blocked: {message}"
            }
            print(json.dumps(output))
            sys.exit(0)

    sys.exit(0)

if __name__ == "__main__":
    main()

Пример 5: интеллектуальный Stop hook (на основе prompt)

json
{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "prompt",
            "prompt": "Review if Claude completed all requested tasks. Check: 1) Were all files created/modified? 2) Were there unresolved errors? If incomplete, explain what's missing.",
            "timeout": 30
          }
        ]
      }
    ]
  }
}

Пример 6: трекер использования контекста (парные hooks)

Отслеживайте расход токенов на каждый запрос, используя связку hooks UserPromptSubmit (перед отправкой сообщения) и Stop (после получения ответа).

Файл: .claude/hooks/context-tracker.py

python
#!/usr/bin/env python3
"""
Context Usage Tracker - Tracks token consumption per request.

Uses UserPromptSubmit as "pre-message" hook and Stop as "post-response" hook
to calculate the delta in token usage for each request.

Token Counting Methods:
1. Character estimation (default): ~4 chars per token, no dependencies
2. tiktoken (optional): More accurate (~90-95%), requires: pip install tiktoken
"""
import json
import os
import sys
import tempfile

# Configuration
CONTEXT_LIMIT = 128000  # Claude's context window (adjust for your model)
USE_TIKTOKEN = False    # Set True if tiktoken is installed for better accuracy


def get_state_file(session_id: str) -> str:
    """Get temp file path for storing pre-message token count, isolated by session."""
    return os.path.join(tempfile.gettempdir(), f"claude-context-{session_id}.json")


def count_tokens(text: str) -> int:
    """
    Count tokens in text.

    Uses tiktoken with p50k_base encoding if available (~90-95% accuracy),
    otherwise falls back to character estimation (~80-90% accuracy).
    """
    if USE_TIKTOKEN:
        try:
            import tiktoken
            enc = tiktoken.get_encoding("p50k_base")
            return len(enc.encode(text))
        except ImportError:
            pass  # Fall back to estimation

    # Character-based estimation: ~4 characters per token for English
    return len(text) // 4


def read_transcript(transcript_path: str) -> str:
    """Read and concatenate all content from transcript file."""
    if not transcript_path or not os.path.exists(transcript_path):
        return ""

    content = []
    with open(transcript_path, "r") as f:
        for line in f:
            try:
                entry = json.loads(line.strip())
                # Extract text content from various message formats
                if "message" in entry:
                    msg = entry["message"]
                    if isinstance(msg.get("content"), str):
                        content.append(msg["content"])
                    elif isinstance(msg.get("content"), list):
                        for block in msg["content"]:
                            if isinstance(block, dict) and block.get("type") == "text":
                                content.append(block.get("text", ""))
            except json.JSONDecodeError:
                continue

    return "\n".join(content)


def handle_user_prompt_submit(data: dict) -> None:
    """Pre-message hook: Save current token count before request."""
    session_id = data.get("session_id", "unknown")
    transcript_path = data.get("transcript_path", "")

    transcript_content = read_transcript(transcript_path)
    current_tokens = count_tokens(transcript_content)

    # Save to temp file for later comparison
    state_file = get_state_file(session_id)
    with open(state_file, "w") as f:
        json.dump({"pre_tokens": current_tokens}, f)


def handle_stop(data: dict) -> None:
    """Post-response hook: Calculate and report token delta."""
    session_id = data.get("session_id", "unknown")
    transcript_path = data.get("transcript_path", "")

    transcript_content = read_transcript(transcript_path)
    current_tokens = count_tokens(transcript_content)

    # Load pre-message count
    state_file = get_state_file(session_id)
    pre_tokens = 0
    if os.path.exists(state_file):
        try:
            with open(state_file, "r") as f:
                state = json.load(f)
                pre_tokens = state.get("pre_tokens", 0)
        except (json.JSONDecodeError, IOError):
            pass

    # Calculate delta
    delta_tokens = current_tokens - pre_tokens
    remaining = CONTEXT_LIMIT - current_tokens
    percentage = (current_tokens / CONTEXT_LIMIT) * 100

    # Report usage
    method = "tiktoken" if USE_TIKTOKEN else "estimated"
    print(f"Context ({method}): ~{current_tokens:,} tokens ({percentage:.1f}% used, ~{remaining:,} remaining)", file=sys.stderr)
    if delta_tokens > 0:
        print(f"This request: ~{delta_tokens:,} tokens", file=sys.stderr)


def main():
    data = json.load(sys.stdin)
    event = data.get("hook_event_name", "")

    if event == "UserPromptSubmit":
        handle_user_prompt_submit(data)
    elif event == "Stop":
        handle_stop(data)

    sys.exit(0)


if __name__ == "__main__":
    main()

Текущая дата: среда, 2 сентября 2026 г.

<query>

Конфигурация: </query>

json
{
  "hooks": {
    "UserPromptSubmit": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/context-tracker.py\""
          }
        ]
      }
    ],
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/context-tracker.py\""
          }
        ]
      }
    ]
  }
}

Как это работает:

  1. UserPromptSubmit срабатывает до обработки вашего промпта - сохраняет текущее количество токенов
  2. Stop срабатывает после ответа Claude - вычисляет разницу и выводит информацию об использовании
  3. Каждая сессия изолирована благодаря session_id в имени временного файла

Методы подсчёта токенов:

MethodAccuracyDependenciesSpeed
Character estimation~80-90%None<1ms
tiktoken (p50k_base)~90-95%pip install tiktoken<10ms

Примечание: Anthropic не выпускала официальный офлайн-токенизатор. Оба метода дают лишь приближённую оценку. В транскрипт входят промпты пользователя, ответы Claude и вывод инструментов, но НЕ системные промпты и не внутренний контекст.

Пример 7: предустановка разрешений для auto-mode (одноразовый скрипт настройки)

Одноразовый скрипт настройки, который добавляет в ~/.claude/settings.json около 67 безопасных правил разрешений, соответствующих базовому набору auto-mode в Claude Code, - без каких-либо hook и без запоминания будущих решений. Запустите один раз; повторный запуск безопасен (уже добавленные правила пропускаются).

Файл: 09-advanced-features/setup-auto-mode-permissions.py

bash
# Preview what would be added
python3 09-advanced-features/setup-auto-mode-permissions.py --dry-run

# Apply
python3 09-advanced-features/setup-auto-mode-permissions.py

Что добавляется:

CategoryExamples
Built-in toolsRead(*), Edit(*), Write(*), Glob(*), Grep(*), Agent(*), WebSearch(*)
Git readBash(git status:*), Bash(git log:*), Bash(git diff:*)
Git write (local)Bash(git add:*), Bash(git commit:*), Bash(git checkout:*)
Package managersBash(npm install:*), Bash(pip install:*), Bash(cargo build:*)
Build & testBash(make:*), Bash(pytest:*), Bash(go test:*)
Common shellBash(ls:*), Bash(cat:*), Bash(find:*), Bash(cp:*), Bash(mv:*)
GitHub CLIBash(gh pr view:*), Bash(gh pr create:*), Bash(gh issue list:*)
Что намеренно исключено (никогда не добавляется этим скриптом):
  • rm -rf, sudo, force push, git reset --hard
  • DROP TABLE, kubectl delete, terraform destroy
  • npm publish, curl | bash, деплой в production

Пример 8: Логирование прогресса обучения (SessionEnd)

Записывайте, какие модули вы изучили, в конце каждой сессии Claude Code. Прогресс сохраняется в ~/.claude-howto-progress.json - вне репозитория, поэтому он переживает git pull без перезаписи.

Почему SessionEnd, а не Stop? Stop срабатывает после каждого ответа Claude. SessionEnd срабатывает один раз - при завершении сессии, что как раз и нужно для дневниковой записи в конце сессии.

Почему /dev/tty для ввода? Hook-скрипты получают JSON-payload hook через stdin, поэтому интерактивный read должен обращаться напрямую к /dev/tty, чтобы достучаться до терминала.

Файл: 06-hooks/session-end.sh

bash
#!/usr/bin/env bash
# SessionEnd hook: prompts for modules worked on, then appends a session record
# to ~/.claude-howto-progress.json for persistent learning progress tracking.

PROGRESS_FILE="$HOME/.claude-howto-progress.json"

# Guard: only run inside this repo
if [[ "$CLAUDE_PROJECT_DIR" != *"claude-howto"* ]] && [[ "$PWD" != *"claude-howto"* ]]; then
  exit 0
fi

if [ ! -f "$PROGRESS_FILE" ]; then
  echo '{"sessions":[]}' > "$PROGRESS_FILE"
fi

DATE=$(date +"%Y-%m-%d")
TIME=$(date +"%H:%M")

echo ""
echo " Which modules did you work on? (e.g. 06,07 or press Enter to skip)"
echo " 01=Slash  02=Memory  03=Skills  04=Subagents  05=MCP"
echo " 06=Hooks  07=Plugins 08=Checkpoints 09=Advanced 10=CLI"
printf " > "
read -r INPUT </dev/tty

if [ -z "$INPUT" ] || [ "$INPUT" = "skip" ]; then
  exit 0
fi

MODULES_JSON=$(echo "$INPUT" | tr ',' '\n' | tr -d ' ' | while read -r m; do
  case "$m" in
    01) echo '"01-slash-commands"' ;;
    02) echo '"02-memory"' ;;
    03) echo '"03-skills"' ;;
    04) echo '"04-subagents"' ;;
    05) echo '"05-mcp"' ;;
    06) echo '"06-hooks"' ;;
    07) echo '"07-plugins"' ;;
    08) echo '"08-checkpoints"' ;;
    09) echo '"09-advanced-features"' ;;
    10) echo '"10-cli"' ;;
    *)  echo "\"$m\"" ;;
  esac
done | paste -sd ',' -)

printf " Notes? (optional, press Enter to skip): "
read -r NOTES </dev/tty

# Pass NOTES as a separate argument so Python handles JSON escaping -
# avoids broken JSON when notes contain quotes or backslashes.
python3 - "$PROGRESS_FILE" "$DATE" "$TIME" "$MODULES_JSON" "$NOTES" <<'PYEOF'
import sys, json

path, date, time_str, modules_raw, notes = sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5]

new_session = {
    "date": date,
    "time": time_str,
    "modules": json.loads(f"[{modules_raw}]") if modules_raw else [],
    "notes": notes,
}

with open(path, 'r') as f:
    data = json.load(f)

data.setdefault('sessions', []).append(new_session)

with open(path, 'w') as f:
    json.dump(data, f, indent=2)
PYEOF

echo " Saved to $PROGRESS_FILE"

Установка - скопируйте скрипт в каталог hook'ов проекта, чтобы путь, указанный в settings.json, корректно разрешался:

bash
mkdir -p .claude/hooks
cp 06-hooks/session-end.sh .claude/hooks/
chmod +x .claude/hooks/session-end.sh

Конфигурация (в .claude/settings.json):

json
{
  "hooks": {
    "SessionEnd": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/session-end.sh\""
          }
        ]
      }
    ]
  }
}

Вывод - ~/.claude-howto-progress.json:

json
{
  "sessions": [
    {
      "date": "2026-04-18",
      "time": "14:32",
      "modules": ["06-hooks", "07-plugins"],
      "notes": "Installed first hook, tried pre-commit example"
    }
  ]
}

Продемонстрированные ключевые паттерны:

PatternWhy it matters
SessionEnd eventFires once on exit - not after every response like Stop
read -r INPUT </dev/ttyHooks own stdin (JSON payload); use /dev/tty for user input
$CLAUDE_PROJECT_DIRPortable path - never hardcode /Users/yourname/...
Guard clause at topPrevents the hook running in unrelated projects if installed globally
Store outside the repo~/ path survives git pull without overwriting your data
Дополнение: визуальный трекер прогресса

Полноценный интерфейс с чекбоксами, охватывающий все 10 модулей, доступен во встроенном трекере - откройте его в браузере:

bash
open local-progress/index.html

Прогресс хранится в браузерном localStorage (на диск в репозитории ничего не пишется). Кнопка Export сохраняет снимок в формате JSON, а Import - восстанавливает его.

Hooks плагинов

Плагины могут содержать hooks в файле hooks/hooks.json:

Файл: plugins/hooks/hooks.json

json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PLUGIN_ROOT}/scripts/validate.sh"
          }
        ]
      }
    ]
  }
}

Переменные окружения в hooks плагинов:

  • ${CLAUDE_PLUGIN_ROOT} - путь к директории плагина
  • ${CLAUDE_PLUGIN_DATA} - путь к директории данных плагина

Это позволяет плагинам подключать собственные hooks для валидации и автоматизации.

Hooks для MCP-инструментов

MCP-инструменты соответствуют шаблону mcp__<server>__<tool>:

json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "mcp__memory__.*",
        "hooks": [
          {
            "type": "command",
            "command": "echo '{\"systemMessage\": \"Memory operation logged\"}'"
          }
        ]
      }
    ]
  }
}

Вопросы безопасности

Отказ от ответственности

ИСПОЛЬЗУЙТЕ НА СВОЙ СТРАХ И РИСК: hooks выполняют произвольные shell-команды. Вся ответственность лежит на вас:

  • за команды, которые вы настраиваете;
  • за права доступа к файлам и их изменения;
  • за возможную потерю данных или повреждение системы;
  • за тестирование hooks в безопасном окружении до использования в production.

Замечания по безопасности

  • Требуется доверие к рабочему пространству: команды, выводимые hooks statusLine и fileSuggestion, теперь применяются только после подтверждения доверия к рабочему пространству.
  • Размер терминала для status-line (v2.1.153): скриптам команд строки состояния теперь передаются переменные окружения COLUMNS и LINES, что позволяет адаптировать вывод под ширину/высоту терминала (например, [ "$COLUMNS" -lt 80 ] && short_output).
  • HTTP hooks и переменные окружения: для подстановки переменных окружения в URL HTTP hooks требуют явно заданного списка allowedEnvVars. Это защищает от случайной утечки чувствительных переменных окружения на удалённые endpoints.
  • Иерархия управляемых настроек: параметр disableAllHooks теперь подчиняется иерархии управляемых настроек - это значит, что настройки уровня организации могут принудительно отключать hooks, и отдельный пользователь не сможет это переопределить.
  • Автоодобрение PowerShell (v2.1.119): команды инструмента PowerShell теперь можно автоматически одобрять в permission mode - так же, как Bash. Это уравнивает возможности для пользователей Windows, использующих Claude Code с shell-инструментами на базе PowerShell.
  • Закрыта лазейка с автоодобрением голых env-var в Bash (v2.1.145): до версии 2.1.145 Bash-команда вида FOO=bar somecommand (присваивание переменной перед командой, отсутствующей в allowlist) могла быть автоматически одобрена, если в allowlist находилось лишь само FOO=bar. В v2.1.145 эта лазейка закрыта - теперь такие команды вызывают запрос разрешения. Скрипты, полагавшиеся на неявное разрешение, начнут запрашивать подтверждение; чтобы вернуть автоодобрение, явно добавьте permission rule Bash(...), покрывающее команду целиком, а не только присваивание переменной.

Рекомендации

DoDon't
Validate and sanitize all inputsTrust input data blindly
Quote shell variables: "$VAR"Use unquoted: $VAR
Block path traversal (..)Allow arbitrary paths
Use absolute paths with $CLAUDE_PROJECT_DIRHardcode paths
Skip sensitive files (.env, .git/, keys)Process all files
Test hooks in isolation firstDeploy untested hooks
Use explicit allowedEnvVars for HTTP hooksExpose all env vars to webhooks

Отладка

Включение режима отладки

Запустите Claude с флагом debug, чтобы получить подробные логи hook:

bash
claude --debug

Подробный режим

Нажмите Ctrl+O в Claude Code, чтобы включить подробный режим и наблюдать за ходом выполнения hooks.

Тестирование hooks по отдельности

bash
# Test with sample JSON input
echo '{"tool_name": "Bash", "tool_input": {"command": "ls -la"}}' | python3 .claude/hooks/validate-bash.py

# Check exit code
echo $?

Полный пример конфигурации

json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/validate-bash.py\"",
            "timeout": 10
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/format-code.sh\"",
            "timeout": 30
          },
          {
            "type": "command",
            "command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/security-scan.py\"",
            "timeout": 10
          }
        ]
      }
    ],
    "UserPromptSubmit": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/validate-prompt.py\""
          }
        ]
      }
    ],
    "SessionStart": [
      {
        "matcher": "startup",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/session-init.sh\""
          }
        ]
      }
    ],
    "Stop": [
      {
        "hooks": [
          {
            "type": "prompt",
            "prompt": "Verify all tasks are complete before stopping.",
            "timeout": 30
          }
        ]
      }
    ]
  }
}

Детали выполнения hook

AspectBehavior
Timeout600 seconds default for command/http/mcp_tool (30 for prompt, 60 for agent); configurable per hook
ParallelizationAll matching hooks run in parallel
DeduplicationIdentical hook commands deduplicated
EnvironmentRuns in current directory with Claude Code's environment

Устранение неполадок

Hook не выполняется

  • Убедитесь, что синтаксис JSON-конфигурации корректен
  • Проверьте, что паттерн matcher совпадает с именем инструмента
  • Убедитесь, что скрипт существует и является исполняемым: chmod +x script.sh
  • Запустите claude --debug, чтобы увидеть логи выполнения hook
  • Убедитесь, что hook читает JSON из stdin (а не из аргументов командной строки)

Hook неожиданно блокирует выполнение

  • Протестируйте hook на тестовом JSON: echo '{"tool_name": "Write", ...}' | ./hook.py
  • Проверьте код выхода: 0 - разрешить, 2 - заблокировать
  • Проверьте вывод в stderr (отображается при коде выхода 2)

Ошибки парсинга JSON

  • Всегда читайте из stdin, а не из аргументов командной строки
  • Используйте полноценный парсер JSON (а не манипуляции со строками)
  • Корректно обрабатывайте отсутствующие поля

Установка

Шаг 1. Создайте директорию для hooks

bash
mkdir -p ~/.claude/hooks

Шаг 2. Скопируйте примеры hooks

bash
cp 06-hooks/*.sh ~/.claude/hooks/
chmod +x ~/.claude/hooks/*.sh

Шаг 3: Настройка в settings

Отредактируйте ~/.claude/settings.json или .claude/settings.json, добавив конфигурацию hook, показанную выше.

Связанные концепции

  • Checkpoints и Rewind - сохранение и восстановление состояния диалога
  • Slash Commands - создание собственных slash commands
  • Skills - переиспользуемые автономные возможности
  • Subagents - делегирование выполнения задач
  • Plugins - упакованные расширения
  • Продвинутые возможности - знакомство с расширенными возможностями Claude Code

Дополнительные материалы


Последнее обновление: 2 сентября 2026 г. Версия Claude Code: 2.1.257 Источники:

ЛОКАЛЬНАЯ ОТМЕТКА · БЕЗ ПРОВЕРКИ
cc-learnМОДУЛЬ 06
МОДУЛЬ 06/УРОК

Хуки

Hooks

Hooks - это автоматические скрипты, которые запускаются в ответ на определённые события во время сессий Claude Code. Они позволяют реализовать автоматизацию, валидацию, управление разрешениями и пользовательские сценарии работы.

Обзор

Hooks - это автоматические действия (shell-команды, HTTP webhooks, промпты к LLM, вызовы MCP-инструментов или обращения к subagent), которые выполняются при возникновении определённых событий в Claude Code. Они принимают на вход JSON и возвращают результаты через exit codes и JSON на выходе.

Ключевые возможности:

  • Автоматизация, управляемая событиями
  • Ввод/вывод в формате JSON
  • Поддержка hook-типов command, http, mcp_tool, prompt и agent
  • Сопоставление по шаблонам для hooks, привязанных к конкретным инструментам

Конфигурация

Hooks настраиваются в файлах settings со строго заданной структурой:

  • ~/.claude/settings.json - пользовательские настройки (для всех проектов)
  • .claude/settings.json - настройки проекта (доступны для совместной работы, попадают в commit)
  • .claude/settings.local.json - локальные настройки проекта (не попадают в commit)
  • Managed policy - настройки на уровне организации
  • hooks/hooks.json в plugin - hooks в области видимости plugin
  • Frontmatter у Skill/Agent - hooks жизненного цикла компонента

Базовая структура конфигурации

json
{
  "hooks": {
    "EventName": [
      {
        "matcher": "ToolPattern",
        "hooks": [
          {
            "type": "command",
            "command": "your-command-here",
            "timeout": 60
          }
        ]
      }
    ]
  }
}

Ключевые поля:

FieldDescriptionExample
matcherPattern to match tool names (case-sensitive)"Write", "Edit|Write", "*"
hooksArray of hook definitions[{ "type": "command", ... }]
typeHook type: "command" (bash), "prompt" (LLM), "http" (webhook), "mcp_tool" (MCP tool invocation, v2.1.118+), or "agent" (subagent)"command"
commandShell command to execute"$CLAUDE_PROJECT_DIR/.claude/hooks/format.sh"
timeoutOptional timeout in seconds. Defaults: 600 for command/http/mcp_tool, 30 for prompt, 60 for agent.30
onceIf true, run the hook only once per sessiontrue
asyncIf true, runs in the background without blockingtrue
asyncRewakeIf true, runs in the background and wakes Claude on exit code 2. Implies async.true
shellAccepts "bash" or "powershell". Defaults to "bash", or to "powershell" on Windows when Git Bash isn't installed."bash"
statusMessageCustom spinner message displayed while the hook runs"Formatting…"

Примечание: Некоторые события уменьшают timeout по умолчанию. UserPromptSubmit снижает значение по умолчанию для command, http и mcp_tool до 30 секунд, а MessageDisplay - до 10 секунд. Hook-и SessionEnd используют общий бюджет в 1,5 секунды; если в ваших настройках задан больший timeout для отдельного hook, Claude Code увеличивает общий бюджет до соответствующего значения, но не более 60 секунд.

Шаблоны matcher

PatternDescriptionExample
Exact stringMatches specific tool"Write"
Regex patternMatches multiple tools"Edit|Write"
Comma-separatedMatches any listed tool (v2.1.191+)"Write,Edit"
WildcardMatches all tools"*" or ""
MCP toolsServer and tool pattern"mcp__memory__.*"

Матчеры сопоставляются строго (v2.1.195+). Идентификатор с дефисом (например, имя MCP-инструмента, содержащее дефис) больше не срабатывает случайно как подстрока для другого инструмента. Матчеры со списком через запятую, такие как "Write,Edit", теперь срабатывают на любом инструменте из списка - в более ранних сборках они молча не срабатывали никогда.

Значения матчера InstructionsLoaded:

Matcher ValueDescription
session_startInstructions loaded at session startup
nested_traversalInstructions loaded during nested directory traversal
path_glob_matchInstructions loaded via path glob pattern matching

Сужение через условия if (пути в аргументах инструмента)

Поле matcher выбирает hook по имени инструмента ("Write", "Edit|Write", "*"). Чтобы фильтровать точнее - по аргументам инструмента, например запускать hook только когда правка затрагивает src/, или блокировать чтение секретных файлов, - добавьте условие if к конкретному обработчику hook. Это не то же самое, что matcher по имени инструмента: matcher определяет, какой инструмент, а if - какой именно вызов.

В if используется синтаксис правил разрешений (ToolName(pattern)), который проверяется одновременно по имени инструмента и его аргументам. Для Read/Edit/Write шаблон пути подчиняется семантике gitignore с теми же якорями, что и в правилах разрешений: голое имя вроде .env совпадает на любой глубине, src/** отсчитывается от текущей директории, /src/** - от корня проекта, ~/... - от домашней директории, а //... - абсолютный путь в файловой системе.

Поле if располагается на уровне обработчика hook - рядом с type и command внутри массива hooks, - а не на уровне matcher:

json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "if": "Edit(src/**)",
            "command": "./hooks/lint-src.sh"
          }
        ]
      },
      {
        "matcher": "Read",
        "hooks": [
          {
            "type": "command",
            "if": "Read(.env)",
            "command": "./hooks/block-secret-read.sh"
          }
        ]
      }
    ]
  }
}

Примеры допустимых шаблонов if: Edit(src/**) (правки в src/), Read(~/.ssh/**) (чтение любых SSH-ключей), Read(.env) (любой .env в текущем каталоге или ниже по дереву), Bash(git push *) (только подкоманды git push).

Обновление v2.1.214: односегментный шаблон dir/** в условии if у hook (например, Edit(src/**)) теперь соответствует только <cwd>/dir - а не одноимённому каталогу на любой глубине дерева. Ранее src/** также совпадал с foo/src/**. Если нужно сопоставление на любой глубине, используйте **/dir/**. Важно: это сужение действует только для условий if: у hook и для автоодобрения по allow-правилам - правила разрешений deny/ask по-прежнему сопоставляют dir/** на любой глубине.

Типы hook

Claude Code поддерживает пять типов hook:

Command Hooks

Тип hook по умолчанию. Выполняет shell-команду и обменивается данными через stdin/stdout в формате JSON и коды завершения.

json
{
  "type": "command",
  "command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/validate.py\"",
  "timeout": 60
}

Exec-форма (args)

Добавлено в v2.1.139.

Вместо shell-формы "command": "..." command hook может запускать бинарный файл напрямую через execve(), используя массив args. Парсинг shell при этом не выполняется, поэтому плейсхолдеры путей не требуется экранировать кавычками, а сама конфигурация защищена от уязвимостей типа shell injection.

json
{
  "type": "command",
  "args": ["python3", "$CLAUDE_PROJECT_DIR/.claude/hooks/validate.py", "--strict"],
  "timeout": 60
}

Эти две формы взаимоисключающие - hook с одновременно заданными command и args отклоняется при загрузке конфигурации. Используйте command, когда нужны конвейеры, перенаправления, цепочки через &amp;&amp; или раскрытие шелла; используйте args, когда вызываете один бинарник с аргументами.

HTTP Hooks

> Добавлено в v2.1.63.

Удалённые webhook-эндпоинты, принимающие тот же JSON, что и command hooks. HTTP hooks отправляют POST с JSON на URL и получают JSON в ответ. При включённом sandboxing HTTP hooks маршрутизируются через sandbox. Для подстановки переменных окружения в URL в целях безопасности требуется явно заданный список allowedEnvVars.

json
{
  "hooks": {
    "PostToolUse": [{
      "type": "http",
      "url": "https://my-webhook.example.com/hook",
      "matcher": "Write"
    }]
  }
}

Ключевые свойства:

  • "type": "http" - обозначает, что это HTTP-hook
  • "url" - URL эндпоинта webhook
  • Маршрутизируется через sandbox, если тот включён
  • Требует явного указания списка allowedEnvVars для подстановки любых переменных окружения в URL

Prompt Hooks

Промпты, вычисляемые LLM: содержимое hook - это промпт, который оценивает Claude. Применяются главным образом с событиями Stop и SubagentStop для интеллектуальной проверки завершения задачи.

json
{
  "type": "prompt",
  "prompt": "Evaluate if Claude completed all requested tasks.",
  "timeout": 30
}

LLM оценивает prompt и возвращает структурированное решение (подробности см. в Prompt-Based Hooks).

MCP Tool Hooks

Добавлено в v2.1.118.

Тип mcp_tool напрямую вызывает настроенный MCP-инструмент; в конфигурации указываются MCP-сервер и имя инструмента, а не shell-команда или URL. Это удобно, когда логика валидации или реакции уже реализована в одном из настроенных вами MCP-серверов.

json
{
  "matcher": "Edit",
  "hooks": [{
    "type": "mcp_tool",
    "server": "my-mcp-server",
    "tool": "validate_edit"
  }]
}

Ключевые свойства:

  • "type": "mcp_tool" - указывает, что это MCP tool hook
  • "server" - имя настроенного MCP-сервера
  • "tool" - имя вызываемого tool на этом сервере

Входные данные hook (имя tool, входные данные tool, контекст сессии) передаются в качестве аргументов MCP tool. О настройке MCP-серверов см. MCP server setup.

Agent Hooks

Верификационные hooks на основе subagent: они запускают отдельный agent для проверки условий или выполнения сложных проверок. В отличие от prompt hooks (одноходовая LLM-оценка), agent hooks могут использовать tools и выполнять многошаговые рассуждения.

Примечание: agent hooks являются экспериментальными и могут измениться.

json
{
  "type": "agent",
  "prompt": "Verify the code changes follow our architecture guidelines. Check the relevant design docs and compare.",
  "timeout": 120
}

Ключевые свойства:

  • "type": "agent" - обозначает, что это agent hook
  • "prompt" - описание задачи для субагента
  • Агент может использовать инструменты (Read, Grep, Bash и т. д.) для выполнения проверки
  • Возвращает структурированное решение, аналогично prompt hooks

События hook

Claude Code поддерживает 33 события hook:

EventWhen TriggeredMatcher InputCan BlockCommon Use
SessionStartSession begins/resumes/clear/compactstartup/resume/clear/compact/forkNoEnvironment setup
SetupInitial environment setup (one-time per session)(none)NoProvision tooling, install deps
InstructionsLoadedAfter CLAUDE.md or rules file loaded(none)NoModify/filter instructions
UserPromptSubmitUser submits prompt(none)YesValidate prompts
UserPromptExpansionUser prompt is expanded (e.g., @ mentions, slash commands resolved)(none)YesTransform or inspect expanded prompt
PreToolUseBefore tool executionTool nameYes (allow/deny/ask/defer)Validate, modify inputs
PermissionRequestPermission dialog shownTool nameYesAuto-approve/deny
PermissionDeniedUser denies a permission promptTool nameNoLogging, analytics, policy enforcement
PostToolUseAfter tool succeedsTool nameNoAdd context, feedback
PostToolUseFailureTool execution failsTool nameNoError handling, logging
PostToolBatchAfter a batch of tool uses completes(none)NoAggregate reporting, batched validation
NotificationNotification sentNotification typeNoCustom notifications
MessageDisplayWhile assistant message text is displayed(none)NoTransform or hide displayed message text (v2.1.152)
SubagentStartSubagent spawnedAgent type nameNoSubagent setup
SubagentStopSubagent finishesAgent type nameYesSubagent validation
StopClaude finishes responding(none)YesTask completion check
StopFailureAPI error ends turn(none)NoError recovery, logging
TeammateIdleAgent team teammate idle(none)YesTeammate coordination
TaskCompletedTask marked complete(none)YesPost-task actions
TaskCreatedTask created via TaskCreate(none)NoTask tracking, logging
ConfigChangeConfig file changes(none)Yes (except policy)React to config updates
CwdChangedWorking directory changes(none)NoDirectory-specific setup
DirectoryAddedNew working directory registered mid-session via /add-dir or the SDK register_repo_root control request (v2.1.219)(none)NoSet up tooling for a newly added directory
FileChangedWatched file changes(none)NoFile monitoring, rebuild
PreCompactBefore context compactionmanual/autoNoPre-compact actions
PostCompactAfter compaction completes(none)NoPost-compact actions
PreModelSwitchBefore Claude Code applies a requested model switchCanonical name of the model being switched to (from to_model)YesGate or veto model changes
PostModelSwitchAfter the session's model changes, including changes Claude Code makes itself (such as restoring the model on resume)Canonical name of the model switched to (from to_model)NoLog or react to model changes
WorktreeCreateWorktree being created(none)Yes (path return)Worktree initialization
WorktreeRemoveWorktree being removed(none)NoWorktree cleanup
ElicitationMCP server requests user input(none)YesInput validation
ElicitationResultUser responds to elicitation(none)YesResponse processing
SessionEndSession terminates(none)NoCleanup, final logging
PreModelSwitch и PostModelSwitch требуют версии v2.1.251 или новее. Оба получают from_model и to_model; matcher применяется к каноническому имени, полученному из to_model (например, claude-opus-5, .*opus.*). Для них таймаут по умолчанию у command, http и mcp_tool снижен до 30 секунд.

Для TaskCreated и TaskCompleted нужны включённые todo-инструменты (v2.1.233). Эти два события возникают в инструментах отслеживания todo/задач (TaskCreate/Get/Update/List, TodoWrite), которые больше недоступны в Opus 4.8, Sonnet 5, Fable 5, Mythos 5 и более новых моделях. На таких моделях hooks остаются валидной конфигурацией, но просто никогда не срабатывают - ни вывода, ни ошибки вы не получите. Установите CLAUDE_CODE_ENABLE_TODO_TOOLS=1, чтобы вернуть эти инструменты, а вместе с ними и события.

Длительность PostToolUse (v2.1.119): входные данные hooks PostToolUse и PostToolUseFailure теперь содержат duration_ms - подробности см. в разделе PostToolUse.

PreToolUse

Выполняется после того, как Claude сформировал параметры инструмента, но до его вызова. Используйте для валидации или изменения входных параметров инструмента.

Конфигурация:

json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/validate-bash.py"
          }
        ]
      }
    ]
  }
}

Типичные matchers: Task, Bash, Glob, Grep, Read, Edit, Write, WebFetch, WebSearch

Управление выводом:

  • permissionDecision: "allow", "deny", "ask" или "defer"
    • "allow" пропускает запрос разрешения (кроме инструментов, требующих взаимодействия с пользователем, и connector-инструментов, для которых ваша организация задала ask)
    • "deny" блокирует вызов инструмента
    • "ask" запрашивает подтверждение у пользователя
    • "defer" корректно завершает работу, чтобы инструмент можно было возобновить позже; при этом значении permissionDecisionReason, updatedInput и additionalContext игнорируются
    • Правила deny и ask всё равно применяются независимо от того, что вернул hook. Если несколько hooks PreToolUse дают противоречивые решения, приоритет следующий: deny > defer > ask > allow
  • permissionDecisionReason: обоснование решения. Показывается пользователю (а не Claude) для "allow" и "ask"; показывается Claude для "deny"; игнорируется для "defer"
  • updatedInput: изменённые входные параметры инструмента

PostToolUse

Запускается сразу после завершения работы инструмента. Используйте для проверки, логирования или передачи контекста обратно в Claude.

Конфигурация:

json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/security-scan.py"
          }
        ]
      }
    ]
  }
}

Управление выводом:

  • решение "block" передаёт Claude сообщение с обратной связью
  • additionalContext: контекст, добавляемый для Claude

Дополнительные поля ввода (v2.1.119):

FieldTypeDescription
duration_msnumberTool execution time in milliseconds. Excludes time spent in permission prompts and PreToolUse hook execution. Available on both PostToolUse and PostToolUseFailure hooks.

Восстанавливаемые блокировки (continueOnBlock, v2.1.139)

По умолчанию hook PostToolUse, возвращающий "decision": "block", прерывает текущий ход. Укажите у hook "continueOnBlock": true, чтобы вместо прерывания отказ был передан Claude в виде tool_result - тогда модель сможет прочитать это сообщение и повторить вызов или скорректировать действия.

json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/policy-check.py",
            "continueOnBlock": true
          }
        ]
      }
    ]
  }
}

Используйте это, когда reason у hook - это то, на что Claude может отреагировать (например, «этот файл доступен только для чтения; запишите в другое место»); не указывайте, если блокировка должна полностью прервать ход.

UserPromptSubmit

Запускается, когда пользователь отправляет prompt, до того как Claude приступит к его обработке.

Конфигурация:

json
{
  "hooks": {
    "UserPromptSubmit": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/validate-prompt.py"
          }
        ]
      }
    ]
  }
}

Управление выводом:

  • decision: "block" - заблокировать обработку
  • reason: пояснение причины блокировки
  • additionalContext: контекст, добавляемый в prompt

Stop и SubagentStop

Срабатывают, когда Claude завершает ответ (Stop) или когда завершает работу субагент (SubagentStop). Поддерживают оценку на основе prompt для интеллектуальной проверки завершённости задачи.

Дополнительное входное поле: hook'и Stop и SubagentStop получают в JSON-входе поле last_assistant_message с последним сообщением от Claude или субагента перед остановкой. Это удобно для оценки завершённости задачи.

Конфигурация:

json
{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "prompt",
            "prompt": "Evaluate if Claude completed all requested tasks.",
            "timeout": 30
          }
        ]
      }
    ]
  }
}

Защита от последовательных блокировок (v2.1.143): если Stop hook возвращает "decision": "block" (или устанавливает continue: false) 8 раз подряд в рамках одного хода, Claude Code прерывает цикл и завершает сессию с предупреждением. Порог можно переопределить переменной окружения CLAUDE_CODE_STOP_HOOK_BLOCK_CAP=<integer> (значение 0 полностью отключает ограничение). Это защищает от бесконечного зацикливания сессии из-за некорректно работающего Stop hook.

Новое поле возврата (v2.1.163): hook Stop или SubagentStop может вернуть hookSpecificOutput.additionalContext, чтобы передать Claude обратную связь и продолжить ход, не показывая пометку об ошибке. Раньше повлиять на модель из Stop hook было неудобно; теперь hook может аккуратно подмешивать контекст, минуя поведение с пометкой об ошибке, свойственное прежним способам передачи обратной связи (например, "decision": "block").

json
{
  "hookSpecificOutput": {
    "hookEventName": "Stop",
    "additionalContext": "Reminder: run the test suite before declaring done."
  }
}

SubagentStart

Срабатывает при запуске subagent. На вход matcher подаётся имя типа агента, что позволяет hooks нацеливаться на конкретные типы subagent.

Конфигурация:

json
{
  "hooks": {
    "SubagentStart": [
      {
        "matcher": "code-review",
        "hooks": [
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/subagent-init.sh"
          }
        ]
      }
    ]
  }
}

SessionStart

Запускается при старте или возобновлении сессии. Позволяет сохранять переменные окружения.

Matchers: startup, resume, clear, compact, fork

Обновление в v2.1.214: форкнутая сессия теперь возвращает source "fork" - ранее возвращалось "resume".

Особенность: используйте CLAUDE_ENV_FILE для сохранения переменных окружения (также доступно в hooks CwdChanged и FileChanged):

bash
#!/bin/bash
if [ -n "$CLAUDE_ENV_FILE" ]; then
  echo 'export NODE_ENV=development' >> "$CLAUDE_ENV_FILE"
fi
exit 0

Вывод на уровне сессии (v2.1.152): hook SessionStart может возвращать JSON для повторного сканирования skills и установки заголовка сессии:

json
{
  "reloadSkills": true,
  "hookSpecificOutput": {
    "sessionTitle": "Payments migration"
  }
}

Параметр верхнего уровня reloadSkills: true запускает повторное сканирование skills в текущей сессии (аналогично команде /reload-skills), благодаря чему skills, только что установленные hook'ом, сразу становятся доступными. Поле hookSpecificOutput.sessionTitle задаёт отображаемое название сессии при её запуске и возобновлении.

SessionEnd

Выполняется при завершении сессии для очистки или финального логирования. Не может предотвратить завершение.

Значения поля Reason:

  • clear - пользователь очистил сессию
  • logout - пользователь вышел из аккаунта
  • prompt_input_exit - пользователь вышел из строки ввода
  • other - иная причина

Конфигурация:

json
{
  "hooks": {
    "SessionEnd": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/session-cleanup.sh\""
          }
        ]
      }
    ]
  }
}

Событие Notification

Обновлённые matchers для событий уведомлений:

  • permission_prompt - уведомление о запросе разрешения
  • idle_prompt - уведомление о простое
  • auth_success - успешная аутентификация
  • elicitation_dialog - диалог, показанный пользователю
  • agent_needs_input - фоновому агенту требуется ввод (v2.1.198)
  • agent_completed - фоновый агент завершил работу (v2.1.198)

PreModelSwitch

Срабатывает перед тем, как Claude Code применит запрошенную смену модели - например, при выполнении /model или когда какой-либо компонент запрашивает другую модель. Требуется v2.1.251 или новее.

Matchers: каноническое имя модели, на которую выполняется переключение, берётся из to_model. Можно сопоставлять конкретную модель (claude-opus-5) или целое семейство через regex (.*opus.*).

Входные поля: помимо общих полей, hook получает from_model (модель, использовавшаяся до переключения) и to_model (запрошенная модель).

Может блокировать: да. Код выхода 2 отменяет переключение и выводит stderr как ошибку, так что сессия продолжает работу с текущей моделью. Используйте это для контроля или запрета смены модели - например, чтобы в проекте, чувствительном к расходам, нельзя было переключиться на самую дорогую модель.

Timeout: это событие уменьшает timeout по умолчанию для command, http и mcp_tool до 30 секунд.

Конфигурация:

json
{
  "hooks": {
    "PreModelSwitch": [
      {
        "matcher": ".*opus.*",
        "hooks": [
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/gate-model-switch.sh"
          }
        ]
      }
    ]
  }
}
bash
#!/bin/bash
# gate-model-switch.sh - refuse a switch to Opus on this project
input=$(cat)
to_model=$(echo "$input" | jq -r '.to_model')

if [[ "$to_model" == *opus* ]]; then
  echo "This project is budgeted for Sonnet; staying on the current model." >&2
  exit 2
fi

exit 0

PostModelSwitch

Срабатывает после смены модели в сессии. Также вызывается при изменениях, которые Claude Code инициирует сам, - например, при восстановлении ранее выбранной модели во время возобновления сессии, - а не только при переключениях, запрошенных вами. Требуется версия v2.1.251 или новее.

Matchers: те же, что и у PreModelSwitch, - каноническое имя, полученное из to_model.

Входные поля: from_model и to_model, а также общие поля.

Может блокировать: нет. Переключение уже произошло; hook может только наблюдать и реагировать.

Тайм-аут: это событие уменьшает тайм-аут по умолчанию для command, http и mcp_tool до 30 секунд.

Конфигурация:

json
{
  "hooks": {
    "PostModelSwitch": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/log-model-switch.sh"
          }
        ]
      }
    ]
  }
}
bash
#!/bin/bash
# log-model-switch.sh - append every model change to a session log
input=$(cat)
from=$(echo "$input" | jq -r '.from_model')
to=$(echo "$input" | jq -r '.to_model')

echo "$(date -Iseconds) $from -> $to" >> ~/.claude/model-switches.log
exit 0

Hooks на уровне компонента

Hooks можно привязать к конкретным компонентам (skills, agents, commands) через их frontmatter:

В SKILL.md, agent.md или command.md:

yaml
---
name: secure-operations
description: Perform operations with security checks
hooks:
  PreToolUse:
    - matcher: "Bash"
      hooks:
        - type: command
          command: "./scripts/check.sh"
          once: true  # Only run once per session
---

Поддерживаемые события для hooks компонентов: PreToolUse, PostToolUse, Stop

Это позволяет определять hooks непосредственно в том компоненте, который их использует, благодаря чему связанный код хранится вместе.

Hooks во frontmatter субагента

Если hook Stop определён во frontmatter субагента, он автоматически преобразуется в hook SubagentStop, привязанный к этому субагенту. Таким образом, stop hook срабатывает только по завершении именно этого субагента, а не при остановке основной сессии.

yaml
---
name: code-review-agent
description: Automated code review subagent
hooks:
  Stop:
    - hooks:
        - type: prompt
          prompt: "Verify the code review is thorough and complete."
  # The above Stop hook auto-converts to SubagentStop for this subagent
---

Требуется доверие к workspace (v2.1.218): hooks во frontmatter проектного subagent теперь требуют подтверждённого доверия к workspace для папки, из которой был загружен файл агента, - иначе они не будут запущены. До версии v2.1.218 эти hooks могли выполняться из папок, которым вы не выдавали доверие. Список scopes, на которые это требование не распространяется, см. в документации по subagents.

Событие PermissionRequest

Обрабатывает запросы разрешений с пользовательским форматом вывода:

json
{
  "hookSpecificOutput": {
    "hookEventName": "PermissionRequest",
    "decision": {
      "behavior": "allow|deny",
      "updatedInput": {},
      "message": "Custom message",
      "interrupt": false
    }
  }
}

Входные и выходные данные hook

JSON на входе (через stdin)

Все hook получают на вход JSON через stdin:

json
{
  "session_id": "abc123",
  "transcript_path": "/path/to/transcript.jsonl",
  "cwd": "/current/working/directory",
  "permission_mode": "default",
  "hook_event_name": "PreToolUse",
  "tool_name": "Write",
  "tool_input": {
    "file_path": "/path/to/file.js",
    "content": "..."
  },
  "tool_use_id": "toolu_01ABC123...",
  "agent_id": "agent-abc123",
  "agent_type": "main",
  "worktree": "/path/to/worktree",
  "effort": { "level": "medium" }
}

Общие поля:

FieldDescription
session_idUnique session identifier
transcript_pathPath to the conversation transcript file
cwdCurrent working directory
prompt_idUUID of the prompt being processed; correlates with the OpenTelemetry prompt.id attribute (v2.1.196)
hook_event_nameName of the event that triggered the hook
agent_idIdentifier of the agent running this hook
agent_typeType of agent ("main", subagent type name, etc.)
worktreePath to the git worktree, if the agent is running in one
effort.level(v2.1.133+) Active effort level: low, medium, high, xhigh, or max

Коды возврата

Exit CodeMeaningBehavior
0SuccessContinue, parse JSON stdout
2Blocking errorBlock operation, stderr shown as error
OtherNon-blocking errorContinue, stderr shown in verbose mode

JSON-вывод (stdout, exit code 0)

json
{
  "continue": true,
  "stopReason": "Optional message if stopping",
  "suppressOutput": false,
  "systemMessage": "Optional warning message",
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "allow",
    "permissionDecisionReason": "File is in allowed directory",
    "updatedInput": {
      "file_path": "/modified/path.js"
    }
  }
}

Область действия (v2.1.121+): hookSpecificOutput.updatedToolOutput теперь учитывается для всех инструментов, а не только для MCP. Hook PostToolUse на Bash, Edit, Read и т. п. может переписать вывод инструмента до того, как его увидит Claude - это удобно для скрытия секретов, нормализации diff-ов или фильтрации шумного вывода команд. Пример (удаление ANSI-кодов цвета из вывода Bash):

json
{
  "hookSpecificOutput": {
    "hookEventName": "PostToolUse",
    "updatedToolOutput": "<plain-text output with ANSI escapes removed>"
  }
}

retry (PermissionDenied): используйте в JSON hookSpecificOutput.retry: true, чтобы сообщить модели, что она может повторить отклонённый вызов инструмента.

Устаревшая форма decision для PreToolUse: для PreToolUse поля верхнего уровня decision и reason устарели - вместо них используйте hookSpecificOutput.permissionDecision (allow / deny / ask / defer) и permissionDecisionReason. Приоритет решений: deny > defer > ask > allow. Учтите также, что suppressOutput принимается, но не даёт никакого эффекта.

terminalSequence (v2.1.141)

Hook-и могут выводить сырые escape-последовательности OSC (operating system command), задавая поле terminalSequence в JSON-выводе. Когда hook завершает работу, host пишет эту последовательность в свой управляющий терминал - это удобно для desktop-уведомлений, обновления заголовка окна и звуковых сигналов терминала, причём без необходимости иметь собственный TTY.

FieldTypeDescription
terminalSequencestringRaw escape sequence (typically OSC 9 / OSC 0 / OSC 777). Written to the host terminal verbatim.
Пример - отправить desktop-уведомление через OSC 9 по завершении длительной задачи:
json
{
  "terminalSequence": "]9;Task complete"
}

Настройте это на hook Stop, чтобы уведомление срабатывало, когда Claude завершает ход. Поддержка escape-последовательностей зависит от терминала; Kitty/iTerm2/Windows Terminal поддерживают OSC 9.

Переменные окружения

VariableAvailabilityDescription
CLAUDE_PROJECT_DIRAll hooksAbsolute path to project root
CLAUDE_ENV_FILESessionStart, CwdChanged, FileChangedFile path for persisting env vars
CLAUDE_CODE_REMOTEAll hooks"true" if running in remote environments
${CLAUDE_PLUGIN_ROOT}Plugin hooksPath to plugin directory
${CLAUDE_PLUGIN_DATA}Plugin hooksPath to plugin data directory
CLAUDE_CODE_SESSIONEND_HOOKS_TIMEOUT_MSSessionEnd hooksConfigurable timeout in milliseconds for SessionEnd hooks (overrides default)
CLAUDE_CODE_SESSION_IDBash tool subprocesses (v2.1.132+)Session UUID; matches the session_id field in hook input JSON. Use to correlate bash logs with hook telemetry.
CLAUDE_EFFORTBash tool subprocesses (v2.1.133+)Active effort level (low/medium/high/xhigh/max); matches effort.level in hook input JSON.
CLAUDE_CODE_STOP_HOOK_BLOCK_CAPProcess-wide (v2.1.143+)Max consecutive Stop-hook blocks before the session ends with a warning (default 8). Set to 0 to disable the cap.

Hooks на основе промптов

Для событий Stop и SubagentStop можно использовать оценку с помощью LLM:

json
{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "prompt",
            "prompt": "Review if all tasks are complete. Return your decision.",
            "timeout": 30
          }
        ]
      }
    ]
  }
}

Схема ответа LLM:

json
{
  "decision": "approve",
  "reason": "All tasks completed successfully",
  "continue": false,
  "stopReason": "Task complete"
}

Примеры

Пример 1: Валидатор Bash-команд (PreToolUse)

Файл: .claude/hooks/validate-bash.py

python
#!/usr/bin/env python3
import json
import sys
import re

BLOCKED_PATTERNS = [
    (r"\brm\s+-rf\s+/", "Blocking dangerous rm -rf / command"),
    (r"\bsudo\s+rm", "Blocking sudo rm command"),
]

def main():
    input_data = json.load(sys.stdin)

    tool_name = input_data.get("tool_name", "")
    if tool_name != "Bash":
        sys.exit(0)

    command = input_data.get("tool_input", {}).get("command", "")

    for pattern, message in BLOCKED_PATTERNS:
        if re.search(pattern, command):
            print(message, file=sys.stderr)
            sys.exit(2)  # Exit 2 = blocking error

    sys.exit(0)

if __name__ == "__main__":
    main()

Текущая дата: среда, 2 сентября 2026 г.

<query>

Конфигурация: </query>

json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/validate-bash.py\""
          }
        ]
      }
    ]
  }
}

Пример 2: Сканер безопасности (PostToolUse)

Файл: .claude/hooks/security-scan.py

python
#!/usr/bin/env python3
import json
import sys
import re

SECRET_PATTERNS = [
    (r"password\s*=\s*['\"][^'\"]+['\"]", "Potential hardcoded password"),
    (r"api[_-]?key\s*=\s*['\"][^'\"]+['\"]", "Potential hardcoded API key"),
]

def main():
    input_data = json.load(sys.stdin)

    tool_name = input_data.get("tool_name", "")
    if tool_name not in ["Write", "Edit"]:
        sys.exit(0)

    tool_input = input_data.get("tool_input", {})
    content = tool_input.get("content", "") or tool_input.get("new_string", "")
    file_path = tool_input.get("file_path", "")

    warnings = []
    for pattern, message in SECRET_PATTERNS:
        if re.search(pattern, content, re.IGNORECASE):
            warnings.append(message)

    if warnings:
        output = {
            "hookSpecificOutput": {
                "hookEventName": "PostToolUse",
                "additionalContext": f"Security warnings for {file_path}: " + "; ".join(warnings)
            }
        }
        print(json.dumps(output))

    sys.exit(0)

if __name__ == "__main__":
    main()

Пример 3: Автоформатирование кода (PostToolUse)

Файл: .claude/hooks/format-code.sh

bash
#!/bin/bash

# Read JSON from stdin
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | python3 -c "import sys, json; print(json.load(sys.stdin).get('tool_name', ''))")
FILE_PATH=$(echo "$INPUT" | python3 -c "import sys, json; print(json.load(sys.stdin).get('tool_input', {}).get('file_path', ''))")

if [ "$TOOL_NAME" != "Write" ] && [ "$TOOL_NAME" != "Edit" ]; then
    exit 0
fi

# Format based on file extension
case "$FILE_PATH" in
    *.js|*.jsx|*.ts|*.tsx|*.json)
        command -v prettier &>/dev/null && prettier --write "$FILE_PATH" 2>/dev/null
        ;;
    *.py)
        command -v black &>/dev/null && black "$FILE_PATH" 2>/dev/null
        ;;
    *.go)
        command -v gofmt &>/dev/null && gofmt -w "$FILE_PATH" 2>/dev/null
        ;;
esac

exit 0

Пример 4: Валидатор промптов (UserPromptSubmit)

Файл: .claude/hooks/validate-prompt.py

python
#!/usr/bin/env python3
import json
import sys
import re

BLOCKED_PATTERNS = [
    (r"delete\s+(all\s+)?database", "Dangerous: database deletion"),
    (r"rm\s+-rf\s+/", "Dangerous: root deletion"),
]

def main():
    input_data = json.load(sys.stdin)
    prompt = input_data.get("user_prompt", "") or input_data.get("prompt", "")

    for pattern, message in BLOCKED_PATTERNS:
        if re.search(pattern, prompt, re.IGNORECASE):
            output = {
                "decision": "block",
                "reason": f"Blocked: {message}"
            }
            print(json.dumps(output))
            sys.exit(0)

    sys.exit(0)

if __name__ == "__main__":
    main()

Пример 5: интеллектуальный Stop hook (на основе prompt)

json
{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "prompt",
            "prompt": "Review if Claude completed all requested tasks. Check: 1) Were all files created/modified? 2) Were there unresolved errors? If incomplete, explain what's missing.",
            "timeout": 30
          }
        ]
      }
    ]
  }
}

Пример 6: трекер использования контекста (парные hooks)

Отслеживайте расход токенов на каждый запрос, используя связку hooks UserPromptSubmit (перед отправкой сообщения) и Stop (после получения ответа).

Файл: .claude/hooks/context-tracker.py

python
#!/usr/bin/env python3
"""
Context Usage Tracker - Tracks token consumption per request.

Uses UserPromptSubmit as "pre-message" hook and Stop as "post-response" hook
to calculate the delta in token usage for each request.

Token Counting Methods:
1. Character estimation (default): ~4 chars per token, no dependencies
2. tiktoken (optional): More accurate (~90-95%), requires: pip install tiktoken
"""
import json
import os
import sys
import tempfile

# Configuration
CONTEXT_LIMIT = 128000  # Claude's context window (adjust for your model)
USE_TIKTOKEN = False    # Set True if tiktoken is installed for better accuracy


def get_state_file(session_id: str) -> str:
    """Get temp file path for storing pre-message token count, isolated by session."""
    return os.path.join(tempfile.gettempdir(), f"claude-context-{session_id}.json")


def count_tokens(text: str) -> int:
    """
    Count tokens in text.

    Uses tiktoken with p50k_base encoding if available (~90-95% accuracy),
    otherwise falls back to character estimation (~80-90% accuracy).
    """
    if USE_TIKTOKEN:
        try:
            import tiktoken
            enc = tiktoken.get_encoding("p50k_base")
            return len(enc.encode(text))
        except ImportError:
            pass  # Fall back to estimation

    # Character-based estimation: ~4 characters per token for English
    return len(text) // 4


def read_transcript(transcript_path: str) -> str:
    """Read and concatenate all content from transcript file."""
    if not transcript_path or not os.path.exists(transcript_path):
        return ""

    content = []
    with open(transcript_path, "r") as f:
        for line in f:
            try:
                entry = json.loads(line.strip())
                # Extract text content from various message formats
                if "message" in entry:
                    msg = entry["message"]
                    if isinstance(msg.get("content"), str):
                        content.append(msg["content"])
                    elif isinstance(msg.get("content"), list):
                        for block in msg["content"]:
                            if isinstance(block, dict) and block.get("type") == "text":
                                content.append(block.get("text", ""))
            except json.JSONDecodeError:
                continue

    return "\n".join(content)


def handle_user_prompt_submit(data: dict) -> None:
    """Pre-message hook: Save current token count before request."""
    session_id = data.get("session_id", "unknown")
    transcript_path = data.get("transcript_path", "")

    transcript_content = read_transcript(transcript_path)
    current_tokens = count_tokens(transcript_content)

    # Save to temp file for later comparison
    state_file = get_state_file(session_id)
    with open(state_file, "w") as f:
        json.dump({"pre_tokens": current_tokens}, f)


def handle_stop(data: dict) -> None:
    """Post-response hook: Calculate and report token delta."""
    session_id = data.get("session_id", "unknown")
    transcript_path = data.get("transcript_path", "")

    transcript_content = read_transcript(transcript_path)
    current_tokens = count_tokens(transcript_content)

    # Load pre-message count
    state_file = get_state_file(session_id)
    pre_tokens = 0
    if os.path.exists(state_file):
        try:
            with open(state_file, "r") as f:
                state = json.load(f)
                pre_tokens = state.get("pre_tokens", 0)
        except (json.JSONDecodeError, IOError):
            pass

    # Calculate delta
    delta_tokens = current_tokens - pre_tokens
    remaining = CONTEXT_LIMIT - current_tokens
    percentage = (current_tokens / CONTEXT_LIMIT) * 100

    # Report usage
    method = "tiktoken" if USE_TIKTOKEN else "estimated"
    print(f"Context ({method}): ~{current_tokens:,} tokens ({percentage:.1f}% used, ~{remaining:,} remaining)", file=sys.stderr)
    if delta_tokens > 0:
        print(f"This request: ~{delta_tokens:,} tokens", file=sys.stderr)


def main():
    data = json.load(sys.stdin)
    event = data.get("hook_event_name", "")

    if event == "UserPromptSubmit":
        handle_user_prompt_submit(data)
    elif event == "Stop":
        handle_stop(data)

    sys.exit(0)


if __name__ == "__main__":
    main()

Текущая дата: среда, 2 сентября 2026 г.

<query>

Конфигурация: </query>

json
{
  "hooks": {
    "UserPromptSubmit": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/context-tracker.py\""
          }
        ]
      }
    ],
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/context-tracker.py\""
          }
        ]
      }
    ]
  }
}

Как это работает:

  1. UserPromptSubmit срабатывает до обработки вашего промпта - сохраняет текущее количество токенов
  2. Stop срабатывает после ответа Claude - вычисляет разницу и выводит информацию об использовании
  3. Каждая сессия изолирована благодаря session_id в имени временного файла

Методы подсчёта токенов:

MethodAccuracyDependenciesSpeed
Character estimation~80-90%None<1ms
tiktoken (p50k_base)~90-95%pip install tiktoken<10ms

Примечание: Anthropic не выпускала официальный офлайн-токенизатор. Оба метода дают лишь приближённую оценку. В транскрипт входят промпты пользователя, ответы Claude и вывод инструментов, но НЕ системные промпты и не внутренний контекст.

Пример 7: предустановка разрешений для auto-mode (одноразовый скрипт настройки)

Одноразовый скрипт настройки, который добавляет в ~/.claude/settings.json около 67 безопасных правил разрешений, соответствующих базовому набору auto-mode в Claude Code, - без каких-либо hook и без запоминания будущих решений. Запустите один раз; повторный запуск безопасен (уже добавленные правила пропускаются).

Файл: 09-advanced-features/setup-auto-mode-permissions.py

bash
# Preview what would be added
python3 09-advanced-features/setup-auto-mode-permissions.py --dry-run

# Apply
python3 09-advanced-features/setup-auto-mode-permissions.py

Что добавляется:

CategoryExamples
Built-in toolsRead(*), Edit(*), Write(*), Glob(*), Grep(*), Agent(*), WebSearch(*)
Git readBash(git status:*), Bash(git log:*), Bash(git diff:*)
Git write (local)Bash(git add:*), Bash(git commit:*), Bash(git checkout:*)
Package managersBash(npm install:*), Bash(pip install:*), Bash(cargo build:*)
Build & testBash(make:*), Bash(pytest:*), Bash(go test:*)
Common shellBash(ls:*), Bash(cat:*), Bash(find:*), Bash(cp:*), Bash(mv:*)
GitHub CLIBash(gh pr view:*), Bash(gh pr create:*), Bash(gh issue list:*)
Что намеренно исключено (никогда не добавляется этим скриптом):
  • rm -rf, sudo, force push, git reset --hard
  • DROP TABLE, kubectl delete, terraform destroy
  • npm publish, curl | bash, деплой в production

Пример 8: Логирование прогресса обучения (SessionEnd)

Записывайте, какие модули вы изучили, в конце каждой сессии Claude Code. Прогресс сохраняется в ~/.claude-howto-progress.json - вне репозитория, поэтому он переживает git pull без перезаписи.

Почему SessionEnd, а не Stop? Stop срабатывает после каждого ответа Claude. SessionEnd срабатывает один раз - при завершении сессии, что как раз и нужно для дневниковой записи в конце сессии.

Почему /dev/tty для ввода? Hook-скрипты получают JSON-payload hook через stdin, поэтому интерактивный read должен обращаться напрямую к /dev/tty, чтобы достучаться до терминала.

Файл: 06-hooks/session-end.sh

bash
#!/usr/bin/env bash
# SessionEnd hook: prompts for modules worked on, then appends a session record
# to ~/.claude-howto-progress.json for persistent learning progress tracking.

PROGRESS_FILE="$HOME/.claude-howto-progress.json"

# Guard: only run inside this repo
if [[ "$CLAUDE_PROJECT_DIR" != *"claude-howto"* ]] && [[ "$PWD" != *"claude-howto"* ]]; then
  exit 0
fi

if [ ! -f "$PROGRESS_FILE" ]; then
  echo '{"sessions":[]}' > "$PROGRESS_FILE"
fi

DATE=$(date +"%Y-%m-%d")
TIME=$(date +"%H:%M")

echo ""
echo " Which modules did you work on? (e.g. 06,07 or press Enter to skip)"
echo " 01=Slash  02=Memory  03=Skills  04=Subagents  05=MCP"
echo " 06=Hooks  07=Plugins 08=Checkpoints 09=Advanced 10=CLI"
printf " > "
read -r INPUT </dev/tty

if [ -z "$INPUT" ] || [ "$INPUT" = "skip" ]; then
  exit 0
fi

MODULES_JSON=$(echo "$INPUT" | tr ',' '\n' | tr -d ' ' | while read -r m; do
  case "$m" in
    01) echo '"01-slash-commands"' ;;
    02) echo '"02-memory"' ;;
    03) echo '"03-skills"' ;;
    04) echo '"04-subagents"' ;;
    05) echo '"05-mcp"' ;;
    06) echo '"06-hooks"' ;;
    07) echo '"07-plugins"' ;;
    08) echo '"08-checkpoints"' ;;
    09) echo '"09-advanced-features"' ;;
    10) echo '"10-cli"' ;;
    *)  echo "\"$m\"" ;;
  esac
done | paste -sd ',' -)

printf " Notes? (optional, press Enter to skip): "
read -r NOTES </dev/tty

# Pass NOTES as a separate argument so Python handles JSON escaping -
# avoids broken JSON when notes contain quotes or backslashes.
python3 - "$PROGRESS_FILE" "$DATE" "$TIME" "$MODULES_JSON" "$NOTES" <<'PYEOF'
import sys, json

path, date, time_str, modules_raw, notes = sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5]

new_session = {
    "date": date,
    "time": time_str,
    "modules": json.loads(f"[{modules_raw}]") if modules_raw else [],
    "notes": notes,
}

with open(path, 'r') as f:
    data = json.load(f)

data.setdefault('sessions', []).append(new_session)

with open(path, 'w') as f:
    json.dump(data, f, indent=2)
PYEOF

echo " Saved to $PROGRESS_FILE"

Установка - скопируйте скрипт в каталог hook'ов проекта, чтобы путь, указанный в settings.json, корректно разрешался:

bash
mkdir -p .claude/hooks
cp 06-hooks/session-end.sh .claude/hooks/
chmod +x .claude/hooks/session-end.sh

Конфигурация (в .claude/settings.json):

json
{
  "hooks": {
    "SessionEnd": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/session-end.sh\""
          }
        ]
      }
    ]
  }
}

Вывод - ~/.claude-howto-progress.json:

json
{
  "sessions": [
    {
      "date": "2026-04-18",
      "time": "14:32",
      "modules": ["06-hooks", "07-plugins"],
      "notes": "Installed first hook, tried pre-commit example"
    }
  ]
}

Продемонстрированные ключевые паттерны:

PatternWhy it matters
SessionEnd eventFires once on exit - not after every response like Stop
read -r INPUT </dev/ttyHooks own stdin (JSON payload); use /dev/tty for user input
$CLAUDE_PROJECT_DIRPortable path - never hardcode /Users/yourname/...
Guard clause at topPrevents the hook running in unrelated projects if installed globally
Store outside the repo~/ path survives git pull without overwriting your data
Дополнение: визуальный трекер прогресса

Полноценный интерфейс с чекбоксами, охватывающий все 10 модулей, доступен во встроенном трекере - откройте его в браузере:

bash
open local-progress/index.html

Прогресс хранится в браузерном localStorage (на диск в репозитории ничего не пишется). Кнопка Export сохраняет снимок в формате JSON, а Import - восстанавливает его.

Hooks плагинов

Плагины могут содержать hooks в файле hooks/hooks.json:

Файл: plugins/hooks/hooks.json

json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PLUGIN_ROOT}/scripts/validate.sh"
          }
        ]
      }
    ]
  }
}

Переменные окружения в hooks плагинов:

  • ${CLAUDE_PLUGIN_ROOT} - путь к директории плагина
  • ${CLAUDE_PLUGIN_DATA} - путь к директории данных плагина

Это позволяет плагинам подключать собственные hooks для валидации и автоматизации.

Hooks для MCP-инструментов

MCP-инструменты соответствуют шаблону mcp__<server>__<tool>:

json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "mcp__memory__.*",
        "hooks": [
          {
            "type": "command",
            "command": "echo '{\"systemMessage\": \"Memory operation logged\"}'"
          }
        ]
      }
    ]
  }
}

Вопросы безопасности

Отказ от ответственности

ИСПОЛЬЗУЙТЕ НА СВОЙ СТРАХ И РИСК: hooks выполняют произвольные shell-команды. Вся ответственность лежит на вас:

  • за команды, которые вы настраиваете;
  • за права доступа к файлам и их изменения;
  • за возможную потерю данных или повреждение системы;
  • за тестирование hooks в безопасном окружении до использования в production.

Замечания по безопасности

  • Требуется доверие к рабочему пространству: команды, выводимые hooks statusLine и fileSuggestion, теперь применяются только после подтверждения доверия к рабочему пространству.
  • Размер терминала для status-line (v2.1.153): скриптам команд строки состояния теперь передаются переменные окружения COLUMNS и LINES, что позволяет адаптировать вывод под ширину/высоту терминала (например, [ "$COLUMNS" -lt 80 ] && short_output).
  • HTTP hooks и переменные окружения: для подстановки переменных окружения в URL HTTP hooks требуют явно заданного списка allowedEnvVars. Это защищает от случайной утечки чувствительных переменных окружения на удалённые endpoints.
  • Иерархия управляемых настроек: параметр disableAllHooks теперь подчиняется иерархии управляемых настроек - это значит, что настройки уровня организации могут принудительно отключать hooks, и отдельный пользователь не сможет это переопределить.
  • Автоодобрение PowerShell (v2.1.119): команды инструмента PowerShell теперь можно автоматически одобрять в permission mode - так же, как Bash. Это уравнивает возможности для пользователей Windows, использующих Claude Code с shell-инструментами на базе PowerShell.
  • Закрыта лазейка с автоодобрением голых env-var в Bash (v2.1.145): до версии 2.1.145 Bash-команда вида FOO=bar somecommand (присваивание переменной перед командой, отсутствующей в allowlist) могла быть автоматически одобрена, если в allowlist находилось лишь само FOO=bar. В v2.1.145 эта лазейка закрыта - теперь такие команды вызывают запрос разрешения. Скрипты, полагавшиеся на неявное разрешение, начнут запрашивать подтверждение; чтобы вернуть автоодобрение, явно добавьте permission rule Bash(...), покрывающее команду целиком, а не только присваивание переменной.

Рекомендации

DoDon't
Validate and sanitize all inputsTrust input data blindly
Quote shell variables: "$VAR"Use unquoted: $VAR
Block path traversal (..)Allow arbitrary paths
Use absolute paths with $CLAUDE_PROJECT_DIRHardcode paths
Skip sensitive files (.env, .git/, keys)Process all files
Test hooks in isolation firstDeploy untested hooks
Use explicit allowedEnvVars for HTTP hooksExpose all env vars to webhooks

Отладка

Включение режима отладки

Запустите Claude с флагом debug, чтобы получить подробные логи hook:

bash
claude --debug

Подробный режим

Нажмите Ctrl+O в Claude Code, чтобы включить подробный режим и наблюдать за ходом выполнения hooks.

Тестирование hooks по отдельности

bash
# Test with sample JSON input
echo '{"tool_name": "Bash", "tool_input": {"command": "ls -la"}}' | python3 .claude/hooks/validate-bash.py

# Check exit code
echo $?

Полный пример конфигурации

json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/validate-bash.py\"",
            "timeout": 10
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/format-code.sh\"",
            "timeout": 30
          },
          {
            "type": "command",
            "command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/security-scan.py\"",
            "timeout": 10
          }
        ]
      }
    ],
    "UserPromptSubmit": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/validate-prompt.py\""
          }
        ]
      }
    ],
    "SessionStart": [
      {
        "matcher": "startup",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/session-init.sh\""
          }
        ]
      }
    ],
    "Stop": [
      {
        "hooks": [
          {
            "type": "prompt",
            "prompt": "Verify all tasks are complete before stopping.",
            "timeout": 30
          }
        ]
      }
    ]
  }
}

Детали выполнения hook

AspectBehavior
Timeout600 seconds default for command/http/mcp_tool (30 for prompt, 60 for agent); configurable per hook
ParallelizationAll matching hooks run in parallel
DeduplicationIdentical hook commands deduplicated
EnvironmentRuns in current directory with Claude Code's environment

Устранение неполадок

Hook не выполняется

  • Убедитесь, что синтаксис JSON-конфигурации корректен
  • Проверьте, что паттерн matcher совпадает с именем инструмента
  • Убедитесь, что скрипт существует и является исполняемым: chmod +x script.sh
  • Запустите claude --debug, чтобы увидеть логи выполнения hook
  • Убедитесь, что hook читает JSON из stdin (а не из аргументов командной строки)

Hook неожиданно блокирует выполнение

  • Протестируйте hook на тестовом JSON: echo '{"tool_name": "Write", ...}' | ./hook.py
  • Проверьте код выхода: 0 - разрешить, 2 - заблокировать
  • Проверьте вывод в stderr (отображается при коде выхода 2)

Ошибки парсинга JSON

  • Всегда читайте из stdin, а не из аргументов командной строки
  • Используйте полноценный парсер JSON (а не манипуляции со строками)
  • Корректно обрабатывайте отсутствующие поля

Установка

Шаг 1. Создайте директорию для hooks

bash
mkdir -p ~/.claude/hooks

Шаг 2. Скопируйте примеры hooks

bash
cp 06-hooks/*.sh ~/.claude/hooks/
chmod +x ~/.claude/hooks/*.sh

Шаг 3: Настройка в settings

Отредактируйте ~/.claude/settings.json или .claude/settings.json, добавив конфигурацию hook, показанную выше.

Связанные концепции

  • Checkpoints и Rewind - сохранение и восстановление состояния диалога
  • Slash Commands - создание собственных slash commands
  • Skills - переиспользуемые автономные возможности
  • Subagents - делегирование выполнения задач
  • Plugins - упакованные расширения
  • Продвинутые возможности - знакомство с расширенными возможностями Claude Code

Дополнительные материалы


Последнее обновление: 2 сентября 2026 г. Версия Claude Code: 2.1.257 Источники:

ЛОКАЛЬНАЯ ОТМЕТКА · БЕЗ ПРОВЕРКИ
←ПРЕДЫДУЩИЙMCP (Model Context Protocol)
СЛЕДУЮЩИЙClaude Code Plugins→