Usa l'API mods
Chiama l'API mods da un mod Claude Code per aggiungere comandi e strumenti, chiamare un modello, eseguire lavoro su un timer, inviare messaggi ad altre sessioni e accedere a file e rete.
L'API mods è l'insieme di metodi che un mod chiama per agire: aggiungere comandi e strumenti, chiamare un modello, eseguire lavoro tra gli eventi 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. Events decidono quando un hook viene eseguito, e l'API mods è ciò che l'hook chiama una volta che lo fa.
Costruisci il tuo primo mod prima di iniziare qui. Per ogni metodo, vedi mods API methods o leggi i tipi per la tua build.
Aggiungi un comando o uno strumento
Un mod può aggiungere un comando per l'utente da eseguire e uno strumento per Claude da chiamare. Registra entrambi in un hook session.start. Claude Code attende quell'hook prima del primo prompt, quindi ciò che registri è disponibile dal primo turno.
Aggiungi un comando
Un comando è per l'utente. Registralo, quindi gestisci command.run per il suo nome. Questo esempio aggiunge un comando /standup che accetta un numero facoltativo di giorni:
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 appare con la sua descrizione nell'elenco che vedi quando digiti /. L'argumentHint viene visualizzato nel prompt dopo che digiti 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 comportamento diverso dal tuo.
Il text che restituisci viene stampato nella trascrizione e Claude lo legge. Per non stampare nulla, come un comando che apre solo un pane, restituisci {}. Per consentire al comando di essere eseguito 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'eccezione per un nome occupato, con un messaggio come "/focus" refused: it is the built-in /focus". Un hook che genera un'eccezione viene saltato, quindi il resto del tuo hook session.start non viene eseguito nemmeno. Registra i comandi per ultimi in quell'hook, oppure avvolgi la chiamata in try e catch.
Aggiungi 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 underscore e il nome che hai registrato. Gestisci le sue chiamate in un hook tool.call filtrato a quel nome completo. Questo esempio, da un plugin denominato 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.
Chiama un modello
Un mod può fare una domanda a un modello di sua iniziativa, al di fuori della conversazione, per un piccolo lavoro come ordinare o riassumere un pezzo di testo. $.model.complete invia un prompt a un modello con le credenziali della tua sessione e si risolve nella risposta. Non ha cronologia della conversazione.
Questo hook risponde a un comando /triage, registrato come comando, chiedendo a un piccolo modello 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, come Label: bug. La conversazione di Claude non fa parte della richiesta. Quando il modello non risponde, l'etichetta è unknown.
Un errore dell'API Claude non rifiuta la chiamata, quindi controlla r.isAnswered e leggi r.reason quando è false. La chiamata rifiuta solo per una richiesta che Claude Code non invierà, come un modello che la tua organizzazione blocca. I tipi per la tua build elencano le altre opzioni, come effort, e i limiti forniscono il valore predefinito di maxTokens.
$.model.fork({ prompt }) pone una domanda sulla conversazione corrente, con lo stesso modello e prompt di sistema, quindi l'API Claude serve la maggior parte da prompt caching.
Queste chiamate utilizzano il piano o la chiave API dell'utente.
Esegui lavoro in background
Il lavoro che sopravvive a un evento, come controllare qualcosa una volta al minuto, viene eseguito su un timer che avvii da session.start. Un hook stesso viene eseguito per un evento e ha un limite di tempo di 10 secondi del suo tempo di esecuzione. Il tempo trascorso in attesa di next o di una chiamata all'API mods non conta, tranne un $.clock.sleep. $.clock.every e $.clock.after prendono il posto di setInterval e setTimeout, con il ritardo in millisecondi per primo: $.clock.after(5000, fn) chiama fn una volta, cinque secondi da ora. Ognuno restituisce un timer con un metodo cancel(), e await $.clock.now() fornisce l'ora in millisecondi.
Questo hook cerca i controlli di una pull request una volta al minuto e mostra il risultato sotto il prompt. summarize è una funzione tua 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 inizia come al solito. Un minuto dopo, una riga appare sotto il prompt con un ⚠, il nome del mod e poi checks: e il tuo riassunto. Viene sostituito una volta al minuto dopo. Il callback del timer viene eseguito al di fuori di qualsiasi evento, quindi continua a funzionare tra i turni e non ne avvia uno. Se il callback genera un'eccezione, l'errore va al debug log e il timer viene eseguito di nuovo all'intervallo successivo.
Mostra qualcosa senza avviare un turno
Un lavoro in background può mostrare all'utente qualcosa senza avviare un turno. Ognuna di queste chiamate mette il testo in un posto diverso:
| Chiamata | Cosa vede l'utente |
|---|---|
$.ui.status(text) |
Una riga sotto il prompt che rimane fino a quando non la cambi. Inizia con ⚠ e il nome del mod, come in ⚠ my-mod: checks: 3 passing. |
$.ui.toast(text) |
Una piccola casella in alto a destra, con il nome del mod sopra il testo, che scompare dopo pochi secondi |
$.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. |
Avvia un turno da un lavoro in background
Quando un lavoro in background trova qualcosa che ha bisogno dell'attenzione di Claude, può avviare un turno inviando un prompt con $.prompt.submit({ text }). Claude legge il testo dopo una frase che nomina il tuo mod come mittente. Per inviarlo come parole proprie dell'utente, senza quella frase, aggiungi asUser: true. La chiamata attende fino a quando la sessione è inattiva e quindi avvia un nuovo turno. Si risolve quando quel turno inizia, quindi non await in un handler che viene eseguito mentre Claude sta lavorando.
Interrompi il lavoro in background
Il lavoro in background si interrompe in due modi. I timer si interrompono quando il modulo viene ricaricato. Per il lavoro di lunga durata all'interno di un hook, next.signal è un AbortSignal che si interrompe quando l'evento che il tuo hook sta gestendo viene abbandonato, ad esempio quando l'utente interrompe, quindi passalo a qualsiasi cosa di lunga durata.
Invia e ricevi messaggi tra sessioni
Un mod può inviare un messaggio in 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, la stessa consegna che lo strumento SendMessage effettua. to è { sessionId } per una sessione, { agentId } per un subagent da $.agent.list(), o l'indirizzo stringa da cui proviene un messaggio ricevuto. La chiamata si risolve una volta che il messaggio è in coda, con { isDelivered: true }. Quando nulla è stato consegnato si risolve con { isDelivered: false, reason }, e reason spiega perché.
Questo hook risponde a un comando /ping, registrato come comando, chiedendo alla sessione il cui id digiti dopo di esso uno stato:
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 è in coda, nulla appare nella tua sessione e Claude dell'altra sessione legge Status? One line. Quando nulla è stato consegnato, una piccola casella in alto a destra fornisce il motivo e scompare dopo pochi secondi.
Due eventi consentono a un mod di osservare i messaggi. Restituisci next(e) da entrambi per passare ogni messaggio invariato:
| Evento | Si attiva quando | Campi utili |
|---|---|---|
session.receive |
Un messaggio arriva per questa sessione, prima che Claude lo legga | e.text e e.origin.kind, come peer o peer-send-message per un'altra sessione o agente, task-notification o scheduled-trigger. Restituisci { consumed: reason } per impedirlo a Claude. |
session.send |
Un messaggio sta per partire, dallo strumento SendMessage o da un mod | e.to, e.text e e.origin.kind, che è model o plugin |
Una sessione impostata per rifiutare messaggi in entrata rifiuta un messaggio prima che session.receive si attivi, quindi un hook non lo vede mai. Un messaggio che è in sospeso per la tua approvazione raggiunge prima l'hook, quindi un mod può leggere un messaggio che non hai ancora approvato. Il next(e) dell'hook rifiuta 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.
Accedi a file, processi e rete
Un mod accede al file system, ai processi e alla rete attraverso l'API mods, con le stesse autorizzazioni dell'utente che esegue Claude Code. Il modulo hooks stesso non ha API Node.js, nessun timer globale come setTimeout e nessun accesso di rete o file proprio. Le API JavaScript standard e web come URL, TextEncoder, AbortController e crypto.subtle sono disponibili. Ogni namespace di seguito copre un tipo di accesso:
| Namespace | Cosa fa |
|---|---|
$.fs |
read(path), write(path, text), exists(path), stat(path) e list(path) funzionano su file e directory |
$.process |
run(['git', 'status']) avvia un comando e si risolve quando esce. spawn trasmette 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 che il corpo è stato letto. |
$.store |
Un archivio chiave-valore JSON del tuo plugin, mantenuto tra le sessioni |
$.env |
get e set variabili di ambiente. Scrivi il nome come una stringa letterale. |
$.settings |
read cosa contengono i file di impostazioni e la politica gestita |
$.session |
messages() restituisce la trascrizione come un elenco di { role, text, toolUses }. Anche la directory di lavoro, il modello e altro. usage() restituisce l'uso della finestra di contesto e i limiti del piano. |
$.mcp |
call uno strumento su un server MCP connesso |
File e processi hanno poche regole proprie:
- Percorsi: un percorso relativo è sotto la directory di lavoro della sessione
$.fs.list: restituisce le voci di una directory come{ name, kind, size, isLink }e non scende nelle sottodirectory$.process.run: accetta un elenco di argomenti e non utilizza shell. Si risolve in{ exitCode, stdout, stderr }indipendentemente dal codice di uscita. Rifiuta se il programma non può avviarsi o è ancora in esecuzione al timeout, che è 30 secondi per impostazione predefinita, quindi avvolgilo intryecatch.
Ognuna di queste chiamate è essa stessa un evento, denominato per il suo namespace e metodo senza il $., come fs.read per $.fs.read. Un mod precedente nella catena può osservare, riscrivere o rifiutare la tua chiamata, che è come un'organizzazione limita ciò che i mod raggiungono.
Passaggi successivi
- Reagisci agli eventi: hook tool calls, prompts e turns
- Disegna nell'interfaccia: mostra ciò che il tuo mod raccoglie in un pane o sopra il prompt
- Testa un mod: stub qualsiasi di queste chiamate in un test
- Mods reference: ogni evento, ogni metodo dell'API mods e i limiti