SpyBara
Go Premium

plugins/mods/api.md 2026-11-10 21:57 UTC to 2026-11-11 22:59 UTC

No changes for this document in this history range.

2026
Mon 2 23:57

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.

[!TIP]

Wenn MCP Tool Search ein registriertes Tool zurückstellt, sieht Claude dessen Namen, aber nicht dessen Beschreibung, bis Claude danach sucht. Wenn Claude das Tool in jedem Turn berücksichtigen soll, fügen Sie der Registrierung isDeferred: false hinzu, um das vollständige Tool vorab zu laden. Das Feld erfordert Claude Code v2.1.293 oder höher, und frühere Versionen ignorieren es.

Ein Modell aufrufen

Ein Mod kann eigene Anfragen an ein Modell senden, um eine kleine Aufgabe wie das Klassifizieren oder Zusammenfassen eines Textes zu erledigen. $.model.complete sendet Ihren Prompt für sich allein, und $.model.fork({ prompt }) sendet die aktuelle Konversation mit Ihrem Prompt am Ende.

Diese Tabelle vergleicht, was jede Anfrage enthält:

In der Anfrage $.model.complete $.model.fork
Modell Das model, das Sie übergeben Das Modell der Sitzung
System-Prompt Ein kurzer Attributionsblock, danach Ihr system, falls Sie eines übergeben Der System-Prompt der Sitzung
Nachrichten Eine Benutzernachricht, Ihr prompt Die bisherige Konversation, danach Ihr prompt als Benutzernachricht
CLAUDE.md und anderer Projektkontext Nicht enthalten Enthalten, wie in der letzten Anfrage der Konversation
Tools Keine Die Tools von Claude, die das Modell nicht aufrufen kann

Ein Fork wiederholt die letzte Anfrage der Konversation, sodass die Claude API den Großteil davon aus dem Prompt-Cache bedient, solange die Konversation noch im Cache liegt.

Beide Aufrufe nutzen die Anmeldedaten der Sitzung, sodass sie über den Plan, den API-Schlüssel oder den Cloud-Anbieter des Benutzers abgerechnet werden. Die Typen für Ihren Build dokumentieren jede $.model-Methode.

Einen einzelnen Prompt senden

Übergeben Sie model und prompt an $.model.complete. prompt wird zur Benutzernachricht. Um dem Modell Anweisungen zu geben, etwa eine Rolle oder ein Ausgabeformat, übergeben Sie zusätzlich system, das zum System-Prompt wird.

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. 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.

Prompt-Caching verwenden

$.model.complete unterstützt das Prompt-Caching der Claude API. Die API speichert den Anfang einer Anfrage, das sogenannte Präfix, bis zu einem von Ihnen gesetzten Cache-Breakpoint im Cache. Wenn jeder Aufruf mit demselben langen statischen Inhalt beginnt, etwa Anweisungen oder Referenzmaterial, setzen Sie einen Breakpoint am Ende dieses Inhalts. Spätere Aufrufe lesen ihn dann aus dem Cache, statt dafür den vollen Eingabepreis zu zahlen.

Um einen Breakpoint zu setzen, übergeben Sie prompt als Array von { text }-Blöcken statt als String und fügen dem letzten Block des statischen Inhalts cache: true hinzu. Claude Code sendet diesen Block mit dem Feld cache_control der API. system akzeptiert dieselbe Array-Form. Wie Sie sich zwischen beiden entscheiden, erfahren Sie unter Zwischen prompt und system wählen.

[!NOTE]

Arrays von Blöcken erfordern Claude Code v2.1.292 oder höher. Frühere Versionen lehnen ein Array in prompt mit einem Fehler ab, der mit takes { model, prompt } (host check) endet, und lassen ein Array in system in der Anfrage weg.

Diese Version des /triage-Hooks sendet vor dem zu kennzeichnenden Text einen langen Satz von Kennzeichnungsregeln, mit einem Breakpoint nach den Regeln. RULES ist ein eigener String:

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

Für die TTL und die Anzahl der Breakpoints gelten diese Limits:

  • TTL: Ein Cache-Eintrag bleibt nach seiner letzten Verwendung fünf Minuten lang bestehen. Die TTL stammt aus den Claude Code-Einstellungen des Benutzers, nicht aus dem Aufruf. Für eine Stunde setzen Sie subagentPromptCacheTtl auf 1h.
  • Breakpoints pro Anfrage: Die API akzeptiert bis zu vier, und ein weiterer kommt als api-error in r.reason zurück

