SpyBara
Go Premium

plugins/mods/events.md 2026-10-01 23:59 UTC to 2026-10-02 13:00 UTC

This page contains 90 additions and 63 deletions.

2026
Thu 1 23:59 Fri 2 13:00

Реагировать на события с помощью мода

Обрабатывайте события Claude Code из мода: наблюдайте, переписывайте или отвечайте на вызовы инструментов, подсказки и ходы, фильтруйте события, которые обрабатывает hook, и планируйте для других модов.

Hook — это обработчик события: функция, которую Claude Code запускает при возникновении именованного события. Claude Code генерирует событие в каждой точке, где он собирается действовать, например, когда он запускает инструмент, отправляет подсказку, отправляет запрос к модели или начинает или завершает сеанс. Ваш hook запускается перед тем, как Claude Code действует, поэтому он может наблюдать событие, переписать его или ответить на него вместо Claude Code. Вы регистрируете hook с помощью on(eventName, handler).

Создайте свой первый мод перед тем, как начать здесь. Для каждого события и его точных полей см. справочник или прочитайте типы для вашей сборки.

Как hook обрабатывает событие

Hook находится между событием и тем, что Claude Code сделает с ним, поэтому он может наблюдать событие, переписать его или ответить на него сам. Он получает три аргумента: mods API как $, событие как e и следующий обработчик как next. Обработчики события образуют цепочку middleware. next(e) вызывает следующий обработчик, который является hook другого мода или, в конце цепочки, собственным поведением Claude Code, и разрешается в результат. То, что ваш hook делает с next, определяет, какой из трёх вариантов он выполняет.

Наблюдать событие

Чтобы наблюдать событие без его изменения, выполните свою работу и верните next(e). Этот hook регистрирует каждый инструмент, который Claude собирается использовать:

on('tool.call', async ($, e, next) => {
  // Запускается перед запуском инструмента
  $.ui.log('Claude is about to use ' + e.tool)
  // Передайте событие дальше без изменений
  return next(e)
})

Перед запуском каждого инструмента в расшифровке появляется тусклая строка, такая как ● my-mod: Claude is about to use Bash, где my-mod — это имя вашего плагина. Инструмент запускается так же, как без мода.

Чтобы действовать после события, await next(e), выполните свою работу и верните результат. Этот hook регистрирует каждый инструмент после его запуска:

on('tool.call', async ($, e, next) => {
  // Позвольте инструменту запуститься и дождитесь его результата
  const result = await next(e)
  // Запускается после запуска инструмента
  $.ui.log(e.tool + ' finished')
  // Верните результат без изменений
  return result
})

Строка теперь появляется после завершения каждого инструмента. Claude читает один и тот же результат в любом случае, потому что hook возвращает то, в чём разрешился next(e).

Переписать событие

Чтобы изменить то, на что действует Claude Code, например текст промпта, вызовите next с изменённой копией события. Само событие неизменяемо: оно заморожено на каждой глубине, и присваивание полю выбрасывает исключение. Этот хук обрезает каждый промпт перед его отправкой:

on('prompt.submit', async ($, e, next) => {
  // Передайте копию события с изменённым текстом
  return next({ ...e, text: e.text.trim() })
})

Более поздние обработчики и Claude Code получают обрезанную подсказку и никогда не видят оригинал. Вы также можете изменить результат: await next(e), затем верните копию результата с заменённым полем.

Ответить на событие

Чтобы обработать событие самостоятельно, верните результат без вызова next. Это короткозамыкает цепочку, поэтому более поздние моды и собственное поведение Claude Code не запускаются. Этот hook отказывает в каждой команде Bash:

on('tool.call', { tool: 'Bash' }, async () => {
  // Нет вызова next, поэтому команда никогда не запускается
  return { deny: 'Bash is turned off in this project. Use the file tools.' }
})

Когда Claude пытается выполнить команду Bash, команда не запускается, и Claude читает текст deny как результат инструмента. Каждое событие имеет свою форму результата, которую справочник событий перечисляет.

Фильтровать события, которые обрабатывает hook

Чтобы запустить hook только для некоторых событий, передайте фильтр в качестве второго аргумента on. Claude Code называет фильтр matcher. Это объект, чьи поля сравниваются с полями события, и hook запускается только когда каждое поле совпадает. Поле может быть значением, массивом допустимых значений или регулярным выражением.

