SpyBara
Go Premium

plugins/mods/api.md 2026-10-08 22:58 UTC to 2026-10-09 22: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

Usa l'API dei mod

Chiama l'API dei mod da un mod di Claude Code per aggiungere comandi e strumenti, chiamare un modello, eseguire lavoro a intervalli di tempo, inviare messaggi ad altre sessioni e accedere ai file e alla rete.

L'API dei mod è l'insieme di metodi che un mod chiama per agire: aggiungere comandi e strumenti, chiamare un modello, eseguire lavoro tra un evento e l'altro e accedere al file system, ai processi e alla rete. Ogni hook la riceve come primo argomento, $, con i metodi raggruppati in namespace come $.ui e $.fs. Gli eventi decidono quando viene eseguito un hook, e l'API dei mod è ciò che l'hook chiama una volta in esecuzione.

Crea il tuo primo mod prima di iniziare da qui. Per ogni metodo, consulta metodi dell'API dei mod oppure leggi i tipi per la tua build.

Aggiungere un comando o uno strumento

Un mod può aggiungere un comando che l'utente può eseguire e uno strumento che Claude può chiamare. Registra entrambi in un hook session.start. Claude Code attende quell'hook prima del primo prompt, quindi ciò che registri è disponibile fin dal primo turno.

Aggiungere un comando

Un comando è per l'utente. Registralo, poi gestisci command.run per il suo nome. Questo esempio aggiunge un comando /standup che accetta un numero di giorni facoltativo:

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): ...' }
})

Dopo l'avvio della sessione, /standup compare con la sua descrizione nell'elenco che vedi quando digiti /. L'argumentHint viene mostrato nel prompt dopo che hai digitato il comando e uno spazio, come in /standup [days]. Quando esegui /standup 3, il secondo hook restituisce Summary for the last 3 day(s): ... e la trascrizione mostra quel testo dopo il nome del plugin. L'hook non chiama mai next, perché il comando non ha altro comportamento oltre al tuo.

Il text che restituisci viene stampato nella trascrizione e Claude lo legge. Per non stampare nulla, come fa un comando che apre soltanto un pannello, restituisci {}. Per consentire l'esecuzione del comando mentre Claude sta lavorando, aggiungi immediate: true alla registrazione.

Scegli un nome che nessun comando integrato utilizza. Digita / in una sessione per vederli. $.command.register genera un errore per un nome già in uso, con un messaggio come "/focus" refused: it is the built-in /focus. Un hook che genera un errore viene saltato, quindi non viene eseguito nemmeno il resto del tuo hook session.start. Registra i comandi per ultimi in quell'hook, oppure racchiudi la chiamata in try e catch.

Aggiungere uno strumento

Uno strumento è per Claude. Registralo con un nome, una descrizione che Claude legge e uno JSON Schema per il suo input. Claude lo vede con un nome più lungo composto da mcp__, il nome del tuo plugin, due trattini bassi e il nome che hai registrato. Gestisci le sue chiamate in un hook tool.call filtrato su quel nome completo. Questo esempio, tratto da un plugin chiamato my-mod, registra ticket, quindi il nome completo è mcp__my-mod__ticket. Fornisce a Claude uno strumento che cerca un ticket in un issue tracker:

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 chiedi informazioni su un ticket, Claude può chiamare mcp__my-mod__ticket con il suo id. Il secondo hook recupera il ticket e restituisce il corpo della risposta, che Claude legge come risultato dello strumento. Quando il server risponde con uno stato di errore, Claude legge Lookup failed with status e il numero.

Chiamare un modello

Un mod può inviare richieste proprie a un modello per un piccolo compito come classificare o riassumere un testo. $.model.complete invia il tuo prompt da solo, mentre $.model.fork({ prompt }) invia la conversazione corrente con il tuo prompt alla fine.

Questa tabella confronta il contenuto di ciascuna richiesta:

Nella richiesta $.model.complete $.model.fork
Modello Il model che passi Il modello della sessione
Prompt di sistema Un breve blocco di attribuzione, seguito dal tuo system se ne passi uno Il prompt di sistema della sessione
Messaggi Un solo messaggio utente, il tuo prompt La conversazione fino a quel momento, seguita dal tuo prompt come messaggio utente
CLAUDE.md e altro contesto del progetto Non incluso Incluso, come nell'ultima richiesta della conversazione
Strumenti Nessuno Gli strumenti di Claude, che il modello non può chiamare

Un fork ripete l'ultima richiesta della conversazione, così la Claude API ne serve la maggior parte dalla cache dei prompt finché la conversazione è ancora nella cache.