Zwischen prompt und system wählen

Platzieren Sie den statischen Inhalt, den Ihre Aufrufe gemeinsam haben, am Anfang von prompt, es sei denn, Sie wissen, dass Ihre Anfragen direkt an die Claude API gehen:

  • Direkt an die Claude API, mit einem API-Schlüssel oder einem Claude-Abonnement: Beide Felder funktionieren
  • Über Amazon Bedrock, Claude Platform on AWS, Google Cloud's Agent Platform, Microsoft Foundry oder ein LLM-Gateway: Verwenden Sie prompt. Claude Code beginnt den System-Prompt mit einem Attributionsblock, dessen Fingerabdruck aus dem Anfang der Benutzernachricht stammt. Der Endpunkt api.anthropic.com entfernt diesen Block vor dem Caching. Andere Endpunkte erhalten ihn als Teil des Prompts, sodass ein Breakpoint in system den Cache verfehlen kann, wenn prompt anders beginnt.
  • In einem Mod, den andere Personen ausführen: Verwenden Sie prompt, da Sie deren Anbieter nicht auswählen

system steht im Präfix vor prompt, sodass ein Breakpoint in prompt auch system abdeckt und ein Aufruf mit einem anderen system den Cache verfehlt.

Auf Cache-Treffer prüfen

Das Ergebnis von $.model.complete enthält ein usage-Objekt mit den Cache-Feldern der API. usage.cache_creation_input_tokens zählt die Token, die der Aufruf in den Cache geschrieben hat, und usage.cache_read_input_tokens zählt die Token, die er aus dem Cache gelesen hat. Erwarten Sie beim ersten Aufruf einen Schreibvorgang und bei späteren Aufrufen innerhalb der TTL Lesevorgänge.

Wenn jeder Aufruf schreibt und keiner liest, unterscheidet sich das Präfix zwischen den Aufrufen oder die Aufrufe liegen weiter auseinander als die TTL. Für ein abweichendes Präfix siehe Zwischen prompt und system wählen.

Wenn beide Felder bei Aufrufen, die das Modell beantwortet hat, auf null bleiben, wurde nichts zwischengespeichert. Prüfen Sie jede dieser Ursachen:

Was ein model.complete-Hook erhält

Wenn Sie sich in das Event model.complete einklinken, um die Anfragen anderer Mods zu prüfen oder zu ändern, lesen Sie den Text aus diesen Feldern:

  • e.prompt: immer ein String. Wenn der Aufrufer ein Array übergeben hat, ist es der Text der Blöcke, in Reihenfolge aneinandergehängt.
  • e.system: ein auf dieselbe Weise gebildeter String oder nicht vorhanden, wenn der Aufrufer kein system übergeben hat
  • e.promptBlocks und e.systemBlocks: die Arrays des Aufrufers, jeweils vorhanden, wenn der Aufrufer für dieses Feld ein Array übergeben hat

Claude Code sendet die Strings, die Ihr Hook an next übergibt, und verwendet die Arrays, die Sie mit ihnen übergeben, um Cache-Breakpoints zu platzieren. Es behält die führenden Blöcke, die noch mit dem Anfang des Strings übereinstimmen, samt ihren Breakpoints bei und sendet den Rest des Strings ohne Breakpoint. Beispielsweise behält next({ ...e, prompt: e.prompt + NOTE }) die Breakpoints des Aufrufers bei, während ein Hook, der den Anfang von prompt ändert, sie entfernt.

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 mit dem Namen des Mods, die nach einigen Sekunden verschwindet. Im Vollbild-Rendering ist sie ein Feld oben rechts, im klassischen Renderer eine Zeile rechts unter dem Eingabefeld.
$.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.
$.ui.notify(text) Eine Benachrichtigung seines Terminals, außerhalb von Claude Code, die so gesendet wird, wie es seine Einstellung preferredNotifChannel festlegt. Der Name des Mods ist ihr Titel, sofern Sie keinen nicht leeren { title } übergeben, der so angezeigt wird, wie Sie ihn angeben.

