SpyBara
Go Premium

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

This page contains 92 additions and 65 deletions.

2026
Thu 1 23:59 Fri 2 09:02

Reagir a eventos com um mod

Manipule eventos do Claude Code a partir de um mod: observe, reescreva ou responda chamadas de ferramentas, prompts e turnos, filtre quais eventos um hook manipula e planeje para outros mods.

Um hook é um manipulador de eventos: uma função que Claude Code executa quando um evento nomeado acontece. Claude Code dispara um evento em cada ponto onde está prestes a agir, como quando executa uma ferramenta, envia um prompt, faz uma solicitação ao modelo ou inicia ou encerra uma sessão. Seu hook é executado antes de Claude Code agir, portanto pode observar o evento, reescrevê-lo ou respondê-lo no lugar de Claude Code. Você registra um hook com on(eventName, handler).

Construa seu primeiro mod antes de começar aqui. Para cada evento e seus campos exatos, consulte a referência ou leia os tipos para sua compilação.

Como um hook manipula um evento

Um hook fica entre um evento e o que Claude Code faria sobre ele, portanto pode observar o evento, reescrevê-lo ou respondê-lo por si mesmo. Ele recebe três argumentos: a API de mods como $, o evento como e e o próximo manipulador como next. Os manipuladores de um evento formam uma cadeia de middleware. next(e) chama o próximo manipulador, que é o hook de outro mod ou, no final da cadeia, o comportamento próprio de Claude Code, e é resolvido para o resultado. O que seu hook faz com next decide qual dos três ele faz.

Observe um evento

Para observar um evento sem alterá-lo, faça seu trabalho e retorne next(e). Este hook registra cada ferramenta que Claude está prestes a usar:

on('tool.call', async ($, e, next) => {
  // Executa antes da ferramenta
  $.ui.log('Claude is about to use ' + e.tool)
  // Passe o evento adiante inalterado
  return next(e)
})

Antes de cada ferramenta ser executada, uma linha fraca como ● my-mod: Claude is about to use Bash aparece na transcrição, onde my-mod é o nome do seu plugin. A ferramenta é executada como seria sem o mod.

Para agir após o evento, await next(e), faça seu trabalho e retorne o resultado. Este hook registra cada ferramenta após sua execução:

on('tool.call', async ($, e, next) => {
  // Deixe a ferramenta ser executada e aguarde seu resultado
  const result = await next(e)
  // Executa após a ferramenta
  $.ui.log(e.tool + ' finished')
  // Devolva o resultado inalterado
  return result
})

A linha agora aparece após cada ferramenta terminar. Claude lê o mesmo resultado de qualquer forma, porque o hook retorna o que next(e) foi resolvido.

Reescreva um evento

Para alterar o que Claude Code age, como o texto de um prompt, chame next com uma cópia modificada do evento. O evento em si é imutável: é congelado em cada profundidade e atribuir a um campo lança um erro. Este hook corta cada prompt antes de ser enviado:

on('prompt.submit', async ($, e, next) => {
  // Passe uma cópia do evento com seu texto alterado
  return next({ ...e, text: e.text.trim() })
})

Manipuladores posteriores e Claude Code recebem o prompt cortado e nunca veem o original. Você também pode alterar o resultado: await next(e), depois retorne uma cópia do resultado com um campo substituído.

Responda a um evento

Para manipular um evento você mesmo, retorne um resultado sem chamar next. Isso interrompe a cadeia, portanto mods posteriores e o comportamento próprio de Claude Code não são executados. Este hook recusa cada comando Bash:

on('tool.call', { tool: 'Bash' }, async () => {
  // Nenhuma chamada para next, portanto o comando nunca é executado
  return { deny: 'Bash is turned off in this project. Use the file tools.' }
})

Quando Claude tenta um comando Bash, o comando não é executado e Claude lê o texto deny como o resultado da ferramenta. Cada evento tem sua própria forma de resultado, que a referência de eventos lista.

Filtre quais eventos um hook manipula

Para executar um hook apenas para alguns eventos, passe um filtro como o segundo argumento para on. Claude Code chama o filtro de matcher. É um objeto cujos campos são comparados com os do evento, e o hook é executado apenas quando cada campo corresponde. Um campo pode ser um valor, uma matriz de valores permitidos ou uma expressão regular.

Cada linha neste exemplo registra a mesma função, hook, para um conjunto mais estreito de chamadas de ferramentas:

// Uma string corresponde a um valor: apenas chamadas Bash
on('tool.call', { tool: 'Bash' }, hook)
// Uma matriz corresponde a qualquer valor nela: chamadas Edit e Write
on('tool.call', { tool: ['Edit', 'Write'] }, hook)
// Uma expressão regular corresponde por padrão: cada ferramenta de um servidor MCP
on('tool.call', { tool: /^mcp__github__/ }, hook)