Entrambe le chiamate usano le credenziali della sessione, quindi vengono addebitate al piano, alla chiave API o al provider cloud dell'utente. I tipi per la tua build documentano ogni metodo di $.model.

Inviare un singolo prompt

Passa model e prompt a $.model.complete. prompt diventa il messaggio utente. Per dare istruzioni al modello, come un ruolo o un formato di output, passa anche system, che diventa il prompt di sistema.

Questo hook risponde a un comando /triage, registrato come comando, chiedendo a un modello piccolo di etichettare il testo digitato dopo di esso:

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 esegui /triage the export button does nothing, il mod invia quel testo al modello e stampa la sua risposta, ad esempio Label: bug. Quando il modello non risponde, l'etichetta è unknown.

Un errore della Claude API non fa rifiutare la chiamata, quindi controlla r.isAnswered e leggi r.reason quando è false. La chiamata viene rifiutata per una richiesta che Claude Code non invierebbe, ad esempio un modello bloccato dalla tua organizzazione.

I tipi per la tua build elencano le altre opzioni, come effort, e i limiti indicano il valore predefinito di maxTokens.

Usare il prompt caching

$.model.complete supporta il prompt caching della Claude API. L'API memorizza nella cache l'inizio di una richiesta, chiamato prefisso, fino a un punto di interruzione della cache che imposti tu. Quando ogni chiamata inizia con lo stesso contenuto statico lungo, come istruzioni o materiale di riferimento, imposta un punto di interruzione alla fine di quel contenuto. Le chiamate successive lo leggono quindi dalla cache invece di pagarne il prezzo pieno di input.

Per impostare un punto di interruzione, passa prompt come array di blocchi { text } invece che come stringa e aggiungi cache: true all'ultimo blocco del contenuto statico. Claude Code invia quel blocco con il campo cache_control dell'API. system accetta la stessa forma ad array. Per scegliere tra i due, consulta Scegliere tra prompt e system.

Questa versione dell'hook /triage invia un lungo insieme di regole di etichettatura prima del testo da etichettare, con un punto di interruzione dopo le regole. RULES è una tua stringa:

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

Il TTL e il numero di punti di interruzione hanno questi limiti:

  • TTL: una voce della cache dura cinque minuti dopo l'ultimo utilizzo. Il TTL deriva dalle impostazioni di Claude Code dell'utente, non dalla chiamata. Per un'ora, imposta subagentPromptCacheTtl su 1h.
  • Punti di interruzione per richiesta: l'API ne accetta fino a quattro, e uno in più viene restituito come api-error in r.reason

Scegliere tra `prompt` e `system`

Metti il contenuto statico condiviso dalle tue chiamate all'inizio di prompt, a meno che tu non sappia che le tue richieste vanno direttamente alla Claude API:

  • Direttamente alla Claude API, con una chiave API o un abbonamento Claude: funzionano entrambi i campi
  • Tramite Amazon Bedrock, Claude Platform on AWS, Agent Platform di Google Cloud, Microsoft Foundry o un gateway LLM: usa prompt. Claude Code inizia il prompt di sistema con un blocco di attribuzione la cui impronta deriva dall'inizio del messaggio utente. L'endpoint api.anthropic.com rimuove quel blocco prima della memorizzazione nella cache. Gli altri endpoint lo ricevono come parte del prompt, quindi un punto di interruzione in system può non trovare corrispondenza nella cache quando prompt inizia in modo diverso.
  • In un mod eseguito da altre persone: usa prompt, perché non sei tu a scegliere il loro provider

system precede prompt nel prefisso, quindi un punto di interruzione in prompt copre anche system, e una chiamata con un system diverso non trova corrispondenza nella cache.

Verificare i riscontri nella cache

Il risultato di $.model.complete ha un oggetto usage con i campi della cache dell'API. usage.cache_creation_input_tokens conta i token che la chiamata ha scritto nella cache e usage.cache_read_input_tokens conta i token che ha letto dalla cache. Aspettati una scrittura alla prima chiamata e letture nelle chiamate successive entro il TTL.

Se ogni chiamata scrive e nessuna legge, il prefisso differisce tra le chiamate oppure le chiamate sono più distanti del TTL. Per un prefisso che differisce, consulta Scegliere tra prompt e system.

Se entrambi i campi restano a zero nelle chiamate a cui il modello ha risposto, non è stato memorizzato nulla nella cache. Verifica ciascuna di queste cause:

Cosa riceve un hook `model.complete`