$.ui.notify ist der einzige dieser Aufrufe, der eine Antwort liefert. Er wird zu { isSent: true, channel } aufgelöst, wenn der Kanal des Benutzers die Benachrichtigung in sein Terminal geschrieben hat. channel ist der Wert der Einstellung, über den sie gesendet wurde, etwa kitty oder terminal_bell. Der Kanal terminal_bell lässt die Glocke ertönen und zeigt weder Ihren Text noch den Titel an.

Andernfalls wird der Aufruf zu { isSent: false, reason } aufgelöst:

reason Bedeutung
disabled Der Benutzer hat preferredNotifChannel auf notifications_disabled gesetzt
no-channel Die Einstellung ist auto, und sein Terminal bietet keine Benachrichtigung, die Claude Code senden kann
no-surface Die Sitzung hat kein Terminal, etwa bei einer -p-Ausführung, im Agent SDK, in der Desktop-App, in der VS Code-Erweiterung oder in einer Cloud-Sitzung. Ebenfalls die Antwort auf einen Aufruf, der vor dem Start der Sitzung erfolgt, während Ihr Mod geladen wird, sowie auf einen Aufruf, der erfolgt, während Claude Code beendet wird
refused Der Hook eines anderen Mods auf ui.notify hat anstelle von Claude Code geantwortet, ohne zu senden

Der Aufruf wird abgelehnt statt aufgelöst, wenn text kein String ist, wenn der Hook eines anderen Mods auf ui.notify { deny } zurückgibt oder wenn das Schreiben in das Terminal fehlschlägt.

Ihr Mod kann den Kanal nicht wählen, und Claude Code begrenzt nicht, wie oft ein Mod Benachrichtigungen sendet. Die Einstellung des Benutzers ist der einzige Schalter.

Wenn der Aufruf eine laufende Sitzung erreicht, werden die Notification-Hooks des Benutzers mit dem Typ plugin_notification ausgeführt. Sie laufen auch dann, wenn die Antwort disabled lautet, sowie in einer Sitzung ohne Terminal, in der die Antwort no-surface lautet. Für einen Aufruf, der vor dem Start der Sitzung oder während des Beendens von Claude Code erfolgt, läuft kein Hook. Der Hook, aus dem Ihr Mod $.ui.notify aufgerufen hat, wird bei diesem Durchlauf ausgelassen, sodass ein classic.Notification-Hook, der eine Benachrichtigung sendet, für seine eigene Benachrichtigung nicht erneut aufgerufen wird.

Ein Terminal meldet nicht, ob es die Benachrichtigung angezeigt hat, daher bedeutet isSent: true, dass sie gesendet wurde, nicht, dass der Benutzer sie gesehen hat.

Dieses Beispiel sendet eine Benachrichtigung, wenn der Turn der Hauptkonversation ohne Unterbrechung endet, und greift auf einen Toast zurück, wenn keine Benachrichtigung mit Text gesendet wurde. Es wartet nicht auf die Benachrichtigung, bevor es das Event weitergibt, da die Notification-Hooks des Benutzers zuerst laufen und eine Weile dauern können:

on('turn.complete', ($, e, next) => {
  const isMainTurn = e.agentId === undefined && !e.isAborted
  const fallBack = () => $.ui.toast('Turn finished')
  if (isMainTurn) {
    $.ui.notify('Turn finished').then(sent => {
      const hasText = sent.isSent && sent.channel !== 'terminal_bell'
      if (!hasText) fallBack()
    }, fallBack)
  }
  return next(e)
})

In der VS Code-Erweiterung werden Statuszeilen und Toasts stattdessen oberhalb des Eingabefelds angezeigt und Log-Zeilen in der Konversation.

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 nach dem Neuladen des Moduls beendet. Für lang laufende Arbeit innerhalb eines Hooks ist next.signal ein AbortSignal, das abgebrochen wird, wenn das Ereignis, das Ihr Hook verarbeitet, verworfen wird, zum Beispiel wenn der Benutzer unterbricht. Übergeben Sie es an alles, was lange läuft.

Aufrufe und Hintergrundarbeit, nachdem der Benutzer einen Turn unterbrochen hat

