SpyBara
Go Premium

plugins/mods/api.md 2026-10-08 22:58 UTC to 2026-10-09 21:01 UTC

This page contains 106 additions and 7 deletions.

2026
Thu 1 23:59 Fri 2 22:59 Thu 8 22:58 Fri 9 23:02

Use the mods API

Chame a mods API de um mod Claude Code para adicionar comandos e ferramentas, chamar um modelo, executar trabalho em um temporizador, enviar mensagens para outras sessões e acessar arquivos e a rede.

A mods API é o conjunto de métodos que um mod chama para agir: adicionar comandos e ferramentas, chamar um modelo, executar trabalho entre eventos e acessar o sistema de arquivos, processos e a rede. Cada hook a recebe como seu primeiro argumento, $, com os métodos agrupados em namespaces como $.ui e $.fs. Events decidem quando um hook é executado, e a mods API é o que o hook chama uma vez que o faz.

Construa seu primeiro mod antes de começar aqui. Para cada método, veja mods API methods ou leia os tipos para sua compilação.

Adicione um comando ou uma ferramenta

Um mod pode adicionar um comando para o usuário executar e uma ferramenta para Claude chamar. Registre ambos em um hook session.start. Claude Code aguarda esse hook antes do primeiro prompt, então o que você registra está disponível desde o primeiro turno.

Adicione um comando

Um comando é para o usuário. Registre-o e, em seguida, manipule command.run para seu nome. Este exemplo adiciona um comando /standup que leva um número opcional de dias:

on('session.start', async ($, e, next) => {
  // Add /standup to the command list, with the description the user sees there
  await $.command.register({ name: 'standup', description: 'Summarize what changed today', argumentHint: '[days]' })
  return next(e)
})

// The matcher limits the hook to /standup, so other commands don't reach it
on('command.run', { command: 'standup' }, async ($, e) => {
  // e.args is the text typed after the command name, or an empty string
  return { text: 'Summary for the last ' + (e.args || '1') + ' day(s): ...' }
})

Após a sessão iniciar, /standup aparece com sua descrição na lista que você vê quando digita /. O argumentHint aparece no prompt após você digitar o comando e um espaço, como em /standup [days]. Quando você executa /standup 3, o segundo hook retorna Summary for the last 3 day(s): ..., e a transcrição mostra esse texto após o nome do plugin. O hook nunca chama next, porque o comando não tem comportamento além do seu.

O text que você retorna é impresso na transcrição e Claude o lê. Para não imprimir nada, como um comando que apenas abre um pane, retorne {}. Para permitir que o comando seja executado enquanto Claude está trabalhando, adicione immediate: true ao registro.

Escolha um nome que nenhum comando integrado use. Digite / em uma sessão para vê-los. $.command.register lança uma exceção para um nome ocupado, com uma mensagem como "/focus" refused: it is the built-in /focus". Um hook que lança uma exceção é ignorado, então o resto do seu hook session.start também não é executado. Registre comandos por último nesse hook ou envolva a chamada em try e catch.

Adicione uma ferramenta

Uma ferramenta é para Claude. Registre-a com um nome, uma descrição que Claude lê e um JSON Schema para sua entrada. Claude a vê sob um nome mais longo feito de mcp__, o nome do seu plugin, dois sublinhados e o nome que você registrou. Você manipula suas chamadas em um hook tool.call filtrado para esse nome completo. Este exemplo, de um plugin chamado my-mod, registra ticket, então o nome completo é mcp__my-mod__ticket. Ele dá a Claude uma ferramenta que procura um ticket em um rastreador de problemas:

on('session.start', async ($, e, next) => {
  await $.tool.register({
    name: 'ticket',
    // Claude decides when to call the tool from this description
    description: 'Look up a ticket by its id and return its title and status',
    // The arguments Claude has to send: one required string named id
    inputSchema: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] },
  })
  return next(e)
})

