SpyBara
Go Premium

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

This page contains 336 additions and 0 deletions.

2026
Thu 1 23:02

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

Обрабатывайте события 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 с изменённой копией события. Само событие неизменяемо: оно заморожено на каждой глубине, и присваивание полю выбрасывает исключение. Этот hook обрезает каждую подсказку перед её отправкой:

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.*' совпадает с каждым событием hook настроек. '*' совпадает с каждым событием, кроме событий телеметрии, которые вы подключаете по имени или как 'telemetry.*'.

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

Подключить то, что делает Claude

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

Охранять или изменять вызов инструмента

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

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

// Matcher ограничивает hook вызовами Bash, поэтому e.command — это команда оболочки
on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
  if (/git push .*--force/.test(e.command)) {
    // Возврат без вызова next отвечает на событие, поэтому команда никогда не запускается
    return { deny: 'Force pushes are not allowed in this repository. Push to a new branch instead.' }
  }
  // Каждая другая команда переходит к проверке разрешений, а затем к Bash
  return next(e)
})

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

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

on('tool.call', { tool: ['Edit', 'Write'] }, async ($, e, next) => {
  // Дождитесь проверки разрешений и инструмента, и сохраните то, что они произвели
  const result = await next(e)
  // Отказанный вызов возвращается как { deny }, а неудачный имеет установленный isError
  const changed = !result.deny && !result.isError
  if (changed && e.file_path.endsWith('.mdx')) $.ui.log('Claude changed ' + e.file_path)
  // Верните результат как он пришёл, чтобы Claude читал то, что вернул инструмент
  return result
})

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

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

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

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

Hook может приостановить вызов инструмента и спросить пользователя, что делать, прежде чем он продолжится. Hook 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) => {
    // Пропустите каждую другую команду без вопроса
    if (!RISKY.test(e.command)) return next(e)
    // Начните с безопасного ответа, чтобы вопрос, на который никто не ответит, отказал в команде
    let answer = 'Refuse'
    try {
      // Вызов инструмента ждёт здесь, пока пользователь выберет один из двух ярлыков
      answer = await $.ui.ask('Run this command? ' + e.command, ['Run it', 'Refuse'])
    } catch {
      // Пользователь отклонил вопрос, или это запуск claude -p без кого-либо, кого можно спросить
    }
    if (answer !== 'Run it') {
      // Ответьте без вызова next, чтобы команда не запускалась
      return { deny: 'The user declined this command. Ask before trying a different approach.' }
    }
    return next(e)
  })
}

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

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

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

Переписать или добавить к подсказке

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

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

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

on('prompt.submit', async ($, e, next) => {
  // Передайте подсказку, которая не упоминает pull request, как она есть
  if (!/\bPR\b|pull request/i.test(e.text)) return next(e)
  const git = await $.process.run(['git', 'branch', '--show-current'])
  // Вне репозитория git команда не удаётся, поэтому нет ветки для добавления
  if (git.exitCode !== 0) return next(e)
  // Сохраните любой контекст, который добавил более ранний hook, и добавьте ещё одну строку для 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 для текста навыка. Текст из этих hooks, который меняется между запросами, делает кэш подсказок недействительным.

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

Ход — это всё, что Claude делает в ответ на одну подсказку. Подключите turn.start, turn.step и turn.complete, чтобы следить за одним:

Событие Когда оно срабатывает Что может делать hook
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' }, чтобы показать строку под ответом

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

// function* делает hook генератором, который может передавать ответ по частям
on('turn.step', async function* ($, e, next) {
  // Отправьте запрос, пересылайте каждую часть по мере её поступления и сохраняйте завершённый результат
  const result = yield* next(e)
  // Пропустите результат, который не сообщает количество токенов
  if (result.usage) {
    $.ui.log('cache read ' + result.usage.cache_read_input_tokens + ' · wrote ' + result.usage.cache_creation_input_tokens)
  }
  // Верните результат без изменений, чтобы ход продолжался как обычно
  return result
})

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

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

Подключить события hook настроек

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

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

on('classic.Stop', async ($, e, next) => {
  // e имеет те же поля, которые hook Stop в файле настроек читает из stdin
  $.ui.log('Transcript saved at ' + e.transcript_path)
  // Передайте событие дальше, чтобы hooks Stop в ваших файлах настроек всё ещё запускались
  return next(e)
})

Каждый раз, когда Claude заканчивает отвечать, тусклая строка в расшифровке даёт путь файла расшифровки.

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

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

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

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 — это событие, где Claude Code решает, может ли вызов инструмента запуститься. Оно срабатывает после этих hooks и правил разрешений, и next(e) разрешается в их решение. Hook на tool.check может вернуть другое решение, такое как { decision: 'allow' }, поэтому он может одобрить вызов, который hook из второй группы заблокировал. Расширить разрешения с помощью hooks перечисляет, какие решения имеют приоритет над модом.

Обработать 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 и запустил бы команду. Обработчик имеет одну секунду для ответа.

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