Pular para o conteúdo
MÓDULO 2.2

⛓️ A cadeia: next, reescrever e responder

Quem chama next decide. Todo hook escolhe entre três atitudes: deixar passar, mudar o que passa ou responder no lugar de todo o resto. Este módulo mostra as três, e o que acontece quando um hook falha ou demora.

6
Tópicos
~35
Minutos
Médio
Nível
Fundamento
Tipo
0 de 60%
1

Entenda a cadeia de plugins

O Rafa tem três mods ligados, e os três penduram hook em tool.call. Quando o Claude pede um Edit, os três rodam. Não lado a lado: um dentro do outro, como camadas de cebola.

O evento entra pela camada de fora. Cada hook chama next(e) para entrar na camada seguinte. No miolo está o comportamento do próprio Claude Code. O resultado volta pelo mesmo caminho, de dentro para fora, e cada hook pode olhar para ele na volta.

🆕 Novo aqui? Três palavras da cadeia

  • Cadeia — todos os hooks do mesmo evento, de todos os plugins, mais o comportamento do Claude Code no fim, ligados por next.
  • Dispatch — uma passagem de um evento pela cadeia. Cada Edit é um dispatch de tool.call.
  • Camada (tier) — os tipos dividem a cadeia em cinco: prepend, user, append, builtin, core. Os seus mods ficam em user.
prepend · gerenciados (antes) user · os seus mods append · gerenciados (depois) builtin · plugins de fábrica corepermissão + ferramenta next(e) ↓ resultado ↑

Como ler o desenho: quanto mais de fora, mais autoridade. Um plugin gerenciado pela empresa (prepend) vê o evento antes do seu mod e pode encerrar a cadeia ali. O seu mod vê o evento antes dos plugins de fábrica e do engine. Na volta, a ordem se inverte.

💡 Não dependa da ordem entre dois mods seus

Os dois ficam na mesma camada user. Escreva cada mod como se fosse o único: ele recebe um e, chama next e cuida do próprio resultado. O exemplo de quem convive bem na mesma tela é a faixa, no módulo 3.2.

2

Observe e passe adiante

A maioria dos hooks só observa. Anota, conta, desenha, e devolve o que veio de baixo sem mexer. Existem dois jeitos, e a diferença é quando o hook olha.

Antes do next, ele vê a intenção: o Claude quer editar este arquivo. Depois do await next(e), ele vê o que aconteceu: editou, falhou ou foi negado. Os tipos chamam isso de escrever o hook "em pós-ordem".

📄 Dois jeitos de observar (trechos reais)
// plugin-authoring/examples/band.tsx — olha ANTES e passa
on('prompt.submit', async ($, e, next) => {
  startedAt = await $.clock.now()
  tools = 0

  return next(e)
})

// inema-mods/mods/recibo-sessao/hooks/register.tsx — olha DEPOIS
on('tool.call', { tool: 'Edit' }, async ($, e, next) => {
  const ran = await next(e)
  if (ran.deny === undefined && ran.isError !== true) await registrar($, e.file_path, 'editado')
  return ran
})
O que olhar: o recibo só anota a edição se ela deu certo (sem deny, sem isError). Por isso precisa esperar o next. E devolve ran sem tocar: quem está acima recebe o resultado intacto.

✓ Observador bem-educado

  • ✓ Sempre devolve o que next devolveu
  • ✓ Olha depois do next quando precisa do resultado
  • ✓ Trabalho rápido: anotar no estado, não processar o mundo

✗ Observador que atrapalha

  • ✗ Esquece de devolver o resultado do next
  • ✗ Conta antes do next e soma as edições negadas
  • ✗ Faz trabalho lento antes do next e atrasa a ferramenta

🆕 Novo aqui? Resultado do next

Cada evento tem o seu. No tool.call vem a resposta da ferramenta, com isError e, se alguém negou, deny. No turn.complete vem { text }. No session.start vem { cwd }. O comentário do evento nos tipos diz qual é (módulo 2.1, tópico 6).

3

Reescreva o que vem depois

O e que você recebe é congelado: não dá para mudar um campo dele. Para mudar o que as camadas de baixo veem, você chama next com uma cópia alterada: next({ ...e, campo: novo }).

Mas só dentro do que o evento permite. Cada evento diz nos tipos o que pode ser reescrito. Mudar um campo travado faz o hook falhar e ser pulado. Mudar o retorno de um evento que só observa não muda nada.