hook é executado uma vez para uma chamada Bash, Edit ou Write, e uma vez para uma chamada a uma ferramenta cujo nome começa com mcp__github__. Uma chamada para qualquer outra ferramenta, como Read, não corresponde a nenhuma das três, portanto hook não é executado para ela.

O nome do evento pode ser um wildcard. 'classic.*' corresponde a cada evento de hook de configurações. '*' corresponde a cada evento exceto os eventos de telemetria, que são registrados pelo próprio nome e com um filtro { to: 'collector' }.

Registre cada evento uma vez por matcher. Se você chamar on duas vezes para session.start sem um matcher, o módulo falhará ao carregar com on("session.start") is registered twice without a matcher. Coloque tudo o que seu mod faz no início da sessão em um hook.

Fazer hook no que o Claude está fazendo

Trate estes eventos para ver ou alterar uma chamada de ferramenta, um prompt ou um turno enquanto acontecem. Para cada evento e o que um hook pode retornar, consulte a referência de eventos.

Proteger ou alterar uma chamada de ferramenta

Um hook tool.call vê cada ferramenta que o Claude está prestes a usar, então pode recusar a chamada, alterar seus argumentos ou deixá-la passar. tool.call é disparado quando o Claude Code está prestes a executar uma ferramenta, incluindo chamadas que um subagente faz e chamadas a ferramentas MCP. e.tool é o nome da ferramenta e os argumentos da ferramenta são campos de e, como e.command para Bash. Quando você chama next(e), o Claude Code executa a verificação de permissão e depois a ferramenta.

Este hook recusa um comando Bash que faz force push e informa ao Claude o motivo:

// 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)
})

Quando o Claude tenta git push --force, o comando não é executado e nenhum prompt de permissão aparece, porque o hook nunca chama next. O Claude lê o texto de deny como o resultado da ferramenta, então escreva-o como uma instrução sobre a qual o Claude possa agir. Todos os outros comandos Bash são executados como seriam sem o mod.

Para agir depois que uma ferramenta foi executada, use await next(e), faça seu trabalho e retorne o que next lhe deu. Este hook registra em log cada arquivo .mdx que o Claude altera, com $.ui.log, que adiciona uma linha esmaecida à transcrição que o Claude não lê:

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
})

Depois que o Claude edita ou escreve um arquivo .mdx, uma linha esmaecida na transcrição indica o nome do arquivo. Nada é registrado em log para outro tipo de arquivo, nem para uma chamada que foi recusada ou falhou. A visão do Claude sobre a chamada não muda, porque o hook retorna o resultado que recebeu.

Para alterar uma chamada, passe argumentos alterados para next. Para tentar novamente uma chamada, chame next(e) outra vez: um hook que vê isError no primeiro resultado pode executar a ferramenta uma segunda vez e retornar esse resultado. Para responder você mesmo a uma chamada, retorne um objeto com um campo result, como { result: 'Skipped by my-mod' }, sem chamar next. Quando você faz isso, nenhum prompt de permissão aparece e a ferramenta não é executada, então o resultado que você retorna é tudo o que o Claude fica sabendo sobre o que aconteceu.

Os hooks nas configurações gerenciadas da sua organização são executados antes do hook tool.call de qualquer mod, e um bloqueio de um deles é definitivo.

Reter uma chamada de ferramenta até o usuário decidir

Um hook pode pausar uma chamada de ferramenta e perguntar ao usuário o que fazer antes que ela prossiga. Um hook tool.call pode usar await antes de chamar next ou retornar, e a chamada de ferramenta permanece pendente até lá. Para fazer a pergunta ao usuário, chame $.ui.ask. Ele mostra sua pergunta acima de uma lista numerada das suas opções, no diálogo que o Claude usa para lhe perguntar algo, e resolve para o rótulo que o usuário escolher. Depois das suas opções, o diálogo adiciona uma linha para digitar uma resposta diferente e uma linha Chat about this.

O padrão RISKY neste exemplo corresponde a rm -r, rm -rf, git reset --hard e git push com --force, e não detecta outras grafias como git push -f. Este módulo pergunta antes de executar um comando Bash que corresponda ao padrão:

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)
  })
}

Quando o Claude tenta um comando como rm -rf build, a pergunta aparece com o comando nela, e o comando aguarda a resposta:

  • O usuário escolhe Run it: o hook chama next(e), e a verificação de permissão habitual ainda é executada depois disso
  • O usuário escolhe Refuse: o comando não é executado, e o Claude lê o texto de deny
  • O usuário digita uma resposta: $.ui.ask resolve para o texto digitado. O hook o compara com Run it, então qualquer outro texto recusa o comando.
  • Ninguém responde: $.ui.ask é rejeitado quando o usuário dispensa a pergunta ou escolhe Chat about this, e em uma execução claude -p, então o bloco catch mantém a resposta como Refuse

