Die Mods-API verwenden
Rufen Sie die Mods-API aus einem Claude Code Mod auf, um Befehle und Tools hinzuzufügen, ein Modell aufzurufen, Arbeit per Timer auszuführen, Nachrichten an andere Sitzungen zu senden und auf Dateien und das Netzwerk zuzugreifen.
Die Mods-API ist die Menge der Methoden, die ein Mod aufruft, um zu handeln: Befehle und Tools hinzufügen, ein Modell aufrufen, Arbeit zwischen Events ausführen und auf das Dateisystem, Prozesse und das Netzwerk zugreifen. Jeder Hook erhält sie als erstes Argument, $, wobei die Methoden in Namespaces wie $.ui und $.fs gruppiert sind. Events entscheiden, wann ein Hook ausgeführt wird, und die Mods-API ist das, was der Hook aufruft, sobald er ausgeführt wird.
Erstellen Sie Ihren ersten Mod, bevor Sie hier beginnen. Informationen zu allen Methoden finden Sie unter Methoden der Mods-API, oder lesen Sie die Typen für Ihren Build.
Einen Befehl oder ein Tool hinzufügen
Ein Mod kann einen Befehl hinzufügen, den der Benutzer ausführt, und ein Tool, das Claude aufruft. Registrieren Sie beides in einem session.start-Hook. Claude Code wartet vor dem ersten Prompt auf diesen Hook, sodass das, was Sie registrieren, ab dem ersten Turn verfügbar ist.
Einen Befehl hinzufügen
Ein Befehl ist für den Benutzer gedacht. Registrieren Sie ihn und behandeln Sie dann command.run für seinen Namen. Dieses Beispiel fügt einen Befehl /standup hinzu, der eine optionale Anzahl von Tagen entgegennimmt:
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): ...' }
})
Nach dem Start der Sitzung erscheint /standup mit seiner Beschreibung in der Liste, die Sie sehen, wenn Sie / eingeben. Der argumentHint wird im Eingabefeld angezeigt, nachdem Sie den Befehl und ein Leerzeichen eingegeben haben, etwa /standup [days]. Wenn Sie /standup 3 ausführen, gibt der zweite Hook Summary for the last 3 day(s): ... zurück, und das Transkript zeigt diesen Text nach dem Namen des Plugins an. Der Hook ruft next nie auf, da der Befehl kein anderes Verhalten als Ihres hat.
Der von Ihnen zurückgegebene text wird im Transkript ausgegeben, und Claude liest ihn. Um nichts auszugeben, wie bei einem Befehl, der nur ein Pane öffnet, geben Sie {} zurück. Damit der Befehl ausgeführt werden kann, während Claude arbeitet, fügen Sie der Registrierung immediate: true hinzu.
Wählen Sie einen Namen, den kein integrierter Befehl verwendet. Geben Sie in einer Sitzung / ein, um diese zu sehen. $.command.register löst bei einem bereits vergebenen Namen einen Fehler aus, mit einer Meldung wie "/focus" refused: it is the built-in /focus. Ein Hook, der einen Fehler auslöst, wird übersprungen, sodass auch der Rest Ihres session.start-Hooks nicht ausgeführt wird. Registrieren Sie Befehle zuletzt in diesem Hook, oder umschließen Sie den Aufruf mit try und catch.
Ein Tool hinzufügen
Ein Tool ist für Claude gedacht. Registrieren Sie es mit einem Namen, einer Beschreibung, die Claude liest, und einem JSON-Schema für seine Eingabe. Claude sieht es unter einem längeren Namen, der sich aus mcp__, dem Namen Ihres Plugins, zwei Unterstrichen und dem registrierten Namen zusammensetzt. Seine Aufrufe behandeln Sie in einem tool.call-Hook, der auf diesen vollständigen Namen gefiltert ist. Dieses Beispiel aus einem Plugin namens my-mod registriert ticket, der vollständige Name lautet also mcp__my-mod__ticket. Es gibt Claude ein Tool, das ein Ticket in einem Issue-Tracker nachschlägt:
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 }
})
Wenn Sie nach einem Ticket fragen, kann Claude mcp__my-mod__ticket mit dessen ID aufrufen. Der zweite Hook ruft das Ticket ab und gibt den Response-Body zurück, den Claude als Ergebnis des Tools liest. Wenn der Server mit einem Fehlerstatus antwortet, liest Claude Lookup failed with status und die Nummer.
Ein Modell aufrufen
Ein Mod kann einem Modell eine eigene Frage stellen, außerhalb der Konversation, für eine kleine Aufgabe wie das Sortieren oder Zusammenfassen eines Textes. $.model.complete sendet einen Prompt mit den Anmeldedaten Ihrer Sitzung an ein Modell und wird mit der Antwort aufgelöst. Es gibt keinen Konversationsverlauf.
Dieser Hook beantwortet einen /triage-Befehl, der als Befehl registriert ist, indem er ein kleines Modell bittet, den danach eingegebenen Text zu kennzeichnen:
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 }
})
Wenn Sie /triage the export button does nothing ausführen, sendet der Mod diesen Text an das Modell und gibt dessen Antwort aus, etwa Label: bug. Die Konversation von Claude ist nicht Teil der Anfrage. Wenn das Modell nicht antwortet, lautet die Kennzeichnung unknown.
Ein Fehler der Claude API führt nicht zur Ablehnung des Aufrufs. Prüfen Sie daher r.isAnswered und lesen Sie r.reason, wenn der Wert false ist. Der Aufruf wird bei einer Anfrage abgelehnt, die Claude Code nicht sendet, etwa bei einem Modell, das Ihre Organisation blockiert. Die Typen für Ihren Build listen die weiteren Optionen auf, etwa effort, und die Limits nennen den Standardwert für maxTokens.
$.model.fork({ prompt }) stellt stattdessen eine Frage über die aktuelle Konversation, mit demselben Modell und demselben System-Prompt, sodass die Claude API den Großteil davon aus dem Prompt-Cache bedient.
Diese Aufrufe nutzen den Plan oder API-Schlüssel des Benutzers.
Arbeit im Hintergrund ausführen
Arbeit, die länger als ein einzelnes Event dauert, etwa eine Prüfung einmal pro Minute, läuft über einen Timer, den Sie in session.start starten. Ein Hook selbst läuft für ein Event und hat ein Zeitlimit für seine eigene Ausführungszeit. Zeit, die mit dem Warten auf next oder auf einen Aufruf der Mods-API verbracht wird, zählt nicht, mit Ausnahme von $.clock.sleep. $.clock.every und $.clock.after ersetzen setInterval und setTimeout, wobei die Verzögerung in Millisekunden zuerst angegeben wird: $.clock.after(5000, fn) ruft fn einmal auf, fünf Sekunden ab jetzt. Beide geben einen Timer mit einer cancel()-Methode zurück, und await $.clock.now() liefert die Zeit in Millisekunden.
Dieser Hook fragt einmal pro Minute die Checks eines Pull Requests ab und zeigt das Ergebnis unter dem Eingabefeld an. summarize ist eine eigene Funktion, die die JSON-Ausgabe des Befehls in wenige Worte umwandelt:
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)
})
Die Sitzung startet wie gewohnt. Eine Minute später erscheint unter dem Eingabefeld eine Zeile mit einem ⚠, dem Namen des Mods und anschließend checks: sowie Ihrer Zusammenfassung. Danach wird sie einmal pro Minute ersetzt. Der Callback des Timers läuft außerhalb jedes Events, daher läuft er zwischen Turns weiter und startet selbst keinen. Wenn der Callback einen Fehler auslöst, wird der Fehler in das Debug-Log geschrieben, und der Timer läuft beim nächsten Intervall erneut.
Etwas anzeigen, ohne einen Turn zu starten
Ein Hintergrundjob kann dem Benutzer etwas anzeigen, ohne einen Turn zu starten. Jeder dieser Aufrufe platziert Text an einer anderen Stelle:
| Aufruf | Was der Benutzer sieht |
|---|---|
$.ui.status(text) |
Eine Zeile unter dem Eingabefeld, die bestehen bleibt, bis Sie sie ändern. Sie beginnt mit ⚠ und dem Namen des Mods, wie in ⚠ my-mod: checks: 3 passing. |
$.ui.toast(text) |
Eine Toast-Benachrichtigung oben rechts, mit dem Namen des Mods über dem Text, die nach einigen Sekunden verschwindet |
$.ui.log(text) |
Eine abgeblendete Zeile im Transkript, die Claude nicht liest. Sie beginnt mit ● und dem Namen des Mods, wie in ● my-mod: build finished. |
Einen Turn aus einem Hintergrundjob starten
Wenn ein Hintergrundjob etwas findet, das Claudes Aufmerksamkeit erfordert, kann er einen Turn starten, indem er mit $.prompt.submit({ text }) einen Prompt übermittelt. Claude liest den Text nach einem Satz, der Ihren Mod als Absender nennt. Um ihn ohne diesen Satz als eigene Worte des Benutzers zu senden, fügen Sie asUser: true hinzu. Der Aufruf wartet, bis die Sitzung inaktiv ist, und startet dann einen neuen Turn. Er wird aufgelöst, wenn dieser Turn beginnt, verwenden Sie ihn daher nicht mit await in einem Handler, der läuft, während Claude arbeitet.
Hintergrundarbeit beenden
Timer werden beendet, wenn das Modul neu geladen wird. Für lang laufende Arbeit innerhalb eines Hooks ist next.signal ein AbortSignal, das abbricht, wenn das Event, das Ihr Hook verarbeitet, verworfen wird, zum Beispiel wenn der Benutzer unterbricht. Übergeben Sie es daher an alles, was lange läuft.
Nachrichten zwischen Sitzungen senden und empfangen
Ein Mod kann eine Nachricht im Klartext an eine andere Ihrer Sitzungen oder an einen der Subagenten dieser Sitzung senden und die eingehenden und ausgehenden Nachrichten beobachten. $.session.send({ to, text }) sendet eine Nachricht, mit derselben Zustellung wie das SendMessage-Tool. to ist { sessionId } für eine Sitzung, { agentId } für einen Subagenten aus $.agent.list() oder die String-Adresse, von der eine empfangene Nachricht stammt. Der Aufruf wird aufgelöst, sobald die Nachricht in die Warteschlange gestellt wurde, mit { isDelivered: true }. Wenn nichts zugestellt wurde, wird er mit { isDelivered: false, reason } aufgelöst, und reason gibt den Grund an.
Dieser Hook beantwortet einen /ping-Befehl, als Befehl registriert, indem er die Sitzung, deren ID Sie danach eingeben, nach einem Status fragt:
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 {}
})
Wenn die Nachricht in die Warteschlange gestellt wurde, erscheint nichts in Ihrer Sitzung, und Claude in der anderen Sitzung liest Status? One line. Wenn nichts zugestellt wurde, nennt eine Toast-Benachrichtigung den Grund.
Mit session.receive und session.send kann ein Mod die Nachrichten beobachten. Geben Sie bei beiden next(e) zurück, um jede Nachricht unverändert weiterzuleiten:
| Ereignis | Wird ausgelöst, wenn | Nützliche Felder |
|---|---|---|
session.receive |
Eine Nachricht für diese Sitzung eintrifft, bevor Claude sie liest | e.text und e.origin.kind, zum Beispiel peer oder peer-send-message für eine andere Sitzung oder einen anderen Agenten, task-notification oder scheduled-trigger. Geben Sie { consumed: reason } zurück, um sie von Claude fernzuhalten. |
session.send |
Eine Nachricht im Begriff ist, gesendet zu werden, vom SendMessage-Tool oder von einem Mod | e.to, e.text und e.origin.kind, das model oder plugin ist |
Eine Sitzung, die so eingestellt ist, dass sie eingehende Nachrichten ablehnt, lehnt eine Nachricht ab, bevor session.receive ausgelöst wird, sodass ein Hook sie nie sieht. Eine Nachricht, die auf Ihre Genehmigung wartet, erreicht den Hook zuerst, sodass ein Mod eine Nachricht lesen kann, die Sie noch nicht genehmigt haben. Das next(e) des Hooks wird abgelehnt, wenn die Nachricht nicht zugestellt wird.
Der Absendername einer empfangenen Nachricht ist das, was der Absender angegeben hat. Stützen Sie daher keine Entscheidung darauf.
Auf Dateien, Prozesse und das Netzwerk zugreifen
Ein Mod greift über die Mods-API auf das Dateisystem, Prozesse und das Netzwerk zu, und zwar mit denselben Berechtigungen wie der Benutzer, der Claude Code ausführt. Das Hooks-Modul selbst verfügt über keine Node.js-APIs, keine Timer-Globals wie setTimeout und keinen eigenen Netzwerk- oder Dateizugriff. Standard-JavaScript- und Web-APIs wie URL, TextEncoder, AbortController und crypto.subtle sind verfügbar. Jeder der folgenden Namespaces deckt eine Art von Zugriff ab:
| Namespace | Funktion |
|---|---|
$.fs |
read(path), write(path, text), exists(path), stat(path) und list(path) arbeiten mit Dateien und Verzeichnissen |
$.process |
run(['git', 'status']) startet einen Befehl und wird aufgelöst, wenn dieser beendet ist. spawn streamt die Ausgabe eines lang laufenden Befehls. |
$.http |
fetch(url, init) über http oder https. Wird zu { status, ok, headers, text } aufgelöst, sobald der Body gelesen wurde. |
$.store |
Ein eigener JSON-Key-Value-Speicher Ihres Plugins, der zwischen Sitzungen erhalten bleibt |
$.env |
Umgebungsvariablen mit get lesen und mit set setzen. Geben Sie den Namen als String-Literal an. |
$.settings |
Mit read auslesen, was die Einstellungsdateien und die verwaltete Richtlinie enthalten |
$.session |
messages() gibt das Transkript als Liste von { role, text, toolUses } zurück. Außerdem das Arbeitsverzeichnis, das Modell und mehr. usage() gibt die Nutzung des Kontextfensters und die Limits des Plans zurück. |
$.mcp |
Mit call ein Tool auf einem verbundenen MCP-Server aufrufen |
Für Dateien und Prozesse gelten einige eigene Regeln:
- Pfade: Ein relativer Pfad wird relativ zum Arbeitsverzeichnis der Sitzung aufgelöst
$.fs.list: Gibt die Einträge eines einzelnen Verzeichnisses als{ name, kind, size, isLink }zurück und arbeitet nicht rekursiv$.process.run: Nimmt eine Argumentliste entgegen und verwendet keine Shell. Wird unabhängig vom Exit-Code zu{ exitCode, stdout, stderr }aufgelöst. Wird abgelehnt, wenn das Programm nicht starten kann oder beim Timeout, standardmäßig 30 Sekunden, noch läuft. Umschließen Sie den Aufruf daher mittryundcatch.
Jeder dieser Aufrufe ist selbst ein Event, benannt nach seinem Namespace und seiner Methode ohne das $., etwa fs.read für $.fs.read. Ein Mod, der früher in der Kette steht, kann Ihren Aufruf beobachten, umschreiben oder ablehnen. Auf diese Weise schränkt eine Organisation ein, worauf Mods zugreifen können.
Nächste Schritte
- Auf Ereignisse reagieren: Tool-Aufrufe, Prompts und Turns abfangen
- In der Oberfläche zeichnen: anzeigen, was Ihr Mod in einem Bereich oder oberhalb des Prompts sammelt
- Einen Mod testen: beliebige dieser Aufrufe in einem Test durch Stubs ersetzen
- Mods-Referenz: Ereignisse, Methoden der Mods-API und Limits