Каждая строка в этом примере регистрирует одну и ту же функцию, hook, для более узкого набора вызовов инструментов:

// Строка совпадает с одним значением: только вызовы Bash
on('tool.call', { tool: 'Bash' }, hook)
// Массив совпадает с любым значением в нём: вызовы Edit и Write
on('tool.call', { tool: ['Edit', 'Write'] }, hook)
// Регулярное выражение совпадает по шаблону: каждый инструмент одного сервера MCP
on('tool.call', { tool: /^mcp__github__/ }, hook)

hook запускается один раз для вызова Bash, Edit или Write и один раз для вызова инструмента, имя которого начинается с mcp__github__. Вызов любого другого инструмента, такого как Read, не совпадает ни с одним из трёх, поэтому hook не запускается для него.

Имя события может быть подстановочным символом. 'classic.*' совпадает с каждым событием хуков настроек. '*' совпадает с каждым событием, кроме событий телеметрии, которые указываются по собственному имени и с фильтром { to: 'collector' }.

Регистрируйте каждое событие один раз для каждого matcher. Если вы вызовете on дважды для session.start без matcher, модуль не загружается с ошибкой on("session.start") is registered twice without a matcher. Поместите всё, что ваш мод делает при запуске сеанса, в один hook.

Подключиться к тому, что делает Claude

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

Защитить или изменить вызов инструмента

Хук tool.call видит каждый инструмент, который Claude собирается использовать, поэтому он может отказать в вызове, изменить его аргументы или пропустить его. tool.call срабатывает, когда Claude Code собирается запустить инструмент, включая вызовы, которые делает субагент, и вызовы инструментов MCP. e.tool — это имя инструмента, а аргументы инструмента — это поля e, например e.command для Bash. Когда вы вызываете next(e), Claude Code выполняет проверку разрешений, а затем запускает инструмент.

Этот хук отказывает в команде Bash, которая выполняет force-push, и объясняет Claude почему:

// The matcher limits the hook to Bash calls, so e.command is the shell command
on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
  if (/git push .*--force/.test(e.command)) {
    // Returning without calling next answers the event, so the command never runs
    return { deny: 'Force pushes are not allowed in this repository. Push to a new branch instead.' }
  }
  // Every other command goes on to the permission check and then to Bash
  return next(e)
})

Когда Claude пытается выполнить git push --force, команда не запускается и запрос разрешения не появляется, потому что хук не вызывает next. Claude читает текст deny как результат инструмента, поэтому пишите его как инструкцию, по которой Claude может действовать. Любая другая команда Bash выполняется так же, как без мода.

Чтобы действовать после запуска инструмента, выполните await next(e), сделайте свою работу и верните то, что дал вам next. Этот хук записывает в лог каждый файл .mdx, который изменяет Claude, с помощью $.ui.log, который добавляет в транскрипт тусклую строку, которую Claude не читает:

on('tool.call', { tool: ['Edit', 'Write'] }, async ($, e, next) => {
  // Wait for the permission check and the tool, and keep what they produced
  const result = await next(e)
  // A refused call comes back as { deny }, and a failed one has isError set
  const changed = !result.deny && !result.isError
  if (changed && e.file_path.endsWith('.mdx')) $.ui.log('Claude changed ' + e.file_path)
  // Return the result as it came, so Claude reads what the tool returned
  return result
})

После того как Claude редактирует или записывает файл .mdx, тусклая строка в транскрипте называет этот файл. Для файлов другого типа, а также для вызовов, в которых было отказано или которые завершились ошибкой, ничего не записывается. Представление Claude о вызове не меняется, потому что хук возвращает полученный результат.

Чтобы изменить вызов, передайте изменённые аргументы в next. Чтобы повторить попытку вызова, снова вызовите next(e): хук, который видит isError в первом результате, может запустить инструмент второй раз и вернуть этот результат. Чтобы ответить на вызов самостоятельно, верните объект с полем result, например { result: 'Skipped by my-mod' }, не вызывая next. В этом случае запрос разрешения не появляется и инструмент не запускается, поэтому возвращённый вами результат — это всё, что Claude узнает о произошедшем.

Хуки в управляемых настройках вашей организации запускаются раньше любого хука tool.call мода, и блокировка от одного из них является окончательной.

Удерживать вызов инструмента, пока пользователь не решит