Mantenha a espera dentro de uma chamada da API de mods, como $.ui.ask, porque esse tempo não conta para o limite de tempo do hook. O tempo gasto aguardando uma promise sua conta. O Claude Code ignora um hook que excede o tempo limite, então o comando retido seria executado.

Aprovar ou recusar uma chamada de ferramenta antes de o usuário ser consultado

Para decidir se uma chamada de ferramenta pode ser executada, trate tool.check, o evento em que o Claude Code toma essa decisão. Ele é disparado depois que as regras de permissão e os hooks de configuração decidiram, e next(e) resolve para a decisão deles: allow, ask ou deny. Seu hook retorna essa decisão ou uma diferente. e.input contém os argumentos da ferramenta, como command para Bash.

Para um comando ou caminho fixo, use uma regra de permissão como Bash(npm test), que não exige código. Trate tool.check quando a decisão depender do que é verdade naquele momento, como o branch Git atual ou um valor que outro hook registrou.

Este hook recusa git push enquanto o branch atual for 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' }
})

Em main, o hook retorna deny, mesmo quando uma regra permite git push. Em outro branch, e para outros comandos, a chamada recebe a decisão que receberia sem o mod.

O hook compara o texto do comando, então trate-o como um lembrete para o Claude. Para bloquear pushes para main para todos, proteja o branch no seu host Git.

Um hook pode retornar allow, ask ou deny, então também pode aprovar uma chamada que um hook PreToolUse fora das configurações gerenciadas bloqueou. Estender permissões com hooks lista quais decisões prevalecem sobre um mod.

Reescrever ou complementar um prompt

Um hook prompt.submit vê cada prompt antes de o turno começar, então pode reescrever o texto ou complementá-lo. e.text é o que foi digitado.

Para fazer isto Retorne isto
Reescrever o prompt. A mensagem na transcrição mostra o novo texto. next({ ...e, text: newText })
Adicionar texto que só o Claude lê, depois do prompt next({ ...e, context: [...(e.context ?? []), extraText] })
Impedir que o prompt seja enviado { drop: 'the reason' }

Este hook adiciona o nome do branch atual para o Claude sempre que um prompt menciona um 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()] })
})

Quando você envia um prompt como open a PR for this change, sua mensagem aparece igual na transcrição, e o Claude também lê uma linha como Current branch: feature/auth depois dela. Um prompt que não menciona um pull request passa sem alterações, e git não é executado.

Outros eventos cobrem o restante do que o Claude lê: prompt.section para cada seção do system prompt, prompt.context para o contexto enviado com a primeira mensagem e skill.prompt para o texto de uma skill. Texto desses hooks que muda entre requisições invalida o cache de prompt.

Acompanhar um turno

Um turno é tudo o que o Claude faz em resposta a um prompt. Trate turn.start, turn.step e turn.complete para acompanhar um:

Evento Quando é disparado O que um hook pode fazer
turn.start Um turno começa Observar. e.turnId identifica o turno nos outros dois eventos.
turn.step O Claude Code está prestes a enviar uma requisição ao modelo. Um turno com chamadas de ferramenta tem várias. e.agentId é definido para a requisição de um subagente. Ler o uso de tokens de cada requisição, enviá-la para um modelo diferente com next({ ...e, model }) ou responder sem chamar o modelo
turn.complete O turno terminou, incluindo um turno que o usuário interrompeu, em que e.isAborted é true. e.answer é o texto final do Claude, e.durationMs quanto tempo levou e e.usage os totais de tokens do turno. O turno de um subagente o dispara com e.agentId definido. Observar, ou retornar um objeto com um campo text, como { text: 'Done in 12 seconds' }, para mostrar uma linha abaixo da resposta

Escreva um hook turn.step como um gerador assíncrono, porque o evento é transmitido em streaming. yield* next(e) encaminha a resposta à medida que ela é transmitida e resulta no resultado final. Este hook registra em log quanto de cada requisição a API do Claude atendeu a partir do cache de prompt:

// 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
})

A resposta do Claude é transmitida para a tela como acontece sem o mod. Depois que cada requisição termina, uma linha esmaecida na transcrição informa o número de tokens lidos do cache e o número gravado nele. Um turno com chamadas de ferramenta tem várias requisições, então adiciona várias linhas.