Der Grund the session was stopped and nobody has sent input since in einem Ergebnis oder Fehler bedeutet, dass der Benutzer einen laufenden Turn gestoppt hat, etwa mit Esc oder Stop. Nach dieser Unterbrechung werden Aufrufe, die neue Arbeit in der Sitzung starten, bis zur nächsten Eingabe des Benutzers abgelehnt, etwa einer Nachricht im Eingabefeld.

Abgesehen von $.prompt.submit und $.command.run erfordert diese Behandlung von Aufrufen und Hintergrundarbeit nach einer Unterbrechung Claude Code v2.1.304 oder höher.

Einen abgelehnten oder verworfenen Aufruf erneut versuchen

Führen Sie einen abgelehnten oder verworfenen Aufruf aus einem turn.complete-Hook erneut aus. Aufrufe bleiben bis zur nächsten Eingabe des Benutzers abgelehnt, und nichts stellt sie für Sie in eine Warteschlange oder wiederholt sie.

Wenn das Ergebnis oder der Fehler eines Aufrufs eine dieser Zeichenfolgen enthält, bewahren Sie seine Argumente für einen späteren Wiederholungsversuch auf:

  • the session was stopped and nobody has sent input since: Der Aufruf wurde abgelehnt
  • was removed from the queue before it ran: Der Prompt oder Befehl hat die Warteschlange verlassen, ohne ausgeführt zu werden, und wurde verworfen, zum Beispiel als der Benutzer den Turn gestoppt hat

Rufen Sie in diesem Beispiel submitPrompt aus Ihrem Hintergrundjob anstelle von $.prompt.submit auf. Platzieren Sie die Variable pendingText und die Funktion submitPrompt oberhalb von register und den turn.complete-Hook darin, da die Validierung $ ablehnt, wenn es an eine innerhalb von register deklarierte Funktion übergeben wird. Wenn das Ergebnis oder der Fehler eine dieser Zeichenfolgen enthält, bewahrt submitPrompt den Text in pendingText auf, damit der turn.complete-Hook ihn erneut übermittelt:

// The text of the last refused or dropped prompt, or null
let pendingText = null

async function submitPrompt($, text) {
  let reason
  try {
    const result = await $.prompt.submit({ text })
    reason = result?.drop ?? ''
  } catch (error) {
    reason = String(error?.message ?? error)
  }
  if (reason.includes('the session was stopped and nobody has sent input since') ||
      reason.includes('was removed from the queue before it ran')) {
    pendingText = text
  }
  // Any other result or error isn't retried
}

on('turn.complete', async ($, e, next) => {
  if (pendingText !== null) {
    const text = pendingText
    // Empty pendingText before retrying, so that a second run of this hook
    // doesn't submit the same text.
    pendingText = null
    // Not awaited: the hook would stay open until the new turn starts
    submitPrompt($, text)
  }
  return next(e)
})

Vor der nächsten Eingabe des Benutzers kann turn.complete für den unterbrochenen Turn selbst und für den Turn eines Subagenten ausgelöst werden, und dann getätigte Aufrufe werden erneut abgelehnt. In diesem Fall bewahrt submitPrompt den Text für ein späteres turn.complete auf.

Bei einem Aufruf, der mit einer Ablehnung (Reject) endet, etwa $.command.run, fangen Sie den Fehler ab und prüfen seine Nachricht so, wie submitPrompt reason prüft.

Was abgelehnte Aufrufe zurückgeben

In den Ergebnissen steht <reason> für den Grund, aus dem der Aufruf abgelehnt wurde. Nach einer Unterbrechung ist das the session was stopped and nobody has sent input since. Andere Gründe finden Sie unter Aufrufe, die in claude -p-Ausführungen und Agent SDK-Sitzungen abgelehnt werden.

