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.
Quando o MCP Tool Search adia uma ferramenta registrada, Claude vê seu nome, mas não sua descrição, até procurá-la. Se Claude deve considerar a ferramenta em todos os turnos, adicione isDeferred: false ao registro para carregar a ferramenta completa antecipadamente. O campo requer Claude Code v2.1.293 ou posterior, e versões anteriores o ignoram.
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.
Arrays de blocos exigem Claude Code v2.1.292 ou posterior. Versões anteriores rejeitam um array em prompt com um erro que termina com takes { model, prompt } (host check) e deixam um array em system fora da requisição.
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
subagentPromptCacheTtlcomo1h. - Pontos de interrupção por requisição: a API aceita até quatro, e um a mais retorna como um
api-erroremr.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 endpointapi.anthropic.comremove esse bloco antes do cache. Outros endpoints o recebem como parte do prompt, então um ponto de interrupção emsystempode falhar quandopromptcomeç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 prefixo é curto demais: a API não armazena em cache um prefixo abaixo do comprimento mínimo do modelo e não retorna nenhum erro
- O cache de prompt está desativado: quando uma variável
DISABLE_PROMPT_CACHINGse aplica ao modelo, Claude Code remove os pontos de interrupção e envia o texto sem cache - Seu gateway remove
cache_control: um gateway pode remover o campo e ainda assim retornar sucesso - Outro mod reescreve o início do texto: Claude Code então o envia sem pontos de interrupção
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 passousysteme.promptBlocksee.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 emtryecatch.
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
- React to events: hook tool calls, prompts, and turns
- Draw in the interface: show what your mod collects in a pane or above the prompt
- Test a mod: stub any of these calls in a test
- Referência de mods: eventos, métodos da API de mods e limites