Хук может приостановить вызов инструмента и спросить пользователя, что делать, прежде чем вызов продолжится. Хук tool.call может выполнить await перед вызовом next или возвратом, и до этого момента вызов инструмента остаётся в ожидании. Чтобы задать вопрос пользователю, вызовите $.ui.ask. Он показывает ваш вопрос над пронумерованным списком ваших вариантов в диалоговом окне, которое Claude использует, чтобы что-то у вас спросить, и разрешается в метку, которую выбрал пользователь. После ваших вариантов диалоговое окно добавляет строку для ввода другого ответа и строку Chat about this.

Шаблон RISKY в этом примере совпадает с rm -r, rm -rf, git reset --hard и git push с --force и пропускает другие варианты написания, например git push -f. Этот модуль спрашивает, прежде чем запустить команду Bash, совпадающую с шаблоном:

const RISKY = /\brm\s+-rf?\b|\bgit\s+reset\s+--hard\b|\bgit\s+push\b.*--force/

export function register(on) {
  on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
    // Let every other command through without a question
    if (!RISKY.test(e.command)) return next(e)
    // Start from the safe answer, so a question nobody answers refuses the command
    let answer = 'Refuse'
    try {
      // The tool call waits here until the user picks one of the two labels
      answer = await $.ui.ask('Run this command? ' + e.command, ['Run it', 'Refuse'])
    } catch {
      // The user dismissed the question, or this is a claude -p run with nobody to ask
    }
    if (answer !== 'Run it') {
      // Answer without calling next, so the command doesn't run
      return { deny: 'The user declined this command. Ask before trying a different approach.' }
    }
    return next(e)
  })
}

Когда Claude пытается выполнить команду, например rm -rf build, появляется вопрос с этой командой, и команда ждёт ответа:

  • Пользователь выбирает Run it: хук вызывает next(e), и после этого всё равно выполняется обычная проверка разрешений
  • Пользователь выбирает Refuse: команда не запускается, и Claude читает текст deny
  • Пользователь вводит ответ: $.ui.ask разрешается во введённый текст. Хук сравнивает его с Run it, поэтому любой другой текст приводит к отказу в команде.
  • Никто не отвечает: $.ui.ask отклоняется, когда пользователь закрывает вопрос или выбирает Chat about this, а также при запуске claude -p, поэтому блок catch оставляет ответ равным Refuse

Держите ожидание внутри вызова mods API, например $.ui.ask, потому что это время не учитывается в ограничении времени хука. Время, потраченное на ожидание вашего собственного promise, учитывается. Claude Code пропускает хук, превысивший время ожидания, поэтому удерживаемая команда будет выполнена.

Одобрение или отклонение вызова инструмента до вопроса пользователю

Чтобы решить, может ли выполняться вызов инструмента, обработайте tool.check — событие, в котором Claude Code принимает это решение. Оно срабатывает после того, как решение приняли правила разрешений и хуки настроек, и next(e) разрешается в их решение: allow, ask или deny. Ваш хук возвращает это решение или другое. e.input содержит аргументы инструмента, например command для Bash.

Для фиксированной команды или пути используйте правило разрешений, например Bash(npm test), которое не требует кода. Обрабатывайте tool.check, когда решение зависит от того, что верно в данный момент, например от текущей ветки Git или значения, записанного другим хуком.

Этот хук отклоняет git push, пока текущая ветка — main:

on('tool.check', { tool: 'Bash' }, async ($, e, next) => {
  // What the permission rules and settings hooks decided: 'allow', 'ask', or 'deny'
  const decided = await next(e)
  if (!e.input.command.includes('git push')) return decided
  const branch = await $.process.run(['git', 'branch', '--show-current'])
  if (branch.stdout.trim() !== 'main') return decided
  return { decision: 'deny', reason: 'Push from a branch other than main' }
})

На main хук возвращает deny, даже если правило разрешает git push. На другой ветке, а также для других команд вызов получает то же решение, что и без мода.

Хук сопоставляет текст команды, поэтому рассматривайте его как напоминание для Claude. Чтобы заблокировать push в main для всех, защитите ветку на своём Git-хостинге.

Хук может вернуть allow, ask или deny, поэтому он также может одобрить вызов, заблокированный хуком PreToolUse вне управляемых настроек. В разделе Расширение разрешений с помощью хуков перечислено, какие решения имеют приоритет над модом.

Переписать промпт или дополнить его

Хук prompt.submit видит каждый промпт до начала хода, поэтому он может переписать текст или дополнить его. e.text — это то, что было введено.