📄 inema-mods/mods/roteador-subagente/hooks/register.tsx (trecho)
on('agent.spawn', async ($, e, next) => {
  // Respeita o que foi pedido: modelo explícito, fork (sempre herda) e teammate.
  if (e.model !== undefined || e.fork || e.isTeammate === true) return next(e)
  const alvo = destino(await read($, modo), mapa, e.subagentType)
  if (alvo === undefined) return next(e)
  const r = await next({ ...e, model: alvo })
  const id = r.agentId
  if (id !== undefined) await update($, roteados, x => ({ ...x, [id]: alvo }))
  return r
})
O que olhar: o mod só reescreve quando ninguém pediu modelo. Nos outros casos, passa o e intacto. Reescrever o que a pessoa pediu de forma explícita seria passar por cima dela.
EventoO que dá para reescreverEfeito
prompt.submitnext({ ...e, text })o modelo e a tela recebem o texto novo
agent.spawnnext({ ...e, model })o subagente roda com outro modelo
ui.rendernext({ ...e, props })o engine desenha com outras props
session.appendsó o content da mensagemmudar outro campo faz o hook falhar
turn.startnadasó observa; outro retorno não muda nada

O que olhar na tabela: a última linha é tão importante quanto as outras. Antes de reescrever, confira nos tipos se o evento aceita. Se o comentário diz "Observe", ele não aceita.

✓ Reescrita respeitosa

  • ✓ muda só o que ninguém pediu de forma explícita, como o roteador
  • ✓ confere nos tipos se o campo pode mudar
  • ✓ avisa na tela quando muda algo que a pessoa vê

✗ Reescrita que atropela

  • ✗ troca o modelo que a pessoa escolheu
  • ✗ mexe em campo travado do session.append
  • ✗ espera efeito ao devolver outra coisa no turn.start

⚠️ Reescrever o prompt é poderoso e invisível

Um prompt.submit que muda o texto altera o que o modelo lê sem a pessoa perceber. Faça isso só quando o mod disser claramente o que faz, e de preferência com um aviso na tela.

4

Responda sem chamar next

Um hook que devolve algo sem chamar next responde no lugar de todo o resto. As camadas de baixo não rodam: nem os outros mods, nem o engine. No tool.call, isso inclui o pedido de permissão e a ferramenta.

É assim que um mod barra uma ação. O guarda-colisao do kit INEMA decide antes de qualquer coisa. Se decidiu barrar, devolve a recusa e acabou.

📄 inema-mods/mods/guarda-colisao/hooks/register.tsx (trecho)
on('tool.call', { tool: 'Edit' }, async ($, e, next) => {
  const barrar = await decidir($, e.file_path, cfg)
  if (barrar) return barrar          // { deny: '...' } e nada abaixo roda
  const ran = await next(e)
  if (ran.deny === undefined && ran.isError !== true) await marcar($, e.file_path, cfg)
  return ran
})
O que olhar: decidir é uma função declarada no topo do arquivo, porque recebe $ (pegadinha 1 do kit INEMA). Ela devolve { deny } ou undefined.
EventoResposta sem nextO que acontece
tool.call{ deny: motivo }a ferramenta não roda; o modelo lê o motivo
tool.call{ result }o mod responde pela ferramenta (sem pedir permissão)
prompt.submit{ drop: motivo }o prompt não vira turno
command.run{ text }o comando do mod responde, sem modelo

⚠️ Responder com { result } pula a permissão

O pedido de permissão mora no core, que é a camada de dentro. Quem responde sem next nunca chega lá. Para ferramentas suas isso é o normal (módulo 3.4). Para ferramentas do Claude, como Bash, é atalho perigoso: prefira negar com motivo. Outro detalhe dos tipos: um hook que devolve enquanto o seu next ainda está pendente aborta o que roda abaixo.

🚫
deny

recusa a ferramenta

📦
result

responde por ela

🗑️
drop

descarta o prompt

💬
text

responde o comando

5

Use .catch para falhar com segurança

Hook que lança erro, devolve o que o evento não aceita ou passa do tempo é pulado. O engine chama next(e) por ele e a cadeia segue como se ele não existisse. Para um contador, ótimo. Para uma guarda, péssimo: falhar vira deixar passar.

O conserto é o .catch na própria registração: on(...).catch(handler). Se o hook falhar, o handler roda no lugar dele, com a mesma forma ($, e, next), e a resposta dele vale como a do hook.

