Warum man Claude-Code-Agenten ihren eigenen Status nicht melden lassen sollte
Mit einem Stop-Hook, einem Event-Log und Dollar-Budgets Claude-Code-Sessions als Organigramm betreiben, dazu vier Überraschungen im Harness, die 74 grüne Tests übersehen haben.
Wer mehr als eine Claude-Code-Session gleichzeitig laufen lässt, probiert irgendwann das Naheliegende: Eine Session soll die anderen managen. Das klappt etwa fünf Minuten lang. Nachdem es in mehreren Projekten jedes Mal auf dieselbe Weise schiefging, war der sinnvolle Schritt, das nicht länger als Prompt-Problem zu behandeln, sondern als Architekturproblem. Hier steht, was scheiterte, was stattdessen gebaut wurde und welche sechs Verhaltensweisen des Harness kein Unit-Test hätte finden können; vier davon zeigten sich erst, als eine echte Session lief.
Drei Fehlschläge, nicht einer
Das Symptom, das allen auffällt, ist Stille: Ein Untergeordneter meldet sich nie zurück, selbst wenn man ihm sagt, er solle Bescheid geben, sobald er fertig ist. Doch das ist das harmloseste von drei getrennten Problemen.
Untergeordnete melden sich nicht. Die Anweisung steht im Kontext des Modells, und Kontext wird komprimiert, ist irgendwann erschöpft oder wird schlicht von der Aufgabe verdrängt. Wenn die Arbeit endet, haben vierhundert Zeilen Testausgabe die Erinnerung an die Meldung längst hinausgeschoben. Das ist kein Disziplinproblem. Es ist ein Problem der Speicherhierarchie, und freundlichere Formulierungen lösen es nicht.
Vorgesetzte können nichts beobachten. Das übersieht man leicht, es folgt aus der Funktionsweise von Claude Code. Eine Session arbeitet nur, solange ein Nutzer-Turn läuft, und dazwischen tut sie gar nichts: Sie pollt nicht, wartet nicht, denkt nicht. Ein Untergeordneter, der tatsächlich eine Meldung schickt, erreicht deshalb niemanden. Die Nachricht liegt in einer Session, die schläft, bis ein Mensch etwas eintippt.
Niemand bemerkt Fehler. Man stelle sich zwei Sessions vor, die dieselbe Datei bearbeiten und sich gegenseitig still überschreiben, eine Session, der mitten in der Aufgabe der Kontext ausgeht und die stirbt, oder eine, die sich in einer Schleife dreht. Von außen sieht jede davon genauso aus wie eine Session, die einfach langsam ist.
Das dritte Problem ist das teure, und die naive Lösung, Sessions um Rückmeldung zu bitten, berührt es überhaupt nicht.
Melden ist etwas, das dem Modell passiert
Alles änderte ein einziges Prinzip: Eine Meldung darf nicht davon abhängen, dass das Modell beschließt, sie zu senden. Sie muss rund um das Modell ausgelöst werden, nicht durch das Modell.
Hängt eine Meldung davon ab, dass das Modell sie abschickt, ist sie nur so zuverlässig wie der verbleibende Kontext des Modells. Nimmt man sie ihm aus der Hand, wird sie so zuverlässig wie der Prozesslebenszyklus.
Claude Code hat dafür einen passenden Mechanismus. Ein Stop-Hook läuft jedes Mal, wenn eine Session einen Turn beendet, und aufgerufen wird er vom Harness, nicht vom Modell. Das Modell kann es vergessen haben, keinen Kontext mehr haben, sogar abgestürzt sein, der Hook läuft trotzdem:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "node",
"args": ["${CLAUDE_PLUGIN_ROOT}/hooks/stop-report.mjs"],
"timeout": 15
}
]
}
]
}
}
Das ist die eine tragende Idee. Alles andere ist Gerüst drumherum.
Das Design
Das entstandene Werkzeug betreibt Claude-Code-Sessions als Organisation. Ein Executive spricht mit Ihnen, Managers verantworten Arbeitsbereiche, Staff erledigt die Arbeit. Rollen ergeben sich aus der Position, nicht aus Etiketten: Ein Staff-Mitglied, das ein eigenes Team rekrutiert, agiert für diesen Zweig als Manager.
you
▲
results ───────┼─────── objective
▼
┌───────────────────┐
│ Executive │
└─────────┬─────────┘
┌─────────────┴─────────────┐
▼ ▼
┌───────────┐ ┌───────────┐
│ Manager │ │ Manager │
└─────┬─────┘ └─────┬─────┘
┌────┴────┐ ┌────┴────┐
▼ ▼ ▼ ▼
┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐
│Staff│ │Staff│ │Staff│ │Staff│
└──┬──┘ └──┬──┘ └──┬──┘ └──┬──┘
└─ peers ─┘ └─ peers ─┘
Jedes Mitglied berichtet an genau einen Vorgesetzten, und Eskalation geht immer nur eine Stufe nach oben. Gleichrangige klären überlappende Arbeit untereinander und holen nur dann einen Manager dazu, wenn sie nicht weiterkommen.
Drei Entscheidungen verdienen eine Erklärung, weil jede auch anders hätte ausfallen können.
Der Zustand ist ein reines Append-only-Event-Log. Bis zu zwölf OS-Prozesse schreiben gleichzeitig in ein Verzeichnis. Locks werden vermieden: Jede Zustandsänderung ist eine angehängte Zeile, und das Betriebssystem garantiert, dass so ein Append atomar ist. Ein gespeichertes Organigramm gibt es ebenfalls nicht, es wird bei jedem Lesen durch Falten des Logs berechnet. Eine Session abzuschießen kann nichts beschädigen. Schlimmstenfalls bleibt ein veralteter working-Eintrag zurück, und den zu erkennen ist Aufgabe der Stall-Erkennung.
Vorgesetzte haken nach. Jeder Vorgesetzte sortiert in jedem Turn seine direkten Untergeordneten und steigt eine feste Leiter hinauf: anstupsen, Transkript lesen, neu zuweisen. Die mittlere Sprosse ist wichtiger, als sie aussieht. Ein langer Testlauf und eine Endlosschleife sehen von außen identisch aus, und nur das Lesen der echten Ausgabe unterscheidet sie. Das ist eine Urteilsfrage für ein Modell, nicht für einen Timeout.
Budgets sind verkettet und werden in Dollar gezählt. Ein Kollege wandte ein, bei einem Abo wirkten Dollar abstrakt, spüren würde man die Tokens. Dollar haben trotzdem gewonnen, aus zwei Gründen. --max-budget-usd ist die einzige Grenze, die der Harness wirklich durchsetzt, während eine Token-Obergrenze nur eine Zahl in einer JSON-Datei ist, an die sich nichts halten muss. Außerdem sind Tokens zwischen Modellstufen nicht vergleichbar: Eine Million Tokens kann auf dem leistungsfähigsten Modell etwa zehnmal so viel kosten wie auf dem günstigsten. Ein Token-Budget kann eine Hierarchie, deren Zweck gerade das Mischen von Stufen ist, nicht begrenzen, denn ein Manager könnte locker darin bleiben und die Rechnung trotzdem verzehnfachen. Tokens werden als Messwert protokolliert. Die Grenze sind Dollar.
Der Ablauf eines Auftrags von Anfang bis Ende enthält in der Mitte einen alternativen Zweig, und genau der trägt: Schritt 5 existiert, damit sich das System nie auf Schritt 4 verlassen muss.
Der Zustand, den keine Uhr sieht
Stall-Erkennung wirkt wie ein schlichtes Timeout-Problem, und größtenteils ist sie das auch. Ein Zustand ist jedoch zugleich der dringendste im System und unsichtbar für alles, was Stille misst.
Nehmen wir ein Mitglied, das blocked meldet. Es hat sich gerade gemeldet, also ist die Zeit seit dem letzten Kontakt null, und jede Leerlaufprüfung hält es für gesund. Tatsächlich hat es aufgehört zu arbeiten und wartet auf eine Entscheidung, die nur sein Vorgesetzter treffen kann. Keine Prüfung schlägt an, denn es hat ja höflich verkündet, dass es feststeckt.
Der Smoke-Test deckte das früh auf, noch vor den härteren Bugs. Ein Untergeordneter schickte blocked: beyond_capability, und die Statusansicht des Vorgesetzten zeigte ihn als OK. Hat man es einmal bemerkt, ist die Korrektur klein. Um es zu bemerken, brauchte es einen Live-Lauf.
Dann lief es wirklich
Zu diesem Zeitpunkt gab es 74 Unit-Tests, alle grün, und die CI lief auf Linux und Windows mit zwei Node-Versionen durch. Das Design hielt auf dem Papier und in der Suite, die Zuversicht war groß.
Dann bekam eine einzige echte Hintergrund-Session eine triviale Aufgabe: eine Datei mit dem Wort „hello“ anlegen. Sie scheiterte viermal hintereinander aus vier voneinander unabhängigen Gründen. Keiner davon war ein Programmierfehler. Jedes Mal verhielt sich der Harness anders als angenommen, und jedes Mal scheiterte es lautlos.
1. Session-IDs werden im Hintergrund ignoriert
Die Identität war um eine elegante Idee herum entworfen: Der Elternprozess vergibt die Session-ID des Kindes vorab, sodass das Organigramm geschrieben ist, bevor das Kind existiert, und es keinen Moment gibt, in dem ein Untergeordneter unregistriert läuft.
In der Praxis ignoriert claude --bg --session-id <uuid> die übergebene UUID, und die Hintergrund-Session denkt sich ihre eigene aus. Das Ergebnis hatte fast etwas Ironisches. Der Stop-Hook suchte im Organigramm nach der Session-ID des Harness, fand niemanden und beendete sich, ohne etwas einzutragen. Die Absicherung, die eine Meldung garantieren sollte, tat also gar nichts.
Die Lösung: Die vom Elternprozess gewählte ID per Umgebungsvariable an das Kind übergeben und als maßgeblich behandeln, die Harness-ID nur nutzen, wenn die Variable fehlt.
2. Niemand kann eine Berechtigungsabfrage beantworten
Der Untergeordnete befolgte seinen Auftrag perfekt. Er las die Aufgabe, verstand das Protokoll und führte seinen ersten Befehl aus, die Inbox-Prüfung, genau wie verlangt.
Dann blieb er für immer an einer Berechtigungsabfrage hängen. Der Berechtigungsmodus acceptEdits genehmigt Dateiänderungen, lässt Shell-Befehle aber ungenehmigt, und an einer Hintergrund-Session hängt kein Mensch, der auf Ja klicken könnte. Für den Vorgesetzten ist das von einem Stall nicht zu unterscheiden, obwohl der Untergeordnete alles richtig macht und nur auf eine Genehmigung wartet, die nie kommen kann.
Die Korrektur: Die CLI beim Start mit --allowedTools vorab freigeben. Beide Shell-Werkzeuge angeben, denn zu welchem ein Modell greift, hängt von der Plattform ab: PowerShell unter Windows, sonst Bash.
3. --allowedTools ist variadisch und verschluckt den Prompt
Mit den Berechtigungen startete die nächste Session und meldete sofort, sie sei untätig und warte auf einen Prompt.
--allowedTools <tools...> ist variadisch und frisst daher auch hinter einem booleschen Flag weiter Argumente. Dieser Befehl:
claude --allowedTools "Bash(node:*)" --bg "<the actual prompt>"
zieht den Prompt in die Werkzeugliste und startet eine Session ohne Anweisungen, die geduldig auf eine Eingabe wartet, die nie kommt. Die Option hört sauber an einem Flag auf, das einen eigenen Wert erwartet, also funktioniert diese Form:
claude --allowedTools "Bash(node:*)" --model haiku --bg "<the actual prompt>"
Jetzt werden die Berechtigungsregeln zuerst ausgegeben, direkt vor --session-id, damit die Korrektheit nicht von der Reihenfolge der Argumente an anderer Stelle im Befehl abhängt.
4. Für Änderungen will sie zuerst einen Worktree
Beim vierten Versuch verkündete die Session, sie müsse vor dem Bearbeiten einen Worktree betreten. Sie versuchte, einen anzulegen, scheiterte, weil das Test-Repository keine Commits hatte (der Basis-Branch „HEAD“ ließ sich nicht auflösen), und versuchte dann ein git commit, das eine Genehmigung brauchte, die niemand erteilen konnte.
Das liegt teils an der Testumgebung. Wissenswert ist der zugrunde liegende Fakt trotzdem: Hintergrund-Sessions neigen beim Schreiben zur Isolation per Worktree, und diese Isolation ist eher ein Standard als eine Option.
Zwei weitere, durch Ausprobieren gefunden
--append-system-prompt-file existiert, ist aber undokumentiert. In claude --help fehlt es, doch beim Antesten kommt die Fehlermeldung „Argument fehlt“ statt „unbekannte Option“. Das ist wichtig, denn ein Rollen-Briefing umfasst rund dreißig Zeilen mit Anführungszeichen und Zeilenumbrüchen, und inline übergeben bricht es unter Windows auf drei Arten: cmd.exe kann keine Zeilenumbrüche in einem Argument transportieren, das Escaping von Anführungszeichen unterscheidet sich je nach Shell, und Befehlszeilen sind auf etwa 8191 Zeichen begrenzt. Das Briefing in eine Datei zu schreiben und deren Pfad zu übergeben umgeht alle drei.
--plugin-dir erreicht gestartete Sessions nicht. Ein Plugin aus einem Klon gilt nur für die aktuelle Session, also starten die von ihr erzeugten Sessions ohne Hooks und ohne Rollen-Skills und ignorieren ein Protokoll, das ihnen nie mitgegeben wurde. Ein installiertes Plugin hat dieses Problem nicht. Der empfohlene Weg ist daher eine ordentliche Installation, und für den Klon-Fall enthält die Konfiguration eine explizite Weiterleitungsoption.
Eine grüne Suite reichte trotzdem nicht
Ein weiterer Fall zeigt, warum Live-Tests allein auch nicht die Antwort sind. Die Continuous Integration fand einen Fehler, den lokale Läufe nie gezeigt hatten. Ereignisse im Log wurden nach Millisekunden-Zeitstempel sortiert, Gleichstände per zufälliger Event-ID aufgelöst. Schnelle Maschinen schreiben oft einen Schwung Ereignisse innerhalb einer Millisekunde, sodass aus anstupsen, anstupsen, melden beim Falten melden, anstupsen, anstupsen werden konnte, und ein Mitglied, das sich gerade gemeldet hatte, galt dann weiterhin als nicht angestupst. Die Projektion war nicht deterministisch und konnte bei jedem Laden anders ausfallen.
Lokal unter Windows liefen alle 74 Tests durch. Unter Linux schlug die CI zweimal fehl.
Feinere Zeitstempel waren nicht die Lösung. Da Appends atomar sind, spiegelt die Reihenfolge der Zeilen in der Datei bereits die Schreibreihenfolge über alle Prozesse hinweg. Gleichstände nach Zeilenposition aufzulösen ergibt eine Ordnung, die deterministisch und kausal korrekt ist.
Was man mitnehmen sollte
Nichts davon zeigt, dass KI-gestützte Entwicklung unzuverlässig ist. Der Code war der billige Teil, und das meiste davon stimmte. Es zeigt vielmehr, woher Zuversicht kommen sollte. Eine grüne Suite beweist, dass das System tut, was man modelliert hat. Ob das eigene Modell der Plattform stimmt, kann sie nicht sagen, und alle oben genannten Fehler lagen genau in diesem toten Winkel, weil die Tests dieselben falschen Annahmen enthielten wie die Implementierung.
Funktioniert hat, Widerlegung billig zu machen und sie immer wieder zu betreiben. Vier Live-Läufe von je wenigen Minuten, die auf dem kleinsten Modell fast nichts kosteten, deckten vier Architekturfehler auf, die 74 Tests abgesegnet hatten.
Wer auf einer Agentenplattform baut, sollte für diese Schleife bewusst Zeit einplanen. Die interessanten Bugs stecken nicht im eigenen Code. Sie sitzen an der Nahtstelle zwischen dem eigenen Code und einem Harness, dessen Verhalten man erschlossen statt überprüft hat. Es lohnt sich auch, diese Fakten über den Harness in einer eigenen Notizdatei getrennt von der Designspezifikation zu führen, denn sie beschreiben die Plattform und nicht das eigene Projekt.