// The full tool name is mcp__, the plugin's name, and the registered name
on('tool.call', { tool: 'mcp__my-mod__ticket' }, async ($, e) => {
  // The tool's arguments are fields of e, so the id is e.id
  const response = await $.http.fetch('https://tickets.example.com/api/' + encodeURIComponent(e.id))
  // Return a result either way, so Claude learns when the lookup failed
  return { result: response.ok ? response.text : 'Lookup failed with status ' + response.status }
})

Quando você pergunta sobre um ticket, Claude pode chamar mcp__my-mod__ticket com seu id. O segundo hook busca o ticket e retorna o corpo da resposta, que Claude lê como o resultado da ferramenta. Quando o servidor responde com um status de erro, Claude lê Lookup failed with status e o número.

Chame um modelo

Um mod pode enviar suas próprias requisições a um modelo para um pequeno trabalho como classificar ou resumir texto. $.model.complete envia seu prompt isoladamente, e $.model.fork({ prompt }) envia a conversa atual com seu prompt no final.

Esta tabela compara o que cada requisição contém:

Na requisição $.model.complete $.model.fork
Modelo O model que você passa O modelo da sessão
System prompt Um breve bloco de atribuição, seguido do seu system, se você passar um O system prompt da sessão
Mensagens Uma mensagem de usuário, o seu prompt A conversa até o momento, seguida do seu prompt como mensagem de usuário
CLAUDE.md e outro contexto do projeto Não incluído Incluído, como na última requisição da conversa
Ferramentas Nenhuma As ferramentas de Claude, que o modelo não pode chamar

Um fork repete a última requisição da conversa, então a Claude API serve a maior parte dela a partir do cache de prompt enquanto a conversa ainda estiver em cache.

Ambas as chamadas usam as credenciais da sessão, então são cobradas no plano, na chave de API ou no provedor de nuvem do usuário. Os tipos para seu build documentam todos os métodos de $.model.

Envie um prompt

Passe model e prompt para $.model.complete. prompt torna-se a mensagem do usuário. Para dar instruções ao modelo, como um papel ou um formato de saída, passe também system, que se torna o system prompt.

Este hook responde a um comando /triage, registrado como um comando, pedindo a um pequeno modelo para rotular o texto digitado após ele:

on('command.run', { command: 'triage' }, async ($, e) => {
  const r = await $.model.complete({
    model: 'haiku',
    // The system prompt sets the job, and the prompt carries the text to label
    system: 'Reply with one word: bug, feature, or question.',
    prompt: e.args,
    // One word needs few tokens, and the call gives up after 15 seconds
    maxTokens: 20,
    timeoutMs: 15000,
  })
  // r.text exists only when the model answered, so check r.isAnswered first
  const label = r.isAnswered ? r.text.trim() : 'unknown'
  return { text: 'Label: ' + label }
})

Quando você executa /triage the export button does nothing, o mod envia esse texto para o modelo e imprime sua resposta, como Label: bug. Quando o modelo não responde, o rótulo é unknown.

Uma falha da Claude API não rejeita a chamada, então verifique r.isAnswered e leia r.reason quando for false. A chamada rejeita para uma requisição que Claude Code não enviará, como um modelo que sua organização bloqueia.

Os tipos para seu build listam as outras opções, como effort, e os limites fornecem o padrão de maxTokens.

Use o cache de prompt

$.model.complete oferece suporte ao cache de prompt da Claude API. A API armazena em cache o início de uma requisição, chamado de prefixo, até um ponto de interrupção de cache que você define. Quando todas as chamadas começam com o mesmo conteúdo estático longo, como instruções ou material de referência, defina um ponto de interrupção no final desse conteúdo. As chamadas seguintes então o leem do cache em vez de pagar o preço integral de entrada por ele.

Para definir um ponto de interrupção, passe prompt como um array de blocos { text } em vez de uma string e adicione cache: true ao último bloco do conteúdo estático. Claude Code envia esse bloco com o campo cache_control da API. system aceita a mesma forma de array. Para decidir entre eles, consulte Escolha entre prompt e system.

Esta versão do hook /triage envia um longo conjunto de regras de rotulagem antes do texto a rotular, com um ponto de interrupção após as regras. RULES é uma string sua:

on('command.run', { command: 'triage' }, async ($, e) => {
  const r = await $.model.complete({
    model: 'haiku',
    prompt: [
      // Identical on every call, so it forms the cached prefix
      { text: RULES, cache: true },
      // Changes on every call, so it goes after the breakpoint
      { text: e.args },
    ],
  })
  return { text: 'Label: ' + (r.isAnswered ? r.text.trim() : 'unknown') }
})

O TTL e o número de pontos de interrupção têm estes limites:

  • TTL: uma entrada de cache dura cinco minutos após seu último uso. O TTL vem das configurações do Claude Code do usuário, não da chamada. Para uma hora, defina subagentPromptCacheTtl como 1h.
  • Pontos de interrupção por requisição: a API aceita até quatro, e um a mais retorna como um api-error em r.reason

Escolha entre `prompt` e `system`

Coloque o conteúdo estático que suas chamadas compartilham no início de prompt, a menos que você saiba que suas requisições vão diretamente para a Claude API:

  • Diretamente para a Claude API, com uma chave de API ou uma assinatura do Claude: qualquer um dos campos funciona
  • Por meio de Amazon Bedrock, Claude Platform on AWS, Agent Platform do Google Cloud, Microsoft Foundry ou um gateway de LLM: use prompt. Claude Code inicia o system prompt com um bloco de atribuição cuja impressão digital vem do início da mensagem do usuário. O endpoint api.anthropic.com remove esse bloco antes do cache. Outros endpoints o recebem como parte do prompt, então um ponto de interrupção em system pode falhar quando prompt começa de forma diferente.
  • Em um mod que outras pessoas executam: use prompt, porque você não escolhe o provedor delas

system vem antes de prompt no prefixo, então um ponto de interrupção em prompt também cobre system, e uma chamada com um system diferente não encontra o cache.

Verifique acertos de cache

O resultado de $.model.complete tem um objeto usage com os campos de cache da API. usage.cache_creation_input_tokens conta os tokens que a chamada gravou no cache, e usage.cache_read_input_tokens conta os tokens que ela leu do cache. Espere uma gravação na primeira chamada e leituras nas chamadas seguintes dentro do TTL.

Se todas as chamadas gravam e nenhuma lê, o prefixo difere entre as chamadas ou as chamadas estão mais espaçadas do que o TTL. Para um prefixo que difere, consulte Escolha entre prompt e system.

Se ambos os campos permanecem em zero em chamadas que o modelo respondeu, nada foi armazenado em cache. Verifique cada uma destas causas:

O que um hook `model.complete` recebe

Se você usar um hook no evento model.complete para inspecionar ou alterar as requisições de outros mods, leia o texto destes campos:

  • e.prompt: sempre uma string. Quando o chamador passou um array, é o texto dos blocos concatenado em ordem.
  • e.system: uma string construída da mesma forma, ou ausente quando o chamador não passou system
  • e.promptBlocks e e.systemBlocks: os arrays do chamador, cada um presente quando o chamador passou um array para aquele campo

Claude Code envia as strings que seu hook passa para next e usa os arrays que você passa junto com elas para posicionar os pontos de interrupção de cache. Ele mantém os blocos iniciais que ainda correspondem ao início da string, com seus pontos de interrupção, e envia o restante da string sem ponto de interrupção. Por exemplo, next({ ...e, prompt: e.prompt + NOTE }) mantém os pontos de interrupção do chamador, e um hook que altera o início de prompt os remove.

Execute trabalho em segundo plano

Trabalho que sobrevive a um evento, como verificar algo uma vez por minuto, é executado em um temporizador que você inicia a partir de session.start. Um hook em si é executado para um evento e tem um limite de tempo para seu próprio tempo de execução. O tempo gasto aguardando next ou uma chamada da mods API não conta, exceto um $.clock.sleep. $.clock.every e $.clock.after substituem setInterval e setTimeout, com o atraso em milissegundos primeiro: $.clock.after(5000, fn) chama fn uma vez, cinco segundos a partir de agora. Cada um retorna um temporizador com um método cancel(), e await $.clock.now() fornece a hora em milissegundos.

