Agrupe os eventos por noun
O arquivo de tipos lista mais de cem eventos. Parece muito até você notar o padrão: todo nome é noun.ação. O noun diz de que parte da sessão se trata. A ação diz o que aconteceu.
Então a primeira pergunta nunca é "qual evento?". É "de que parte?". Ferramenta é tool.*. Tela é ui.*. O que você digitou é prompt.*. Achou o noun, a lista encolhe para meia dúzia de nomes.
🆕 Novo aqui? Evento e chamada têm o mesmo nome
- Evento — algo que o Claude Code faz e anuncia, como
tool.call. Você pendura um hook nele comon('tool.call', hook). - Chamada — algo que o seu mod pede pelo
$, como$.fs.read. Nos tipos, essa chamada também é um evento (fs.read), que outro mod pode observar. - noun.* — jeito curto de dizer "todos os eventos daquele noun", como
session.*.
| Noun | Parte da sessão | Eventos que você mais vai usar |
|---|---|---|
session.* | a sessão inteira | session.start, session.end, session.append |
prompt.* | o que a pessoa manda | prompt.submit, prompt.context |
turn.* | uma resposta do modelo | turn.start, turn.step, turn.complete |
tool.* | as ferramentas | tool.call, tool.describe |
ui.* | a tela | ui.render, ui.press, ui.input, ui.select |
command.* | os comandos de barra | command.run |
state.* | valores guardados pelo host | state.set |
process.* | programas da máquina | process.run, process.spawn |
classic.* | os hooks do settings.json | classic.Stop, classic.PreToolUse e mais 31 |
O que olhar na tabela: nove nouns cobrem quase tudo o que um mod faz. Os outros (agent, skill, telemetry, config...) aparecem quando você precisar deles, e seguem a mesma regra de nome.
o que o Claude roda
cada resposta
o que aparece
hooks de settings
Siga uma volta completa do turno
O Rafa digita "corrija o teste que quebrou" e aperta Enter. Até o Claude responder, cinco eventos passam pela cadeia, sempre na mesma ordem. Saber essa ordem é saber onde pendurar cada coisa.
O miolo se repete: o modelo responde um pedaço (turn.step), pede uma ferramenta (tool.call), lê o resultado e responde de novo. Um turno de verdade dá várias voltas nesse laço antes de terminar.
Como ler o desenho: leia da esquerda para a direita. Os dois blocos azuis formam o laço: cada pedido de ferramenta gera um tool.call, e o resultado volta para o modelo num novo turn.step. Quando o modelo para de pedir ferramentas, o turno termina.
prompt.submit
O texto foi enviado e o turno ainda não começou. É aqui que se reescreve ou se barra um prompt.
turn.start
Antes da primeira chamada ao modelo. Só observa: next(e) devolve o turnId, e devolver outra coisa não muda nada.
turn.step
Uma chamada ao modelo, chegando em pedaços. É um stream (tópico 4).
tool.call
O engine vai rodar uma ferramenta. next(e) passa pelos hooks de baixo e depois pelo pedido de permissão e pela ferramenta em si.
turn.complete
O turno acabou. next(e) devolve { text }, a resposta; e.reason diz por que parou.
💡 O exemplo oficial usa três desses cinco
A faixa de exemplo da referência (plugin-authoring/examples/band.tsx) marca a hora em prompt.submit, conta ferramentas em tool.call e calcula a duração em turn.complete. Três eventos, um dado: quanto tempo o turno levou. Você vai abrir esse arquivo no módulo 2.3.
Use matchers para filtrar
Você já viu o matcher no módulo 1.1: o objeto entre o nome do evento e o hook. Agora a regra completa. O matcher é um pedaço do próprio e: o hook só roda quando os campos que você escreveu batem com os do evento.
Por isso o campo muda de evento para evento. No tool.call, o e tem tool. No command.run, tem command. No ui.render, tem component. O matcher usa os mesmos nomes.
// templates/starter-mod/hooks/starter.mjs
on("tool.call", { tool: "Read" }, hook)
// inema-mods/mods/recibo-sessao/hooks/register.tsx
on('command.run', { command: 'recibo' }, hook)
on('ui.render', { component: 'Pane', requestId: PAINEL }, hook)
// plugin-authoring/examples/band.tsx
on('ui.render', { component: 'AbovePrompt' }, hook)
Pane) e é o painel deste mod.| Valor no matcher | Como compara | Exemplo |
|---|---|---|
| texto, número, booleano | igual, exatamente | { tool: 'Edit' } |
| expressão regular | testa o valor como texto | { command: /^p4 / } |
| lista | basta um item bater | { tool: ['Edit', 'Write'] } |
| objeto | parte de um objeto de dentro | { props: { origin: { kind: 'task-notification' } } } |
O que olhar na tabela: as quatro formas vêm do comentário do tipo Matcher nos tipos. Não existe startsWith nem outra função: para prefixo, use a expressão regular.
✓ Bom uso
- ✓ Um
onpor ferramenta, cada um com seu matcher - ✓
classic.*para ouvir todos os hooks clássicos - ✓ Matcher no
ui.rendersempre: sem ele, o hook roda para cada linha da conversa
✗ Evite
- ✗ Hook sem matcher em
tool.callcom umifenorme por dentro - ✗ Inventar campo: o tipo recusa matcher com nome que o
enão tem - ✗ Contar com
{ startsWith: 'p4' }: nunca casa
Trate os eventos que são stream
Dois eventos não chegam inteiros: turn.step (a resposta do modelo, pedaço por pedaço) e process.spawn (a saída de um programa, pedaço por pedaço). Neles o hook tem outra forma.
Em vez de async ($, e, next) =>, é um gerador assíncrono: async function* ($, e, next). É a única forma que carrega nesses dois eventos. Uma função comum dá erro de tipo, mesmo que só devolva next(e).
🆕 Novo aqui? Stream e gerador assíncrono
- Stream — um evento que entrega várias partes ao longo do tempo e só no fim tem um resultado.
async function*— função que entrega valores um a um comyielde pode esperar comawait.yield* next(e)— repassa todas as partes de baixo, sem mexer, e no fim vale o resultado do evento.
// turn.step é um fluxo: o hook é um gerador assíncrono que repassa tudo e,
// no fim, lê o resultado (modelo, esforço e ferramentas pedidas nesse passo).
on('turn.step', async function* ($, e, next) {
const r = yield* next(e)
if (e.agentId !== undefined) return r
const modelo = r.usage?.model ?? e.model
const esforco = esforcoEmTexto(e.effort)
const usadas = r.toolUses.map(u => tipoDaFerramenta(u.name))
// ... grava modelo, esforço e ferramentas no estado ...
return r
})
yield* e só trabalha com o resultado final r. É o caso mais comum.Como ler o desenho: cada barrinha é um pedaço do stream. Na primeira linha os pedaços saem iguais. Na segunda saem de outra cor: o hook mudou cada um. Na terceira, nada vem de baixo: o hook inventa os próprios pedaços e responde sozinho.
⚠️ Pedaço entregue fica entregue
Se o hook falha no meio do stream, o que ele já entregou com yield fica, e o resto vem direto de baixo. Os tipos mostram o jeito de transformar a saída de um process.spawn: for await (const chunk of next(e)) yield { ...chunk, text: ... }. Faça a transformação ser segura pedaço a pedaço.
Pendure-se nos hooks clássicos
Os hooks clássicos do settings.json têm 33 eventos: Stop, PreToolUse, FileChanged, PreCompact... Um mod enxerga todos com o prefixo classic., mesmo que nenhum hook clássico esteja configurado.
O e é exatamente o JSON que um hook clássico receberia na entrada padrão, com transcript_path, cwd e os outros campos de base. A exceção é classic.PreToolUse: ali o e é o mesmo envelope do tool.call.
import type { Register } from 'claude-code'
export const register: Register = (on, options) => {
on('classic.Stop', async ($, e, next) => {
// e é o JSON do hook Stop: stop_hook_active, last_assistant_message...
if (!e.stop_hook_active) $.ui.toast('O Claude terminou. Confira o resultado.')
return next(e)
})
}
stop_hook_active e last_assistant_message vêm do tipo StopHookInput nos tipos. São os mesmos que um script de shell leria.| Você quer | Evento nativo | Clássico equivalente |
|---|---|---|
| saber que o turno acabou | turn.complete | classic.Stop |
| ver uma ferramenta antes de rodar | tool.call | classic.PreToolUse |
| saber que um arquivo mudou fora | não há | classic.FileChanged |
| agir antes de compactar | não há | classic.PreCompact |
O que olhar na tabela: quando existe evento nativo, prefira ele: o e é mais rico e o resultado é mais fácil de usar. O clássico serve para o que só os hooks de settings anunciam.
✓ Use classic.*
- ✓ para o que só os hooks de settings anunciam (
FileChanged,PreCompact) - ✓ quando quer ler os mesmos campos que um script de shell leria
- ✓ com matcher ou nome exato, para não ouvir os 33 sem querer
✗ Evite
- ✗
classic.Stopquandoturn.completejá resolve - ✗
classic.PreToolUseno lugar detool.callsem motivo - ✗ supor que o
edo clássico tem o formato de um evento nativo
💡 Onde o mod fica na fila dos clássicos
Os tipos descrevem a ordem: primeiro os hooks clássicos gerenciados pela empresa, depois os mods, depois os outros hooks de settings. Um bloqueio gerenciado termina a cadeia antes de chegar ao seu mod. A cadeia é o assunto do módulo 2.2.
Ache qualquer evento nos tipos
A lista deste módulo vale para a 2.1.289. A da sua máquina pode ser outra. A autoridade é um arquivo que o próprio Claude Code escreve dentro da pasta do mod, a cada carga e recarga: .claude-plugin/types/claude-code/index.d.ts.
Ele tem cerca de 20 mil linhas. Ninguém lê de cima a baixo. Você busca o nome do evento entre aspas seguido de dois-pontos, por exemplo 'turn.step':, no editor ou com grep. O comentário logo acima diz quando o evento dispara, o que next(e) devolve e o que você pode devolver.
Na pasta de um mod que já carregou ao menos uma vez com claude --plugin-dir:
ls .claude-plugin/types/
Depois, cole no Claude Code (ele só lê):
Leia .claude-plugin/types/claude-code/index.d.ts e me mostre o comentário do evento 'turn.complete': quando dispara, o que next(e) devolve e o que um hook pode devolver no lugar. Não edite nada.
ls lista claude-code/, claude-code-tools/, claude-code-mcp/ e tsconfig.json. O Claude responde citando o comentário: dispara no fim do turno, next(e) devolve { text }, e um { text } diferente aparece abaixo da resposta sem mudar o registro.Outro atalho: o claude plugin validate lista os eventos que o seu mod pendura, com o matcher entre chaves. É a prova de que o engine leu o que você quis dizer.
claude plugin validate templates/starter-mod (Claude Mods Starter Kit, da Prompt Advisers, MIT; 05/10/2026) ❯ ./starter.mjs hooks: session.start, tool.call{tool=Read}, command.run{command=readcount}
❯ ./starter.mjs calls: $.command.register
✔ Validation passed
Teste rápido (opcional): o Rafa quer contar os caracteres da resposta do modelo enquanto ela chega. Qual hook carrega?
escrito a cada carga
o que buscar
quando, next, retorno
lista seus hooks
🎓 Resumo do módulo
Próximo módulo:
2.2 — A cadeia: next, reescrever e responder