Mods testen und debuggen
Wenn eine Mod scheinbar nichts tut, hat die Engine den Grund meistens schon irgendwo notiert. Dieser Artikel behandelt die drei Werkzeuge, mit denen du diese Aufzeichnungen findest.
| Werkzeug | Wann | Was du siehst |
|---|---|---|
claude plugin validate <Ordner> |
Bevor du die Mod in eine Sitzung lädst | Manifest und Quelltext des Hook-Moduls, also die Stellen, die die Engine ablehnen würde |
claude plugin test <Ordner> |
Nach jeder Änderung | Führt *.test.ts(x) auf der Engine aus |
| Debug-Log | In einer echten Sitzung | Warum ein Hook übersprungen wurde, abgelehntes Zeichnen |
Stand: Die Befehlsausgaben stammen aus eigenen Läufen mit Claude Code 2.1.291. Die API-Beschreibungen habe ich anhand der Typdeklarationen von 2.1.290 und der Referenz geprüft. Mit „Beobachtung“ markierte Punkte habe ich beim Bau von ide-mod unter 2.1.290 selbst erlebt.
validate: eine Prüfung, die den Quelltext liest
Abschnitt betitelt „validate: eine Prüfung, die den Quelltext liest“validate führt die Mod nicht aus. Es liest den Quelltext genauso wie die Engine beim Laden und berichtet. Jede Zeile der Ausgabe hat eine feste Bedeutung.
| Zeile | Bedeutung |
|---|---|
hooks: |
Registrierte Ereignisse und Matcher |
calls: |
Im Quelltext gefundene $-Aufrufe |
gating hook with .catch: / without .catch: |
Hook an einer Stelle, an der etwas abgelehnt werden kann. Ein Tatsachenbericht, der nicht als Warnung zählt |
state reads: / state writes: |
$.state-Schlüssel. Sie werden erkannt, weil plugin und key Literale sind |
types … declares |
Was die Vertragsdatei deklariert, auf die types im Manifest zeigt |
Der Exit-Code ist 1, wenn es Fehler gibt. --strict wertet auch Warnungen als Fehlschlag und passt daher zu CI, und --json gibt dasselbe als JSON aus. Im JSON stehen die Hooks, die etwas ablehnen können, getrennt im Array gatingHooks.
Weil die Prüfung den Quelltext liest, gibt es Regeln. Die beiden folgenden Fehler sind die echte Ausgabe, nachdem ich absichtlich fehlerhafte Mods gebaut hatte (die Pfade sind gekürzt).
✘ Found 1 error:
❯ modules../register.ts: validate-demo: …/hooks/register.ts, compiled line 4 `await hello($);`: $ is passed to "hello", imported from "./helpers": $ is followed only into a function declared in this same file, never across an import; …
✘ Validation failed$ wird nur in Funktionen derselben Datei verfolgt. Gibst du $ an eine andere Datei weiter, weiß der Prüfer nicht, was dort aufgerufen wird, und lehnt ab. Lege Code, der $ benutzt, in eine einzige Hook-Moduldatei, und in die Dateien außerhalb nur reine Funktionen.
❯ modules../register.ts: validate-demo: …/hooks/register.ts, compiled line 7 `on("session.start", async ($, e, next) => next(e));`: on("session.start") is registered twice without a matcher; the first is at …/hooks/register.ts:3; …Auch dasselbe Ereignis zweimal ohne Matcher zu registrieren, ist ein Fehler. Erledige mehrere Aufgaben in einem Hook.
test: Tests, die auf der Engine laufen
Abschnitt betitelt „test: Tests, die auf der Engine laufen“claude plugin test <Ordner> sucht unter dem Ordner *.test.ts und *.test.tsx und führt jede Datei in einem Kindprozess der Claude-Code-Programmdatei aus. Die Umgebung entspricht der, in der Hooks laufen, daher gibt es keinen Zugriff von Node auf Dateien, Netzwerk oder Prozesse. Den Mod-Ordner liest der Loader der Engine wie in einer Sitzung.
Du importierst Tests aus claude-code/testing.
| Name | Was er tut |
|---|---|
test(name, ($, on) => …) |
Ein Test. Standardlimit 5.000 ms, änderbar mit { timeoutMs } |
test(name, { options }, body) |
Übergibt userConfig-Werte, als wären sie in der Konfiguration gespeichert |
test(name, { plugins }, body) |
Lädt andere Plugins inline mit |
describe, expect |
Gruppen und Prüfungen. toBe, toEqual, toMatch, toThrow, expect.any usw. |
mock.clock(on, { now }) |
Eine Uhr im Speicher. Sie bewegt sich nur mit advance, set, settle und sleep |
mock.store(on, entries) / mock.env(on, vars) |
Beantworten $.store und $.env.get aus dem Speicher |
tier('append') |
Legt ganz oben in der Datei fest, auf welche Ebene die Mod geladen wird |
on ist der Platz der Engine
Abschnitt betitelt „on ist der Platz der Engine“Das $ im Test ist die Engine selbst. $.tool.call(...), $.turn.start(...) und $.command.run(...) durchlaufen dieselbe Kette, die die Engine in einer Sitzung aufruft. Hooks, die du im Test mit on einhängst, stehen unter allen Plugins und spielen die Engine. Darunter ist der Boden leer, und wenn kein Hook antwortet, scheitert es so:
HooksError: no implementation for turn.start
nothing beneath the plugins answers turn.start: a test answers it with on('turn.start', ...)
the engine reported: turn-meter's turn.start hook was skipped: turn-meter: no implementation for clock.nowAn die Fehlermeldung hängt the engine reported: die bisher übersprungenen Hooks samt Grund an. Im Beispiel oben wurde mein Hook übersprungen, weil der Boden für die Uhr fehlte. Die Antwort war eine einzige Zeile: mock.clock(on). Das Plugin wird geladen, sobald der Test zum ersten Mal $ aufruft. Registriere Boden-Hooks also davor.
Welche Form ein Boden-Hook zurückgeben muss, hängt vom Ereignis ab. Das sind die Regeln, die ich in den ide-mod-Tests herausgefunden habe (Beobachtung).
- Ereignisse für
$-Aufrufe wiefs.read,fs.stat,ui.open,session.cwdundenv.getverpackst du in{ value: … }. tool.callgibt{ result: … }oder{ deny }zurück.- Ereignisse wie
turn.startundturn.completegeben den Ergebnistyp unverändert zurück ({ turnId },{ text }). - Die Test-Engine löst relative Pfade relativ zum Mod-Ordner auf. Das gefälschte Dateisystem baust du mit absoluten Pfaden.
UI-Tests
Abschnitt betitelt „UI-Tests“$.ui.mount({ plugin, surface, component, props }) zeichnet einmal und gibt dir ein Handle. surface hat keinen Standardwert. Gib eines von terminal, desktop, vscode oder mobile an. Mit dem Handle nutzt du find, findAll, drawn (der ganze Baum), press, input, select, redraw und unmount. key, pointer, post und resize wirken nur auf Client-Elemente.
Zwei Dinge solltest du wissen.
- Das
mount-Handle hat keine Aktion, um Tasten oder das Mausrad an den Inhalt einesPanezu senden. Das Scrollverhalten von ide-mod habe ich auf dem echten Bildschirm geprüft (Beobachtung). - Ein
keyanTextblieb im gezeichneten Baum nicht erhalten (in 2.1.291 bestätigt).TextPropshat keinenkey. Verpacke das gesuchte Element in eineBoxmitkey.
Wird das Zeichnen abgelehnt, lehnt mount mit diesem Grund ab. So fängst du schon im Test die Ursache ab, warum in einer Sitzung der Bildschirm leer bleibt. Das sind die echten Meldungen, nachdem ich absichtlich drei fehlerhafte Bäume gemountet hatte.
refuse-demo: ui.render (Pane) refused: Image source.generation must be a whole number when given; the engine drew its ownrefuse-demo: ui.render (Pane) refused: Image source.png does not start as a PNG file does; the engine drew its ownrefuse-demo: ui.render (Pane) refused: Svg source longer than 131072 characters; the engine drew its ownDas Debug-Log lesen
Abschnitt betitelt „Das Debug-Log lesen“claude --debug-file ./mod-debug.log # 파일로 남기기 (디버그 모드도 켜져요)claude --debug # 디버그 모드claude --debug "hooks" # 범주 거르기(Die Kommentare bedeuten: in eine Datei schreiben, wobei auch der Debug-Modus eingeschaltet wird; Debug-Modus; nach Kategorie filtern.)
Ich habe eine Mod, deren session.start-Hook absichtlich mit JSON.parse('{oops') wirft, per --plugin-dir geladen und claude -p "/ping" ausgeführt. Von 656 Logzeilen sind das die, in denen der Mod-Name vorkommt (ohne Zeitstempel).
[DEBUG] --plugin-dir …/debug-demo is one plugin: .claude-plugin at its top marks it[DEBUG] hooks module debug-demo@inline loaded (worker, environment 1, tier user); events: session.start,command.run[DEBUG] plugin.register: debug-demo (user, debug-demo@inline), judged by core alone: admitted[DEBUG] type root of debug-demo at …/debug-demo/.claude-plugin/types: entries claude-code, claude-code-tools, claude-code-mcp; wrote …, tsconfig.json[DEBUG] $.command.register (debug-demo): /ping listed[ERROR] hook failed: debug-demo: errorKind=SyntaxError errorChars=30 (session.start; skipped; what is below it ran in its place)[ERROR] debug-demo: session.start hook skipped: threw SyntaxError: JSON Parse error: Expected '}'[DEBUG] debug-demo (user) answered command.run without next() in 0.6ms; nothing beneath it ran for this dispatchSo liest du das Log der Reihe nach:
- Fehlt die Zeile
loaded … events:, wurde das Modul nicht geladen. Bei einem-p-Lauf steht der Grund auch in einer Zeile auf stderr. In diesem Lauf stand auf stderrdebug-demo: session.start hook skipped: threw SyntaxError…. - Die Zeile
hook failedenthält nur die Fehlerart und die Länge der Meldung (errorChars=30). Laut Referenz wird der Text selbst getrennt notiert. Die direkt folgende Zeile ist dieser Text. answered … without next()heißt, dass dein Hook selbst geantwortet hat und darunter nichts mehr lief. Wenn sich eine andere Mod nicht bewegt, suche zuerst nach dieser Zeile.
Auch die Zeile type root … wrote lohnt einen Blick. Bei jedem Laden einer Mod schreibt die Engine .claude-plugin/types/ und tsconfig.json in den Mod-Ordner. In einem einmal geladenen Mod-Ordner läuft tsc -p <Ordner> sofort. In .claude-plugin/types/ legt die Engine zusätzlich eine .gitignore mit nur einer Zeile * ab, sodass der Ordner nicht in Git landet. Die tsconfig.json im Stammordner ist eine einzige Zeile, die die Konfiguration dieses Ordners per extends übernimmt.
„the engine drew its own“
Abschnitt betitelt „„the engine drew its own““Abgelehntes Zeichnen bleibt mit den Formulierungen zurück, die die Referenz festlegt.
| Zeile | Bedeutung |
|---|---|
ui.render (<Component>): a hook returned a tree that does not validate |
Debug-Log. Der Baum passt nicht zu den Regeln dieser Oberfläche |
<plugin>: ui.render (<Component>) refused: <Grund>; the engine drew its own |
Dialog in einer Sitzung mit Hot-Reload. Dieselbe Ablehnung |
… threw while drawn: <Grund>; the engine drew its own |
Die Validierung wurde bestanden, aber beim Zeichnen gab es einen Absturz |
…; nothing was drawn / …; the pane was closed |
An dieser Stelle hätte die Engine ursprünglich nichts gezeichnet. Das Band bleibt leer, und das Fenster wird geschlossen |
<n> characters of text are drawn up to the first <m> |
Der Text war länger als 100.000 Zeichen und wurde abgeschnitten |
Die Dialogzeile erscheint nur in Ordnern mit Hot-Reload wie bei --plugin-dir. In anderen Sitzungen bleibt sie nur im Debug-Log. Eine beim Zeichnen abgestürzte Antwort wird nicht erneut versucht. Erst die nächste Antwort (andere Props, invalidate, Reload) zeichnet wieder neu.
Hot-Reload
Abschnitt betitelt „Hot-Reload“| Wie du die Mod geladen hast | Wann eine Änderung wirkt |
|---|---|
Ordner über --plugin-dir, CLAUDE_CODE_PLUGIN_DIRS |
Die interaktive Sitzung beobachtet den Ordner. Beim Speichern läuft register in einer neuen Umgebung erneut, und die alten Timer werden verworfen |
| Mod-Ordner der Sitzung (Enable hot reloading) | Wird auf dieselbe Weise beobachtet |
| Aus einem Ordner-Marketplace installiert | /reload-plugins liest diesen Ordner neu |
| Aus Git, npm usw. installiert | claude plugin update auf die neue Version, danach /reload-plugins |
Was das Modell mitten im Turn ändert, wird einmal neu gelesen, wenn der Turn endet. Steht ein Tool oder Befehl dieser Mod kurz vor der Ausführung, wird schon dann gelesen. Speichert ein Mensch von Hand, wird gelesen, nachdem es im Ordner ruhig geworden ist. Ein einzelnes Speichern wird nach 0,25 Sekunden gelesen, bei mehreren Speichervorgängen in Folge erst, nachdem sie aufgehört haben. claude -p liest immer neu.
/reload-skillsliest Mods nicht neu. Ein Nutzer hatte diesen Befehl eingegeben, und der Bildschirm blieb unverändert (Beobachtung). Nimm/reload-plugins.- Der Mod-Ordner der Sitzung lag unter
~/.claude/dev-mods/<Sitzungs-ID>/. Wird die Sitzung neu gestartet und die ID ändert sich, ändert sich auch der Ordner, und Änderungen im alten Ordner werden nicht gelesen (Beobachtung). Den Zeitpunkt des letzten Ladens erkennst du an der Änderungszeit von.claude-plugin/types/claude-code/index.d.ts. Für eine Mod, die du lange benutzen willst, nimm einen festen Ordner.
Was mir beim Bau von ide-mod begegnet ist
Abschnitt betitelt „Was mir beim Bau von ide-mod begegnet ist“| Symptom | Ursache | Beleg |
|---|---|---|
| Das ganze Bildfenster wird abgelehnt | In source.generation von Image stand die Änderungszeit der Datei (mit Nachkommastellen). Es muss eine ganze Zahl sein |
Typdeklaration „A whole number“, mit dem Test-Kit von 2.1.291 reproduziert |
Image wird abgelehnt |
Die Bytes von source.png sind kein PNG. Die Engine prüft bis zum IHDR |
Beobachtung unter 2.1.290, die Header-Prüfung unter 2.1.291 reproduziert |
Svg auf dem Desktop ist nicht zu sehen |
source ist länger als 131.072 Zeichen |
Referenz, unter 2.1.291 reproduziert |
| Eine WASM-Bibliothek läuft nicht | In der Hook-Umgebung gibt es WebAssembly, eval und new Function nicht. Schwere Arbeit gehört mit $.process.run in einen eigenen Prozess |
Typdeklaration |
setTimeout fehlt |
Hook-Module warten mit $.clock.after, every und sleep |
Typdeklaration |
| Zustand schreiben beim Zeichnen schlägt fehl | Während des Zeichnens wird $.state.set abgelehnt. Schreibe in Button-Handlern oder anderen Ereignissen |
Referenz |
| Eine im Zeichnen gestartete Arbeit wird abgebrochen | Hooks laufen innerhalb eines Dispatches, und wird der Dispatch verworfen, bricht next.signal ab. Starte lang laufende Arbeit in session.start oder in einem $.clock.after-Timer |
Referenz |
| Beim Rollen des Mausrads bewegen sich Baum und Dateispalte zusammen | Die Engine scrollt ein Fenster als Ganzes. Im ui.scroll-Hook siehst du mit e.pointer.column, über welcher Spalte der Zeiger steht, antwortest ohne next mit {} und verschiebst dann die Zeilen dieser Spalte um e.by und zeichnest neu |
Typdeklaration, Beobachtung unter 2.1.290 |
Ein mit $.process.run gestartetes Node-Skript gibt 0 Zeichen aus |
Läuft es in einem verlinkten Ordner, ist $.plugin.root der Link-Pfad, und die Prüfung „direkt ausgeführt“ wird falsch. Vergleiche mit realpath |
Beobachtung unter 2.1.290 |
Beim letzten Punkt habe ich noch etwas gelernt. Reproduziere Diagnosen über denselben Pfad wie die Engine (den Link-Pfad). Die Prüfung über den echten Pfad hat das Problem verdeckt.
Eine kürzere Liste nach Symptomen findest du unter Fehlerbehebung. Beispiele, in denen die Befehle dieses Artikels auf echte Mods angewendet werden, sind die Schritt-für-Schritt-Anleitungen Band über dem Eingabefeld und Bash-Guard.
Inoffizieller Community-Leitfaden, nicht mit Anthropic verbunden oder von Anthropic unterstützt. Claude und Claude Code sind Marken von Anthropic.