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 intryecatch.
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
- Reagire agli eventi: intercetta con hook le chiamate agli strumenti, i prompt e i turni
- Disegnare nell'interfaccia: mostra ciò che il tuo mod raccoglie in un riquadro o sopra il prompt
- Testare un mod: crea uno stub per una qualsiasi di queste chiamate in un test
- Riferimento dei mod: eventi, metodi dell'API dei mod e limiti