Einige abgelehnte Aufrufe Ergebnis
$.prompt.submit Wird mit { drop: '<reason>' } aufgelöst und stellt keinen Prompt in die Warteschlange
$.command.run Wird mit einem Fehler abgelehnt, dessen Nachricht auf <reason> endet
$.agent.spawn Wird mit { deny: '<reason>' } aufgelöst und startet keinen Subagenten
$.session.send an einen Subagenten, der beendet ist Wird mit { isDelivered: false, reason: 'Agent "<id>" was not resumed: <reason>.' } aufgelöst, wobei <id> die ID des Subagenten ist. Ein Subagent, der noch läuft, empfängt die Nachricht.
$.session.send an ein Mitglied eines Agent-Teams Wird mit { isDelivered: false, reason: 'Teammate "<name>" was not messaged: <reason>.' } aufgelöst, wobei <name> der Name des Teammitglieds ist
$.tool.call von Bash mit run_in_background: true, Monitor oder CronCreate Wird mit { deny: '<tool> was not run: it starts background work, a schedule or a goal, and <reason>' } aufgelöst, wobei <tool> der Name des Tools ist

Wenn einer dieser Aufrufe von $.session.send oder $.tool.call nach einer Unterbrechung in einer interaktiven Sitzung abgelehnt wird, kann der Benutzer den Stopped:-Toast sehen.

Arbeit, die Ihr Mod vor der Unterbrechung gestartet hat

Nachdem der Benutzer einen laufenden Turn gestoppt hat, zum Beispiel mit Esc, wird ein Teil der Arbeit, die Ihr Mod bereits gestartet hatte, verworfen, und ein Teil wartet auf die nächste Eingabe des Benutzers.

Arbeit Was bis zur nächsten Eingabe des Benutzers passiert Was zu tun ist
Ein Prompt aus $.prompt.submit, der sich noch in der Warteschlange befindet Verworfen. Der Aufruf wird mit { drop: 'the prompt was removed from the queue before it ran' } aufgelöst. Übermitteln Sie ihn erneut, wie unter Einen abgelehnten oder verworfenen Aufruf erneut versuchen gezeigt
Ein Befehl aus $.command.run, der sich noch in der Warteschlange befindet Verworfen. Der Aufruf wird mit einem Fehler abgelehnt, dessen Nachricht the command was removed from the queue before it ran enthält. Führen Sie ihn erneut aus, wie unter Einen abgelehnten oder verworfenen Aufruf erneut versuchen gezeigt
Ein einmaliger Job, der mit einem Tool wie CronCreate geplant wurde und vor der nächsten Eingabe des Benutzers fällig wird Startet dann keinen Turn und läuft nach dieser Eingabe Nichts erforderlich
Ein wiederkehrender Job, der mit einem Tool wie CronCreate geplant wurde und vor der nächsten Eingabe des Benutzers fällig wird Startet dann keinen Turn. Der Job verliert diese Ausführung und behält seinen Zeitplan. Nichts erforderlich
Ein Event von einem Monitor, den Ihr Mod gestartet hat Der Monitor läuft weiter. Ein Event, das der Monitor vor der nächsten Eingabe des Benutzers auslöst, wird verworfen, mit Ausnahme der Meldung, dass der Monitor beendet wurde; diese kommt nach dieser Eingabe an. Nichts möglich
Ein Subagent, den Ihr Mod gestartet und später mit $.session.send fortgesetzt hat und der vor der nächsten Eingabe des Benutzers fertig wird Seine Abschlussmeldung wartet und erreicht die Konversation nach dieser Eingabe Nichts erforderlich
Ein Bash- oder PowerShell-Befehl im Vordergrund aus $.tool.call, der bereits lief, als der Benutzer den Turn gestoppt hat, und der vor dessen nächster Eingabe seinen Timeout erreicht Wird bei seinem Timeout gestoppt, statt in den Hintergrund verschoben zu werden. Der Aufruf wird mit gesetztem isError aufgelöst, und die Ausgabe enthält die Zeile Ended instead of moving to the background: plugins' input is held, by a Stop or by the host's limits. Führen Sie ihn nach der nächsten Eingabe des Benutzers erneut aus, wie unter Einen abgelehnten oder verworfenen Aufruf erneut versuchen gezeigt

Wenn ein geplanter Job fällig wird oder ein Monitor vor der nächsten Eingabe des Benutzers ein Event auslöst, kann der Benutzer den Stopped:-Toast sehen.

Aufrufe, die in claude -p-Ausführungen und Agent SDK-Sitzungen abgelehnt werden

In einer claude -p-Ausführung oder einer Agent SDK-Sitzung bedeutet ein Ergebnis wie { drop: 'the run reached its --max-turns limit' }, dass die Ausführung den Aufruf Ihres Mods abgelehnt hat.