Чтобы сделать это Верните это
Переписать промпт. Сообщение в транскрипте показывает новый текст. next({ ...e, text: newText })
Добавить после промпта текст, который читает только Claude next({ ...e, context: [...(e.context ?? []), extraText] })
Не допустить отправки промпта { drop: 'the reason' }

Этот хук добавляет для Claude имя текущей ветки всякий раз, когда промпт упоминает pull request:

on('prompt.submit', async ($, e, next) => {
  // Pass on a prompt that doesn't mention a pull request as it is
  if (!/\bPR\b|pull request/i.test(e.text)) return next(e)
  const git = await $.process.run(['git', 'branch', '--show-current'])
  // Outside a git repository the command fails, so there's no branch to add
  if (git.exitCode !== 0) return next(e)
  // Keep any context an earlier hook added, and add one more line for Claude
  return next({ ...e, context: [...(e.context ?? []), 'Current branch: ' + git.stdout.trim()] })
})

Когда вы отправляете промпт, например open a PR for this change, ваше сообщение в транскрипте выглядит так же, а Claude после него также читает строку вида Current branch: feature/auth. Промпт, который не упоминает pull request, проходит без изменений, и git не запускается.

Другие события охватывают остальное, что читает Claude: prompt.section для каждого раздела системного промпта, prompt.context для контекста, отправляемого с первым сообщением, и skill.prompt для текста скилла. Текст из этих хуков, меняющийся между запросами, делает недействительным кэш промптов.

Следить за ходом

Ход — это всё, что Claude делает в ответ на один промпт. Обрабатывайте turn.start, turn.step и turn.complete, чтобы следить за ходом:

Событие Когда срабатывает Что может сделать хук
turn.start Начинается ход Наблюдать. e.turnId идентифицирует ход в двух других событиях.
turn.step Claude Code собирается отправить один запрос к модели. В ходе с вызовами инструментов их несколько. e.agentId задан для запроса субагента. Прочитать использование токенов каждого запроса, отправить его другой модели с помощью next({ ...e, model }) или ответить без вызова модели
turn.complete Ход завершился, включая ход, прерванный пользователем, — в этом случае e.isAborted равно true. e.answer — финальный текст Claude, e.durationMs — сколько времени это заняло, а e.usage — итоговое количество токенов за ход. Ход субагента вызывает его с заданным e.agentId. Наблюдать или вернуть объект с полем text, например { text: 'Done in 12 seconds' }, чтобы показать строку под ответом

Пишите хук turn.step как асинхронный генератор, потому что событие потоковое. yield* next(e) передаёт ответ дальше по мере его поступления и вычисляется в итоговый результат. Этот хук записывает в лог, какую часть каждого запроса Claude API обслужил из кэша промптов:

// function* makes the hook a generator, which can pass the response on piece by piece
on('turn.step', async function* ($, e, next) {
  // Send the request, forward each piece as it arrives, and keep the finished result
  const result = yield* next(e)
  // Skip a result that reports no token counts
  if (result.usage) {
    $.ui.log('cache read ' + result.usage.cache_read_input_tokens + ' · wrote ' + result.usage.cache_creation_input_tokens)
  }
  // Return the result unchanged, so the turn continues as usual
  return result
})

Ответ Claude выводится на экран потоком так же, как без мода. После завершения каждого запроса тусклая строка в транскрипте показывает количество токенов, прочитанных из кэша, и количество записанных в него. В ходе с вызовами инструментов несколько запросов, поэтому он добавляет несколько строк.

result.usage содержит количество токенов, которое Claude API сообщает для запроса, а также model, которая ответила: input_tokens, output_tokens, cache_read_input_tokens и cache_creation_input_tokens. Хук запускается и для запросов субагентов, поэтому проверяйте e.agentId, если вам нужен только основной диалог.

Обработка событий хуков настроек

Хуки настроек — это хуки типов command, HTTP, prompt и agent, которые вы настраиваете в файлах настроек. Каждое событие хука настроек, например Stop, SessionEnd или PostToolUse, также является событием с именем classic., за которым следует имя события хука настроек, например classic.Stop. e — это JSON, который хук настроек получает на stdin, включая transcript_path.

Этот хук использует Stop, которое срабатывает, когда Claude заканчивает отвечать, чтобы записать в лог, где сохранён транскрипт сессии:

on('classic.Stop', async ($, e, next) => {
  // e has the same fields a Stop hook in a settings file reads from stdin
  $.ui.log('Transcript saved at ' + e.transcript_path)
  // Pass the event on, so Stop hooks in your settings files still run
  return next(e)
})

Каждый раз, когда Claude заканчивает отвечать, тусклая строка в транскрипте показывает путь к файлу транскрипта. Хук возвращает next(e), поэтому он наблюдает за событием и никак не меняет то, как завершается ход.

Запускаться рядом с другими модами

Несколько модов могут обрабатывать одно и то же событие, и любой из них может дать сбой. Если ваш мод блокирует вызовы инструментов, проверьте его позицию в цепочке и что происходит, когда его хук даёт сбой.

Порядок, в котором запускаются моды

Hooks на одно и то же событие образуют одну цепочку middleware. Каждый next мода вызывает hook следующего мода, и последний next достигает собственного поведения Claude Code. Первый мод является самым внешним: он видит событие перед другими и результат после них, и он решает, запускаются ли другие вообще. Более поздний мод не может остановить более ранний от просмотра события.

Claude Code упорядочивает цепочку по тому, откуда берётся каждый мод:

  1. Встроенная защита sec-default@builtin, мод, встроенный в Claude Code, который /plugin перечисляет как cc-plugin-sec-default, где он загружается, моды, которые ваша организация перечисляет в prependPlugins, а затем любой другой мод, который считается модом вашей организации и не находится в appendPlugins
  2. Моды, которые вы устанавливаете
  3. Моды, которые ваша организация перечисляет в appendPlugins
  4. Другие моды, встроенные в Claude Code

Среди модов, которые вы устанавливаете, мод запускается перед модами, которые он перечисляет в dependencies в своём манифесте. В одном модуле hooks запускаются в порядке, в котором register вызвал on.

Где hooks настроек запускаются в порядке

Hooks PreToolUse, настроенные в файлах настроек, также запускаются во время вызова инструмента в фиксированных точках цепочки модов:

  • Hooks PreToolUse из управляемых настроек: запускаются перед hook tool.call первого мода, и блокировка от одного из них является окончательной, поэтому ни один мод не видит вызов.
  • Hooks PreToolUse из каждого другого файла настроек и из hooks/hooks.json плагинов: запускаются после того, как последний мод вызовет next, как часть собственного поведения Claude Code. Мод, который отвечает на tool.call без вызова next, не позволяет им запускаться, и мод, который вызывает next, видит их решение в результате, который он возвращает.

tool.check срабатывает после того, как эти хуки и правила разрешений приняли решение, поэтому хук на нём может одобрить вызов, который заблокировал хук из второй группы.

Обработать hook, который не удаётся

Hook, который не удаётся, не нарушает сеанс, и вы можете решить, что происходит вместо этого. Когда hook без обработчика .catch выбрасывает исключение, истекает по времени или возвращает результат неправильной формы, что происходит дальше, зависит от того, вызвал ли он next:

  • Он не удался перед вызовом next: Claude Code пропускает его, и следующий обработчик запускается на его месте
  • Он не удался после разрешения next: этот результат стоит, и ничего не запускается второй раз

Одна строка называет мод, событие и причину, такую как my-mod: tool.call hook skipped: threw Error: boom. Где вы это читаете, зависит от сеанса, как Узнать, почему мод ничего не делает перечисляет. Hook ui.render, чей рисунок не проходит валидацию, сообщается иначе, как Построить дерево из элементов описывает.

Чтобы сделать hook, который блокирует вызовы, не удаётся закрыто, добавьте обработчик ошибок .catch, который отвечает на его месте. Здесь guard — это ваша функция hook:

// on возвращает регистрацию, и .catch присоединяет обработчик к этому одному hook
on('tool.call', { tool: 'Bash' }, guard).catch(async ($, e, next) => {
  // next.error.kind — это 'throw' или 'timeout', что говорит, как guard не удался
  return { deny: 'The command guard failed, so this command was not run: ' + next.error.kind }
})

Пока guard работает, обработчик никогда не запускается. Когда guard выбрасывает исключение или превышает таймаут при вызове Bash, Claude Code вызывает обработчик с тем же событием. Обработчик возвращает { deny }, поэтому команда не запускается, и Claude читает текст с throw или timeout в конце. Без обработчика Claude Code пропустил бы guard и запустил бы команду. У обработчика есть собственное, более короткое ограничение по времени.

Следующие шаги