result.usage contém as contagens de tokens que a API do Claude informa para uma requisição, mais o model que respondeu: input_tokens, output_tokens, cache_read_input_tokens e cache_creation_input_tokens. O hook também é executado para as requisições de subagentes, então verifique e.agentId quando quiser apenas a conversa principal.

Tratar os eventos de hooks de configuração

Hooks de configuração são os hooks de comando, HTTP, prompt e agente que você configura em arquivos de configuração. Cada evento de hook de configuração, como Stop, SessionEnd ou PostToolUse, também é um evento nomeado classic. seguido do nome do evento de hook de configuração, como classic.Stop. e é o JSON que um hook de configuração recebe no stdin, incluindo transcript_path.

Este hook usa Stop, que é disparado quando o Claude termina de responder, para registrar em log onde a transcrição da sessão está salva:

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)
})

Cada vez que o Claude termina de responder, uma linha esmaecida na transcrição informa o caminho do arquivo de transcrição. O hook retorna next(e), então ele observa o evento e não altera nada em como o turno termina.

Execute ao lado de outros mods

Vários mods podem tratar o mesmo evento e qualquer um deles pode falhar. Se seu mod bloqueia chamadas de ferramentas, verifique sua posição na cadeia e o que acontece quando seu hook falha.

A ordem em que os mods são executados

Hooks no mesmo evento formam uma cadeia de middleware. Cada next de um mod chama o hook do mod seguinte, e o último next atinge o comportamento próprio de Claude Code. O primeiro mod é o mais externo: vê o evento antes dos outros e o resultado após eles, e decide se os outros são executados. Um mod posterior não pode impedir que um anterior veja um evento.

Claude Code ordena a cadeia por onde cada mod vem:

  1. O guard integrado sec-default@builtin, um mod integrado em Claude Code que /plugin lista como cc-plugin-sec-default, onde ele carrega, mods que sua organização lista em prependPlugins e depois qualquer outro mod que conta como de sua organização e não está em appendPlugins
  2. Mods que você instala
  3. Mods que sua organização lista em appendPlugins
  4. Outros mods integrados em Claude Code

Entre os mods que você instala, um mod é executado antes dos mods que lista em dependencies em seu manifesto. Dentro de um módulo, hooks são executados na ordem em que register chamou on.

Onde hooks de configurações são executados na ordem

Os hooks PreToolUse configurados em arquivos de configurações também são executados durante uma chamada de ferramenta, em pontos fixos na cadeia de mods:

  • Hooks PreToolUse de configurações gerenciadas: são executados antes do hook tool.call do primeiro mod, e um bloqueio de um deles é final, portanto nenhum mod vê a chamada.
  • Hooks PreToolUse de cada outro arquivo de configurações e de hooks/hooks.json de plugins: são executados após o último mod chamar next, como parte do comportamento próprio de Claude Code. Um mod que responde tool.call sem chamar next os impede de serem executados, e um mod que chama next vê sua decisão no resultado que retorna.

tool.check dispara após esses hooks e as regras de permissão terem decidido, portanto um hook nele pode aprovar uma chamada que um hook no segundo grupo bloqueou.

Manipule um hook que falha

Um hook que falha não quebra a sessão e você pode decidir o que acontece em seu lugar. Quando um hook sem um manipulador .catch lança, expira ou retorna um resultado da forma errada, o que acontece a seguir depende se ele tinha chamado next:

  • Falhou antes de chamar next: Claude Code o pula e o próximo manipulador é executado em seu lugar
  • Falhou após next ser resolvido: esse resultado permanece e nada é executado uma segunda vez

Uma linha nomeia o mod, o evento e o motivo, como my-mod: tool.call hook skipped: threw Error: boom. Onde você o lê depende da sessão, como Descubra por que um mod não faz nada lista. Um hook ui.render cujo desenho não valida é relatado diferentemente, como Construa uma árvore a partir de elementos descreve.

Para fazer um hook que bloqueia chamadas falhar fechado, adicione um manipulador de erro .catch que responda em seu lugar. Aqui, guard é sua função de hook:

// on retorna um registro e .catch anexa um manipulador a esse hook
on('tool.call', { tool: 'Bash' }, guard).catch(async ($, e, next) => {
  // next.error.kind é 'throw' ou 'timeout', que diz como guard falhou
  return { deny: 'The command guard failed, so this command was not run: ' + next.error.kind }
})

Enquanto guard funciona, o manipulador nunca é executado. Quando guard lança ou expira em uma chamada Bash, Claude Code chama o manipulador com o mesmo evento. O manipulador retorna { deny }, portanto o comando não é executado e Claude lê o texto com throw ou timeout no final. Sem o manipulador, Claude Code pularia guard e executaria o comando. O manipulador tem seu próprio limite de tempo, mais curto.

Próximos passos