Codex-CLI-Sandbox-Deadlock unter Windows: headless codex exec hängt nach stillem Upgrade
Ein stilles CLI-Upgrade fror Codex 45 Minuten ein
Ein headless gestarteter Codex-Build fror 45 Minuten ein. Im eigenen Log stand nichts. Ein inoffizielles CLI-Upgrade hatte zwei ungeschriebene Annahmen gleichzeitig gebrochen — und die Diagnose lag nicht dort, wo ich sie suchte.
In meiner Dual-Bridge (dem System, in dem ein KI-Modell Code baut und das andere ihn reviewt) wechseln sich beide Seiten ab. Die Builder-Seite läuft headless: codex exec wird als Subprozess gestartet, bekommt einen Auftrag in einem isolierten Wegwerf-Verzeichnis und arbeitet. Das lief seit Wochen zuverlässig. Dann, an einem Seed-Lauf, fror es ein. Fünfundvierzig Minuten lang. Im Log meines Adapters stand: nichts.
Das ist die unangenehmste Sorte Fehler — der, bei dem das Werkzeug korrekt anzufangen scheint und dann einfach stehenbleibt. Die ersten Bauschritte liefen sichtbar durch. Dann Stille. Kein Fehler, kein Fortschritt, ein Prozess-Baum, der dasteht und wartet.
Zwei Annahmen, gleichzeitig gebrochen
Die Ursache war ein CLI-Upgrade, das ich nicht bewusst eingeplant hatte: Codex war von 0.133 auf 0.136 gesprungen. Solche Sprünge brechen gern Annahmen, die man nie aufgeschrieben hat, weil sie immer galten. Hier waren es zwei auf einmal.
Erstens lädt 0.136 jetzt die ~/.codex/config.toml mit allem, was darinsteht — Plugins, MCP-Server, Hooks. Und es feuert bei internen Shell-Kommandos einen SessionStart-Hook. Dieser Hook ist synchron und ohne Timeout. Unter einer interaktiven Session fällt das nicht auf. Unter einem non-interaktiven exec als Subprozess hängt er sich an einer Stelle auf, an der niemand auf eine Eingabe antwortet.
Zweitens lief der Build unter -s workspace-write — einer Sandbox, die Schreibzugriffe auf das Arbeitsverzeichnis erlaubt, aber %TEMP% nicht zuverlässig einschließt. pytest legt seine temporären Verzeichnisse aber genau dort an. Die Sandbox verweigert den Zugriff, der Hook beginnt nach einer Freigabe zu suchen, die im headless Betrieb nie kommt — und dreht sich in einer Schleife, die nach %TEMP% probt, statt abzubrechen.
Beide Probleme für sich wären unangenehm. Zusammen ergeben sie einen Deadlock: Ein synchroner, timeout-loser Hook trifft auf eine Sandbox, die ihm den Weg versperrt, in einem Kontext, in dem niemand „ja, mach" sagen kann. Und der timeout meines eigenen subprocess.run rettet hier nicht — er tötet den direkten Kindprozess nicht durch die ganze Kette python → node → codex → cmd → bash hindurch.
Die Diagnose lag nicht in meinem Log
Der entscheidende Schritt war, dem eigenen Log nicht zu vertrauen. Mein Adapter-Log war leer oder gepuffert — es zeigte den letzten erfolgreichen Schritt und dann nichts mehr. Das verleitet dazu, an der falschen Stelle zu suchen.
Codex schreibt aber sein eigenes Rollout-Log: ~/.codex/sessions/<datum>/rollout-*.jsonl, eine Zeile pro Ereignis. Darin steht jeder function_call und das zugehörige function_call_output. Der Hängepunkt ist exakt der letzte function_call, dem kein function_call_output folgt. Das ist der Befehl, der losgeschickt wurde und nie zurückkam — in diesem Fall der shell_command, der in der %TEMP%-Sandbox-Probing-Schleife versackte.
Diese Regel verallgemeinert sich: Wenn ein headless gestarteter LLM-Subprozess hängt, ist die Wahrheit im Session-/Rollout-Log des Werkzeugs, nicht im eigenen, oft leeren oder gepufferten Prozess-Log. Der letzte unbeantwortete Aufruf ist der Hängepunkt.
Der Fix: Builder und Reviewer brauchen verschiedene Rechte
Die Behebung war, die Sandbox-Semantik an die Aufgabe anzupassen, statt einen Mittelweg zu erzwingen. Für einen isolierten Wegwerf-Workdir, in dem der Builder ohnehin schreiben muss, ist -s danger-full-access -c approval_policy="never" die richtige Wahl: Der Sandbox-Drop ist die eigentliche Behebung, und approval_policy="never" unterbindet das Approval- und %TEMP%-Probing, bevor es zur Hang-Schleife werden kann.
Spannend war die Kehrseite: Ein Reviewer, der nur liest — etwa einen im Prompt eingebetteten Diff beurteilt und nichts schreiben muss — hängt nicht. Er läuft mit -s read-only -c approval_policy="never" sauber durch. Der Schlüssel ist also nicht der Sandbox-Modus an sich, sondern zweierlei: dass kein Schreibzugriff nötig ist und dass approval_policy="never" das Probing unterbindet. Builder schreiben, also brauchen sie vollen Zugriff; Reviewer lesen nur, also kommen sie mit read-only aus — beide nur mit abgeschaltetem Approval.
Die Lektion: externe Upgrades brechen still die Spielregeln
Der teuerste Teil war die Annahme, dass eine CLI sich zwischen zwei Minor-Versionen gleich verhält. Tut sie nicht. 0.136 hat die Sandbox- und Flag-Semantik verändert, ohne dass mein Code etwas davon mitbekam — er rief weiter dieselben Flags auf, die jetzt etwas anderes bedeuteten. „Funktioniert manuell" war zusätzlich irreführend, weil ein interaktiver Lauf eine andere Approval- und Sandbox-Semantik hat als ein Dienst-Lauf. Genau wie bei den ersten Subprozess-Fehlern war die Lücke zwischen „läuft bei mir in der Konsole" und „läuft headless als Subprozess" der Ort, an dem es brach.
Was offen ist
Die aktuelle Lösung pinnt die Flags pro Rolle — Builder voll, Reviewer read-only, beide ohne Approval. Was fehlt, ist ein Schutz gegen die nächste stille Semantik-Änderung: Der Adapter sollte die Codex-Version prüfen und bei einem unerwarteten Sprung warnen, statt blind die alten Flags zu verwenden. Außerdem ist der subprocess-Timeout, der die Prozesskette nicht sauber durchtötet, weiterhin ein stumpfes Werkzeug — ein robusteres Kill über die ganze python → node → codex → cmd → bash-Kette wäre die ehrlichere Absicherung gegen den nächsten Hang. Bis dahin ist die wichtigste Verteidigung diagnostisch: bei einem Hang nicht dem eigenen Log glauben, sondern das Rollout-Log lesen.
Auch ein stilles CLI-Upgrade kann einen Lauf komplett einfrieren — und die Wahrheit steht dann selten im eigenen Log. Wenn dir das auch passiert ist, schreib mir.