Ein abgelehnter Aufruf von $.prompt.submit wird mit { drop: '<reason>' } aufgelöst. Andere abgelehnte Aufrufe, etwa $.command.run und $.agent.spawn, liefern die unter Was abgelehnte Aufrufe zurückgeben gezeigten Ergebnisse, mit dem Grund anstelle von <reason>. Suchen Sie unten Ihren <reason>, um zu erfahren, was zu tun ist:

  • the run reached its --max-turns limit: Turns aus den Prompts Ihres Mods und aus Subagenten, die er startet, zählen zu --max-turns oder zum Turn-Limit des Agent SDK. Für mehr Turns starten Sie die Ausführung mit einem höheren Limit. Wenn das nicht möglich ist, sorgen Sie dafür, dass Ihr Mod den abgelehnten Aufruf nicht mehr tätigt. Wenn die Ausführung einen weiteren Prompt vom Benutzer oder von der SDK-Anwendung entgegennimmt, wird die Anzahl der Turns zurückgesetzt, und die Ausführung nimmt wieder Aufrufe an.
  • the run is past its --max-budget-usd limit: Starten Sie die Ausführung mit einem höheren --max-budget-usd oder erhöhen Sie das Ausgabenlimit des Agent SDK. Wenn das nicht möglich ist, sorgen Sie dafür, dass Ihr Mod den abgelehnten Aufruf nicht mehr tätigt. Das Limit gilt für die bisherigen Gesamtkosten der Ausführung. Diese Summe wächst weiter, wenn die Ausführung einen weiteren Prompt entgegennimmt, daher liefert ein Wiederholungsversuch denselben Grund.
  • the host has ended its input: Zum Beispiel hat die SDK-Anwendung ihre Streaming-Eingabe beendet, oder stdin wurde bei einer mit --input-format stream-json gestarteten Ausführung geschlossen. Die Ausführung wird beendet und nimmt keine weiteren Eingaben an, daher liefert ein Wiederholungsversuch denselben Grund. Sorgen Sie dafür, dass Ihr Mod den abgelehnten Aufruf nicht mehr tätigt. Wenn Sie die Ausführung selbst starten, halten Sie diese Eingabe beim nächsten Mal offen, bis Ihr Mod keine Aufrufe mehr zu tätigen hat.
  • the session was stopped and nobody has sent input since: Die Ausführung wurde unterbrochen, zum Beispiel mit interrupt() des Agent SDK. Um den Aufruf nach der nächsten Nachricht des Benutzers oder der SDK-Anwendung erneut auszuführen, folgen Sie Einen abgelehnten oder verworfenen Aufruf erneut versuchen.

Eine claude -p-Ausführung mit Ihrem Mod wird nie beendet

Wenn Ihr Mod am Ende jedes Turns einen Prompt übermittelt, startet jeder Prompt einen weiteren Turn. Ohne --max-turns oder --max-budget-usd läuft eine claude -p-Ausführung, die den Mod lädt, weiter, bis Sie sie selbst stoppen.

Damit die Ausführung von selbst endet, fügen Sie dem Befehl in Ihrer Shell eines der beiden Flags hinzu. Wenn andere Personen die Ausführung starten, sorgen Sie dafür, dass Ihr Mod keine Prompts mehr übermittelt, sobald seine Arbeit erledigt ist. Dieser Befehl fügt --max-turns hinzu:

claude -p "find the TODOs" --max-turns 5

Sobald die Ausführung dieses Limit erreicht, wird der nächste Prompt des Mods abgelehnt, statt einen Turn zu starten, und die Ausführung endet.

Wenn die Ausführung das Limit dennoch überschreitet, führen Sie claude --version in Ihrer Shell aus und aktualisieren Sie Claude Code, wenn Ihre Version älter als v2.1.304 ist. Die Prompts des Mods werden ab v2.1.299 beim Limit abgelehnt. Wenn Ihr Mod Subagenten startet, zählen deren Turns ab v2.1.304 zu --max-turns.

Nachrichten zwischen Sitzungen senden und empfangen

Ein Mod kann eine Nachricht im Klartext an eine andere Ihrer Sitzungen, an einen der Subagenten dieser Sitzung oder an ein Teammitglied in seinem Agent-Team senden. Er kann außerdem die eingehenden und ausgehenden Nachrichten beobachten.