📄 Exemplo da referência oficial (plugin-authoring/reference.md)
on("session.append", ($, e, next) => next({ ...e, message: redact(e.message) }))
  .catch(($, e, next) => next({ ...e, message: placeholderFor(e.message) }))
O que olhar: o hook tenta esconder dados sensíveis. Se a função falhar, o .catch não deixa a mensagem original passar: troca por um texto neutro. Falha segura, não falha aberta.
📄 A mesma ideia numa guarda (exemplo curto, forma tipada)
import type { EngineInterface, Register } from 'claude-code'

// recebe $, então fica no topo do arquivo (pegadinha 1)
async function decidir($: EngineInterface, caminho: string) {
  const fora = !caminho.startsWith(await $.session.cwd())
  return fora ? { deny: 'Fora da pasta do projeto.' } : undefined
}

export const register: Register = (on, options) => {
  on('tool.call', { tool: 'Write' }, async ($, e, next) => {
    const barrar = await decidir($, e.file_path)
    return barrar ?? next(e)
  }).catch(($, e, next) => ({ deny: 'A guarda falhou; escrita barrada por segurança.' }))
}
O que olhar: sem o .catch, um erro em decidir faria o engine chamar next(e) e a escrita passaria. Com ele, a falha vira recusa com motivo. A comparação por texto do caminho é simplificada aqui; a forma robusta, com realPath, está no módulo 2.4.
1

O hook falha

Lançou erro (throw) ou passou do orçamento (timeout).

2

O handler roda do zero

next.error diz o motivo; next.called diz se o hook já tinha chamado next. Aqui o next é seguro: se já foi chamado, devolve o mesmo resultado sem rodar nada de novo.

3

Ele tem 1 segundo

O prazo do handler é de 1.000 ms. Devolveu a tempo, vale. Devolveu undefined ou estourou, o hook conta como ausente.

💡 Três regras do .catch

Um por registração (um segundo .catch lança erro). Só dentro do register (depois que ele retorna, também lança). Em evento de stream, o handler também é um gerador. O kit INEMA usa isso em todo mod que pergunta: sem tela para perguntar, o padrão é seguro e explícito, nunca seguir calado.

6

Respeite o orçamento e o next.signal

Cada hook tem 10 segundos do próprio tempo por dispatch. O relógio para enquanto ele espera um next ou uma chamada ao $. Uma cadeia lenta abaixo, ou uma chamada longa ao engine, não gastam o orçamento do seu hook.

Com uma exceção: as esperas do relógio. $.clock.sleep conta. Um hook que dorme em laço para "esperar alguma coisa" paga cada soneca do mesmo orçamento. O tempo que resta está em next.budget.remainingMs.

seu código await next(e)não conta seu código await $.fs...não conta clock.sleep: conta 10 s só o azul enche o orçamento; no limite, next.signal aborta e o hook conta como ausente

Como ler o desenho: some só os trechos azuis. Esperar o next ou o engine é de graça. Dormir com o relógio não é. Quem chega na linha vermelha perde a vez: o .catch é chamado, se houver, ou o engine segue sem o hook.

O next.signal é um AbortSignal que dispara quando o dispatch é abandonado: a pessoa interrompeu, um hook acima já respondeu, ou o orçamento acabou. Passe esse sinal adiante, para a espera acabar junto.

📄 Do comentário de $.clock.sleep nos tipos
await $.clock.sleep(500, { signal: next.signal })
O que olhar: depois que o sinal dispara, o hook ainda tem 5 segundos para terminar. Passou disso, o engine registra que ele ficou para trás. Trabalho que precisa durar mais que um dispatch vai para outro lugar: módulo 2.4.

Teste rápido (opcional): o hook do Rafa em tool.call devolve { deny: 'não' } sem chamar next. O que acontece com o pedido de permissão e com os mods abaixo dele?

⏱️
10 s

do próprio tempo

🩹
1 s

para o .catch

🐢
5 s

depois do signal

📡
next.signal

pare junto

🎓 Resumo do módulo

✓
A cadeia é uma cebola — prepend, user, append, builtin, core; next entra, o resultado volta.
✓
Observar é devolver o que next devolveu — antes vê a intenção, depois vê o resultado.
✓
next({ ...e, campo }) reescreve — só o que o evento permite.
✓
Sem next, você responde por todos — deny, drop, result, text.
✓
.catch e orçamento — falha segura, 10 s do próprio tempo, next.signal para parar junto.

Próximo módulo:

2.3 — Estado que sobrevive