Se agganci l'evento model.complete per ispezionare o modificare le richieste di altri mod, leggi il testo da questi campi:

  • e.prompt: sempre una stringa. Quando il chiamante ha passato un array, è il testo dei blocchi concatenato in ordine.
  • e.system: una stringa costruita allo stesso modo, oppure assente quando il chiamante non ha passato alcun system
  • e.promptBlocks ed e.systemBlocks: gli array del chiamante, ciascuno presente quando il chiamante ha passato un array per quel campo

Claude Code invia le stringhe che il tuo hook passa a next e usa gli array che passi insieme a esse per posizionare i punti di interruzione della cache. Mantiene i blocchi iniziali che corrispondono ancora all'inizio della stringa, con i relativi punti di interruzione, e invia il resto della stringa senza punti di interruzione. Ad esempio, next({ ...e, prompt: e.prompt + NOTE }) mantiene i punti di interruzione del chiamante, mentre un hook che modifica l'inizio di prompt li rimuove.

Eseguire lavoro in background

Il lavoro che dura più di un singolo evento, come controllare qualcosa una volta al minuto, viene eseguito su un timer che avvii da session.start. Un hook di per sé viene eseguito per un solo evento e ha un limite di tempo sul proprio tempo di esecuzione. Il tempo trascorso in attesa di next o di una chiamata all'API dei mod non viene conteggiato, tranne un $.clock.sleep. $.clock.every e $.clock.after prendono il posto di setInterval e setTimeout, con il ritardo in millisecondi come primo argomento: $.clock.after(5000, fn) chiama fn una volta, tra cinque secondi. Ciascuno restituisce un timer con un metodo cancel(), e await $.clock.now() restituisce l'ora in millisecondi.

Questo hook verifica i controlli di una pull request una volta al minuto e mostra il risultato sotto il prompt. summarize è una tua funzione che trasforma l'output JSON del comando in poche parole:

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

La sessione si avvia come al solito. Un minuto dopo, sotto il prompt compare una riga con un ⚠, il nome del mod, e poi checks: e il tuo riepilogo. Da quel momento viene sostituita una volta al minuto. La callback del timer viene eseguita al di fuori di qualsiasi evento, quindi continua a essere eseguita tra un turno e l'altro e non ne avvia uno. Se la callback genera un errore, l'errore finisce nel log di debug e il timer viene eseguito di nuovo all'intervallo successivo.

Mostrare qualcosa senza avviare un turno

Un job in background può mostrare qualcosa all'utente senza avviare un turno. Ciascuna di queste chiamate inserisce il testo in un punto diverso:

Chiamata Cosa vede l'utente
$.ui.status(text) Una riga sotto il prompt che rimane finché non la modifichi. Inizia con ⚠ e il nome del mod, come in ⚠ my-mod: checks: 3 passing.
$.ui.toast(text) Una notifica toast con il nome del mod che scompare dopo qualche secondo. È un riquadro in alto a destra nel rendering a schermo intero, e una riga a destra sotto il prompt nel renderer classico.
$.ui.log(text) Una riga attenuata nella trascrizione che Claude non legge. Inizia con ● e il nome del mod, come in ● my-mod: build finished.

Avviare un turno da un job in background

Quando un job in background trova qualcosa che richiede l'attenzione di Claude, può avviare un turno inviando un prompt con $.prompt.submit({ text }). Claude legge il testo dopo una frase che indica il tuo mod come mittente. Per inviarlo come parole dell'utente stesso, senza quella frase, aggiungi asUser: true. La chiamata attende che la sessione sia inattiva e poi avvia un nuovo turno. Si risolve quando quel turno inizia, quindi non usare await su di essa in un handler che viene eseguito mentre Claude sta lavorando.

Interrompere il lavoro in background

I timer si fermano quando il modulo viene ricaricato. Per il lavoro di lunga durata all'interno di un hook, next.signal è un AbortSignal che viene annullato quando l'evento gestito dal tuo hook viene abbandonato, ad esempio quando l'utente interrompe, quindi passalo a qualsiasi operazione di lunga durata.

Inviare e ricevere messaggi tra sessioni

Un mod può inviare un messaggio di testo semplice a un'altra delle tue sessioni, a uno dei subagent di questa sessione o a un membro del suo team di agenti. Può anche osservare i messaggi che arrivano e partono.

Per inviarne uno, chiama $.session.send({ to, text }), che effettua la stessa consegna dello strumento SendMessage. Imposta to in base a chi riceve il messaggio:

  • Un'altra delle tue sessioni: { sessionId }
  • Un subagent o un membro del team: { agentId }, con un id ottenuto da $.agent.list()
  • Il mittente di un messaggio che hai ricevuto: l'indirizzo stringa da cui proviene quel messaggio