Um eine Nachricht zu senden, rufen Sie $.session.send({ to, text }) auf. Dies nutzt dieselbe Zustellung wie das SendMessage-Tool. Legen Sie to danach fest, wer die Nachricht empfängt:

  • Eine andere Ihrer Sitzungen: { sessionId }
  • Ein Subagent oder Teammitglied: { agentId }, mit einer ID aus $.agent.list()
  • Der Absender einer Nachricht, die Sie empfangen haben: die String-Adresse, von der diese 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 read() wird zu allen Einstellungen aufgelöst, zusammengeführt in einem Objekt. Mit { source } oder { key } wird es stattdessen zu den Einstellungen einer Quelle oder dem Wert eines Schlüssels aufgelöst.
$.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, Prozesse und Anfragen gelten einige eigene Regeln:

  • Pfade: Ein relativer Pfad wird relativ zum Arbeitsverzeichnis der Sitzung aufgelöst oder zu dem des Subagenten, dessen Event der Hook gerade verarbeitet
  • $.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 mit try und catch.
  • $.http.fetch: Folgt bis zu fünf Weiterleitungen. Bei einer Weiterleitung zu einem anderen Origin behält es von den von Ihnen gesetzten Anfrage-Headern nur accept, accept-language, content-type und user-agent bei und verwirft die übrigen. Eine Anfrage, die von einem anderen Header wie Authorization abhängt, kann daher nach einer solchen Weiterleitung fehlschlagen. Die Limits nennen den Timeout und die Body-Größen.

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.

Ein Mod kann Ihren $.process.spawn-Aufruf ablehnen, nachdem der Befehl bereits Ausgabe erzeugt hat oder beendet wurde, und nichts von dem, was der Befehl getan hat, wird rückgängig gemacht. Der Aufruf wird dann mit einer Meldung abgelehnt, die mit einer der folgenden Zeichenfolgen und der Begründung des ablehnenden Mods endet:

  • $.process.spawn started, and a plugin withheld its result:: Der ablehnende Mod hatte die Ausgabe des Befehls nicht bis zum Ende gelesen. Claude Code beendet den Befehl, falls er noch läuft.
  • $.process.spawn ran, and a plugin withheld its result:: Der ablehnende Mod hatte die Ausgabe des Befehls bis zum Ende gelesen, der Befehl war also bereits beendet

Alle Einstellungen, eine Quelle oder einen Schlüssel lesen

$.settings.read() wird zu einem Objekt aufgelöst, das die Einstellungen aus allen verwendeten Quellen enthält, zusammengeführt nach Vorrang. Übergeben Sie { source }, um stattdessen eine einzelne Quelle zu lesen, { key }, um einen einzelnen Schlüssel zu lesen, oder beides:

Aufruf Wird aufgelöst zu
$.settings.read() Den zusammengeführten Einstellungen als ein Objekt. Seine Schlüssel der obersten Ebene sind die einer settings.json-Datei, etwa permissions und hooks.
$.settings.read({ source: 'policy' }) Den Einstellungen dieser einen Quelle als ein Objekt oder {}, wenn diese Quelle keine Einstellungen enthält
$.settings.read({ key: 'spinnerTipsEnabled' }) Dem Wert dieses Schlüssels in den zusammengeführten Einstellungen oder undefined, wenn der Schlüssel nicht gesetzt ist
$.settings.read({ key: 'spinnerTipsEnabled', source: 'user' }) Dem Wert dieses Schlüssels in dieser einen Quelle oder undefined, wenn diese Quelle ihn nicht setzt

Übergeben Sie für source den Namen der einen Einstellungsquelle, die gelesen werden soll:

  • user: ~/.claude/settings.json
  • project: .claude/settings.json
  • local: .claude/settings.local.json
  • flag: was Sie mit --settings übergeben, sowie die Inline-Einstellungen des SDK
  • policy: verwaltete Einstellungen

Übergeben Sie für key den exakten Namen eines Schlüssels der obersten Ebene, keinen Pfad mit Punkten in ein verschachteltes Objekt. Das Argument key erfordert Claude Code v2.1.304 oder höher.

Nächste Schritte