Este hook procura as verificações de uma solicitação de pull uma vez por minuto e mostra o resultado sob o prompt. summarize é uma função sua que transforma a saída JSON do comando em algumas palavras:

on('session.start', async ($, e, next) => {
  // Call the function every 60,000 milliseconds, starting one minute from now
  $.clock.every(60_000, async () => {
    const status = await $.process.run(['gh', 'pr', 'checks', '--json', 'state'])
    // Replace the line under the prompt with the latest summary
    $.ui.status('checks: ' + summarize(status.stdout))
  })
  // Return without waiting for the timer, so the session starts right away
  return next(e)
})

A sessão inicia como de costume. Um minuto depois, uma linha aparece sob o prompt com um ⚠, o nome do mod e depois checks: e seu resumo. É substituído uma vez por minuto depois disso. O callback do temporizador é executado fora de qualquer evento, então continua funcionando entre turnos e não inicia um. Se o callback lançar uma exceção, o erro vai para o debug log e o temporizador é executado novamente no próximo intervalo.

Mostre algo sem iniciar um turno

Um trabalho em segundo plano pode mostrar ao usuário algo sem iniciar um turno. Cada uma dessas chamadas coloca texto em um lugar diferente:

Chamada O que o usuário vê
$.ui.status(text) Uma linha sob o prompt que permanece até você alterá-la. Começa com ⚠ e o nome do mod, como em ⚠ my-mod: checks: 3 passing.
$.ui.toast(text) Uma notificação toast com o nome do mod que desaparece após alguns segundos. É uma caixa no canto superior direito na renderização em tela cheia e uma linha à direita sob o prompt no renderizador clássico.
$.ui.log(text) Uma linha fraca na transcrição que Claude não lê. Começa com ● e o nome do mod, como em ● my-mod: build finished.

Inicie um turno a partir de um trabalho em segundo plano

Quando um trabalho em segundo plano encontra algo que precisa da atenção de Claude, ele pode iniciar um turno enviando um prompt com $.prompt.submit({ text }). Claude lê o texto após uma frase que nomeia seu mod como o remetente. Para enviá-lo como as próprias palavras do usuário, sem essa frase, adicione asUser: true. A chamada aguarda até que a sessão esteja ociosa e depois inicia um novo turno. Ela resolve quando esse turno inicia, então não await em um manipulador que é executado enquanto Claude está trabalhando.

Pare o trabalho em segundo plano

Os temporizadores param quando o módulo é recarregado. Para trabalho de longa duração dentro de um hook, next.signal é um AbortSignal que aborta quando o evento que seu hook está manipulando é abandonado, por exemplo quando o usuário interrompe, então passe-o para qualquer coisa de longa duração.

Envie e receba mensagens entre sessões

Um mod pode enviar uma mensagem em texto simples para outra de suas sessões, para um dos subagentes desta sessão ou para um colega de equipe em sua equipe de agentes. Ele também pode observar as mensagens que chegam e saem.

Para enviar uma, chame $.session.send({ to, text }), que faz a mesma entrega que a ferramenta SendMessage faz. Defina to de acordo com quem recebe a mensagem:

  • Outra de suas sessões: { sessionId }
  • Um subagente ou colega de equipe: { agentId }, com um id de $.agent.list()
  • O remetente de uma mensagem que você recebeu: o endereço de string de onde essa mensagem veio

A chamada resolve uma vez que a mensagem é enfileirada, com { isDelivered: true }. Quando nada foi entregue, ela resolve com { isDelivered: false, reason }, e reason diz por quê.

Este hook responde a um comando /ping, registrado como um comando, pedindo à sessão cujo id você digita após ele um status:

on('command.run', { command: 'ping' }, async ($, e) => {
  // e.args is the session id typed after /ping
  const sent = await $.session.send({ to: { sessionId: e.args }, text: 'Status? One line.' })
  // The call resolves either way, so check isDelivered to learn what happened
  if (!sent.isDelivered) $.ui.toast('Not delivered: ' + sent.reason)
  // An empty result prints nothing in this session's transcript
  return {}
})

