SpyBara
Go Premium

plugins/mods/api.md 2026-10-01 23:59 UTC to 2026-10-02 19:58 UTC

This page contains 59 additions and 59 deletions.

2026
Thu 1 23:59 Fri 2 19:58

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ò porre a un modello una domanda propria, al di fuori della conversazione, per un piccolo compito come classificare o riassumere un testo. $.model.complete invia un prompt a un modello con le credenziali della tua sessione e si risolve con la risposta. Non ha cronologia della conversazione.

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. La conversazione di Claude non fa parte della richiesta. 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.

$.model.fork({ prompt }) pone invece una domanda sulla conversazione corrente, con lo stesso modello e lo stesso prompt di sistema, così la Claude API ne serve la maggior parte dalla cache dei prompt.

Queste chiamate usano il piano o la chiave API dell'utente.

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 in alto a destra, con il nome del mod sopra il testo, che scompare dopo qualche secondo
$.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 o a uno dei subagent di questa sessione, e osservare i messaggi che arrivano e partono. $.session.send({ to, text }) ne invia uno, con la stessa consegna effettuata dallo strumento SendMessage. to è { sessionId } per una sessione, { agentId } per un subagent ottenuto da $.agent.list(), oppure l'indirizzo stringa da cui proviene un messaggio ricevuto. 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.

Passaggi successivi