La chiamata si risolve non appena il messaggio viene messo in coda, con { isDelivered: true }. Quando non è stato consegnato nulla, si risolve con { isDelivered: false, reason }, e reason ne indica il motivo.

Questo hook risponde a un comando /ping, registrato come comando, chiedendo uno stato alla sessione il cui id digiti dopo di esso:

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 il messaggio viene messo in coda, nella tua sessione non appare nulla, e Claude nell'altra sessione legge Status? One line. Quando non è stato consegnato nulla, una notifica toast ne indica il motivo.

session.receive e session.send permettono a un mod di osservare i messaggi. Restituisci next(e) da entrambi per far passare ogni messaggio senza modifiche:

Evento Si attiva quando Campi utili
session.receive Un messaggio arriva per questa sessione, prima che Claude lo legga e.text ed e.origin.kind, ad esempio peer o peer-send-message per un'altra sessione o un altro agente, task-notification o scheduled-trigger. Restituisci { consumed: reason } per non farlo arrivare a Claude.
session.send Un messaggio sta per partire, dallo strumento SendMessage o da un mod e.to, e.text ed e.origin.kind, che è model o plugin

Una sessione impostata per rifiutare i messaggi in entrata rifiuta un messaggio prima che session.receive si attivi, quindi un hook non lo vede mai. Un messaggio trattenuto in attesa della tua approvazione raggiunge prima l'hook, quindi un mod può leggere un messaggio che non hai ancora approvato. Il next(e) dell'hook viene rifiutato quando il messaggio non viene consegnato.

Il nome del mittente su un messaggio ricevuto è quello che il mittente ha scritto, quindi non basare una decisione su di esso.

Accedere a file, processi e rete

Un mod accede al file system, ai processi e alla rete tramite la mods API, con gli stessi permessi dell'utente che esegue Claude Code. Il modulo degli hook in sé non dispone di API Node.js, di globali per i timer come setTimeout, né di un proprio accesso alla rete o ai file. Sono disponibili le API standard di JavaScript e del web come URL, TextEncoder, AbortController e crypto.subtle. Ciascun namespace qui sotto copre un tipo di accesso:

Namespace Cosa fa
$.fs read(path), write(path, text), exists(path), stat(path) e list(path) operano su file e directory
$.process run(['git', 'status']) avvia un comando e si risolve quando termina. spawn trasmette in streaming l'output di un comando di lunga durata.
$.http fetch(url, init) su http o https. Si risolve in { status, ok, headers, text } una volta letto il body.
$.store Un archivio chiave-valore JSON proprio del tuo plugin, mantenuto tra una sessione e l'altra
$.env get e set delle variabili d'ambiente. Scrivi il nome come stringa letterale.
$.settings read di ciò che contengono i file delle impostazioni e la policy gestita
$.session messages() restituisce la trascrizione come elenco di { role, text, toolUses }. Inoltre la directory di lavoro, il modello e altro. usage() restituisce l'uso della finestra di contesto e i limiti del piano.
$.mcp call di uno strumento su un server MCP connesso

File e processi hanno alcune regole proprie:

  • Percorsi: un percorso relativo viene risolto rispetto alla directory di lavoro della sessione
  • $.fs.list: restituisce le voci di una singola directory come { name, kind, size, isLink } e non è ricorsivo
  • $.process.run: accetta un elenco di argomenti e non usa alcuna shell. Si risolve in { exitCode, stdout, stderr } qualunque sia il codice di uscita. Viene rifiutato se il programma non riesce ad avviarsi o è ancora in esecuzione allo scadere del timeout, che per impostazione predefinita è di 30 secondi, quindi racchiudilo in try e catch.

Ognuna di queste chiamate è a sua volta un evento, denominato in base al suo namespace e al suo metodo senza il $., come fs.read per $.fs.read. Un mod precedente nella catena può osservare, riscrivere o rifiutare la tua chiamata, ed è così che un'organizzazione limita ciò a cui i mod possono accedere.

Un mod può rifiutare la tua chiamata $.process.spawn dopo che il comando ha prodotto output o è terminato, e nulla di ciò che il comando ha fatto viene annullato. La chiamata viene quindi rifiutata con un messaggio che termina con una di queste stringhe seguita dal motivo indicato dal mod che ha rifiutato:

  • $.process.spawn started, and a plugin withheld its result:: il mod che ha rifiutato non aveva letto l'output del comando fino alla fine. Claude Code arresta il comando se è ancora in esecuzione.
  • $.process.spawn ran, and a plugin withheld its result:: il mod che ha rifiutato aveva letto l'output del comando fino alla fine, quindi il comando era già terminato

Passaggi successivi