Quando a mensagem é enfileirada, nada aparece em sua sessão e o Claude da outra sessão lê Status? One line. Quando nada foi entregue, uma notificação toast fornece o motivo.

session.receive e session.send permitem que um mod observe as mensagens. Retorne next(e) de ambos para passar cada mensagem inalterada:

Evento Dispara quando Campos úteis
session.receive Uma mensagem chega para esta sessão, antes de Claude lê-la e.text e e.origin.kind, como peer ou peer-send-message para outra sessão ou agente, task-notification ou scheduled-trigger. Retorne { consumed: reason } para mantê-la longe de Claude.
session.send Uma mensagem está prestes a sair, da ferramenta SendMessage ou de um mod e.to, e.text e e.origin.kind, que é model ou plugin

Uma sessão definida para recusar mensagens de entrada recusa uma mensagem antes de session.receive disparar, então um hook nunca a vê. Uma mensagem que é mantida para sua aprovação chega ao hook primeiro, então um mod pode ler uma mensagem que você ainda não aprovou. O next(e) do hook rejeita quando a mensagem não é entregue.

O nome do remetente em uma mensagem recebida é o que o remetente escreveu, então não baseie uma decisão nele.

Acesse arquivos, processos e a rede

Um mod acessa o sistema de arquivos, processos e a rede através da mods API, com as mesmas permissões do usuário executando Claude Code. O próprio módulo de hooks não tem APIs Node.js, nenhum global de temporizador como setTimeout e nenhum acesso à rede ou arquivo próprio. APIs JavaScript padrão e web como URL, TextEncoder, AbortController e crypto.subtle estão disponíveis. Cada namespace abaixo cobre um tipo de acesso:

Namespace O que faz
$.fs read(path), write(path, text), exists(path), stat(path) e list(path) funcionam em arquivos e diretórios
$.process run(['git', 'status']) inicia um comando e resolve quando ele sai. spawn transmite a saída de um comando de longa duração.
$.http fetch(url, init) sobre http ou https. Ele resolve para { status, ok, headers, text } uma vez que o corpo é lido.
$.store Um armazenamento de chave-valor JSON do seu próprio plugin, mantido entre sessões
$.env get e set variáveis de ambiente. Escreva o nome como uma string literal.
$.settings read o que os arquivos de configurações e a política gerenciada contêm
$.session messages() retorna a transcrição como uma lista de { role, text, toolUses }. Também o diretório de trabalho, modelo e mais. usage() retorna o uso da janela de contexto e limites de plano.
$.mcp call uma ferramenta em um servidor MCP conectado

Arquivos e processos têm algumas regras próprias:

  • Paths: um caminho relativo é resolvido em relação ao diretório de trabalho da sessão
  • $.fs.list: retorna as entradas de um diretório como { name, kind, size, isLink } e não é recursivo
  • $.process.run: leva uma lista de argumentos e não usa shell. Ele resolve para { exitCode, stdout, stderr } qualquer que seja o código de saída. Ele rejeita se o programa não puder iniciar ou ainda estiver em execução no timeout, que é 30 segundos por padrão, então envolva em try e catch.

Cada uma dessas chamadas é em si um evento, nomeado para seu namespace e método sem o $., como fs.read para $.fs.read. Um mod anterior na cadeia pode observar, reescrever ou recusar sua chamada, que é como uma organização restringe o que os mods alcançam.

Um mod pode recusar sua chamada $.process.spawn depois que o comando produziu saída ou saiu, e nada do que o comando fez é desfeito. A chamada então é rejeitada com uma mensagem que termina com uma destas strings e o motivo do mod que recusou:

  • $.process.spawn started, and a plugin withheld its result:: o mod que recusou não havia lido a saída do comando até o fim. Claude Code interrompe o comando se ele ainda estiver em execução.
  • $.process.spawn ran, and a plugin withheld its result:: o mod que recusou havia lido a saída do comando até o fim, então o comando já havia saído

Próximas etapas