Max Nardit
← Artikel
6 mindev

Claude Code Fork-Subagents in claude -p: was 2.1.302 für Headless-Runs geändert hat

Angeboten heißt nicht benutzt: Ein Modell im Headless-Modus wählt den Fork vielleicht nie, und die offizielle Doku beschreibt noch den alten Standard. Wie du prüfst, welchen Subagent deine Pipeline genutzt hat, warum ein Fork im Hintergrund einem Parser zwei Antworten liefert und wo sich das Abschalten lohnt.

Inhalt

Claude Code 2.1.302 kam am 16. Oktober mit einer Zeile, die jeden unbeaufsichtigten Run betrifft. Fork-Subagents, also der Subagent-Typ, der mit der ganzen Konversation des Parents startet statt mit leerem Kontext, sind jetzt standardmäßig an: in claude -p, im Agent SDK, in der Desktop-App und in VS Code. Vor diesem Release bekam ein Headless-Run sie nur, wenn du sie mit CLAUDE_CODE_FORK_SUBAGENT=1 eingeschaltet hattest. Wenn dein CI-Job, ein Cron-Skript oder ein SDK-Service diese Woche aktualisiert wurde, bietet sein Agent-Tool jetzt einen Typ an, den es letzte Woche standardmäßig nicht angeboten hat, und nichts in deiner Konfiguration sagt dir das.

Unangenehm ist, wohin der Standard gewandert ist. Headless-Runs sind der Ort für Jobs, die ihren Input unvoreingenommen ansehen sollen: der CI-Reviewer, der Grader, der Suchlauf, der finden soll, was im Code steht, und nicht, was der Aufrufer erwartet hat. Ein Fork ist genau der Subagent-Typ, der die Annahmen des Aufrufers mitnimmt. Der Wechsel trifft also den Modus, in dem geerbter Kontext am häufigsten falsch ist, und die offizielle Subagents-Seite sagt noch, dass der alte Standard gilt. Ich habe es lokal auf 2.1.302 laufen lassen, um zu sehen, was sich für so einen Job ändert.

Was hat Claude Code 2.1.302 an Fork-Subagents geändert?

Es hat subagent_type: "fork" außerhalb des Terminals standardmäßig verfügbar gemacht. In der Release Note steht, Fork-Subagents seien „on by default in claude -p, the Agent SDK, the desktop app and VS Code, as they already are in the terminal; CLAUDE_CODE_FORK_SUBAGENT=0 turns them off, and claude mcp serve never offers them.“

Laut Doku bekommt ein Fork dieselben Anweisungen, Tools, dasselbe Modell und die komplette Message History wie die Hauptsession und teilt den Prompt Cache des Parents. Deshalb kostet er weniger als ein frischer Subagent, wenn die Arbeit denselben Kontext braucht. Seit 2.1.285 läuft er außerdem im Permission Mode des Parents. Weitere Forks kann er nicht starten.

Die Subagents-Seite ist nicht nachgezogen. Ihr Abschnitt „Turn fork mode on or off“ sagt immer noch, Claude Code „leaves it off by default in non-interactive mode with -p and in the Agent SDK“, und wiederholt das an anderer Stelle. Wer nach der Doku entscheidet, hält seine Pipeline für nicht betroffen. Was die Seite zur Umgebungsvariable sagt, stimmt weiterhin: 1 schaltet den Fork-Modus auch in -p und im SDK ein, 0 schaltet ihn in jeder Art von Session ab.

Erbt ein Claude Code Fork-Subagent in claude -p die Konversation?

Ja. Versteck ein Wort im Prompt, verlang einen Fork und frag den Fork nach dem Wort, ohne dass er etwas liest:

shell
cd "$(mktemp -d)" && git init -q
claude -p 'The secret word for this session is PERSIMMON. Call the Agent tool exactly once with subagent_type set to "fork" and the prompt: "Without reading any file, reply with the secret word for this session, or NONE if you do not know it." Then reply with the subagent result verbatim.' \
  --model haiku --permission-mode bypassPermissions \
  --output-format stream-json --verbose < /dev/null > fork-on.jsonl

Das Bypass-Flag steht da, weil der Run unbeaufsichtigt läuft und der Fork nichts liest. Meine Runs haben außerdem meine eigenen Plugins und Agents geladen; die Agent-Liste, die du siehst, ist also deine und nicht der Auslieferungszustand. Auf 2.1.302 antwortete der Fork mit PERSIMMON. Mit CLAUDE_CODE_FORK_SUBAGENT=0 vor demselben Befehl lehnte das Agent-Tool den Typ ab:

Agent type 'fork' not found. Available agents: ...

Das Modell gab diesen Fehler dann als Antwort zurück, das finale Ergebnis trug is_error: false, und der Prozess endete mit Exit Code 0. Ein Prompt oder Skill, der den Fork-Typ explizit nennt, scheitert beim Abschalten also lautlos: kein fehlgeschlagener Build, nur eine falsche Antwort. Wenn du die Variable setzt, durchsuch vorher deine Prompts nach dem Typnamen.

Wählt Claude Code in einem Headless-Run von sich aus einen Fork?

In meinem Test nicht. Mit eingeschaltetem Fork-Modus und einem neutralen Prompt („use the Agent tool exactly once to delegate this subtask“) verlangte Haiku general-purpose beim Namen, denselben Typ wie bei abgeschaltetem Fork-Modus. Ein Run mit einem Modell beweist nichts über deine Runs. „Standardmäßig an“ heißt, dass der Fork-Typ angeboten wird. Ob deine Runs ihn nutzen, hängt von Modell und Prompt ab, also prüf es, statt es vorherzusagen.

Wie siehst du, welchen Subagent-Typ ein claude -p Run genutzt hat?

Im stream-json-Output, nicht im Startup-Event der Session. Das init-Event listet die Agents einer Session, und auf 2.1.302 stand fork auch bei eingeschaltetem Fork-Modus nicht in dieser Liste. Er taucht erst auf, wenn etwas ihn anfordert. Ein schneller Check:

shell
grep -o '"subagent_type":"[^"]*"' fork-on.jsonl | sort -u
grep -o "Agent type 'fork' not found" fork-on.jsonl | head -1

Nimm beides als ersten Blick, nicht als Beweis. Das erste grep trifft den Agent-Aufruf und auch die Lifecycle-Events der Hintergrund-Tasks, und es zeigt, was angefordert wurde. Ein abgelehnter Fork erscheint dort also trotzdem, das zweite grep fängt diese Ablehnung. Um zu bestätigen, dass ein Fork gelaufen ist, such das task_started-Event zu diesem Tool-Aufruf und die passende Abschlussmeldung. Die Transkripte der Subagents liegen unter ~/.claude/projects/ in einem subagents/-Ordner der Session, falls du sehen willst, was das Kind bekommen hat.

Warum liefert ein Claude Code Fork in claude -p zwei Ergebnisse?

Weil der Fork im Hintergrund lief, und claude -p Hintergrundarbeit ohnehin als Grund für einen weiteren Turn behandelt. In beiden meiner Runs sagte das Tool-Ergebnis „Async agent launched successfully“, das Modell antwortete, der Fork laufe noch, und dieser Turn war zu Ende. Der Prozess blieb offen, der Fork wurde fertig, und die Session startete einen zweiten Turn, um PERSIMMON weiterzugeben. Die Headless-Doku beschreibt das für jeden Hintergrund-Subagent: Der Run wartet auf die Arbeit, und „when background work starts another turn, the run prints each turn’s result with the default text output and the last turn’s result with json output.“

Was ich pro Format gesehen habe:

--output-format text         both turns concatenated: "The fork agent is still running..." then "PERSIMMON"
--output-format json         one object, result "PERSIMMON" (the final turn only)
--output-format stream-json  two events with "type":"result", the first one the non-answer

Ein Parser, der in stream-json das erste result-Event nimmt oder dort aufhört zu lesen, bekommt „still running“ und hält den Job für erledigt. Wer Text-Output verarbeitet, bekommt den Zwischensatz an die Antwort geklebt. Zähl die Events:

shell
grep -c '"type":"result"' fork-on.jsonl

Wenn das bei einem Job, den du für einen einzigen Turn gehalten hast, 2 ausgibt, lies den Stream bis zum Ende und nimm das letzte result, das laut Doku die letzte Zeile ist, oder wechsel auf json. Forks abzuschalten entfernt diese eine Quelle von Hintergrundarbeit, nicht jede.

Wann solltest du Fork-Subagents in einer Claude Code Pipeline abschalten?

In jedem Headless-Job, dessen Wert ein unverstellter Blick ist. Behalte sie dort, wo das Kind die Arbeit des Parents fortsetzt. Ein Reviewer oder Grader lohnt sich gerade, weil er das Framing des Autors nicht teilt. Ein Fork vom Kontext dieses Jobs gibt dem Kind alles mit, was der Job vor dem Aufruf gelesen hat, einschließlich der Vermutungen, die er später verworfen hat. Die längere Version dieses Arguments steht in warum ein geforkter Subagent schon zu viel weiß: Einem geerbten Transkript sieht man nicht an, welche seiner Schlüsse noch gelten und welche tot sind. Fortsetzung ist der Fall, in dem ein Fork seinen Platz verdient: wenn der halbfertige Zustand selbst die Arbeit ist.

Der grobe Schalter gilt pro Job oder pro CI-Step:

shell
CLAUDE_CODE_FORK_SUBAGENT=0 claude -p "..."

Der feinere ist eine Deny-Regel, eingefügt in den permissions-Block der .claude/settings.json des Projekts (oder .claude/settings.local.json für einen lokalen Override):

json
{
  "permissions": {
    "deny": ["Agent(fork)"]
  }
}

Laut Doku lässt sie den Fork-Modus an und hindert Claude nur daran, Forks zu starten. Ich habe die Regel nicht getestet. Die Doku sagt auch, dass der Fork-Modus gestartete Subagents im Hintergrund laufen lässt, in meinem Headless-Run lief aber ein general-purpose-Kind bei eingeschaltetem Fork-Modus im Vordergrund. Verlass dich also weder auf die Regel noch auf die Umgebungsvariable, um das Scheduling festzulegen: Prüf es in deinem eigenen Stream.

Dann pinne die Claude-Code-Version im CI, damit so ein Standard dann kommt, wenn du ihn übernehmen willst. Das ist genau die Verteidigung, die ein Modell hinter einem unveränderten Namen nicht zulässt. Das CLI um das Modell herum ist weiterhin ein gewöhnliches versioniertes Tool, und das kannst du pinnen.

Was solltest du zuerst prüfen, wenn Claude Code 2.1.302 im CI ankommt?

Einen echten Run jedes wichtigen Jobs, komplett gelesen. Zähl die result-Events, schau dir die subagent_type-Werte an und prüf dann die eigenen Abnahmekriterien des Jobs. Zwei unveränderte Zählungen in einem Run zeigen nur, dass sich dieser Run nicht geändert hat, nicht, dass der Job in Ordnung ist. Wenn ein Fork in einem Job auftaucht, der seinen Input frisch ansehen soll, setz CLAUDE_CODE_FORK_SUBAGENT=0 auf diesen Step, nicht global.

Dann prüf deinen Permission Mode. Laut Release Note starten claude -p- und SDK-Sessions ohne konfigurierten Permission Mode bei Drittanbietern wie Bedrock, Vertex und Gateways jetzt im Modus default, „with telemetry off or on a fresh install“; --permission-mode auto behält auto bei. Release 2.1.285 hatte solche Sessions auf auto umgestellt, ein Job, der in den letzten drei Wochen ohne expliziten Modus eingerichtet wurde, hat sich also womöglich auf auto verlassen, ohne es zu wissen. Das habe ich nicht getestet. Setz den Modus explizit, dann stellt sich die Frage nicht.

Angeboten heißt nicht benutzt. Das Release hat geändert, was dein Headless-Run tun darf, und ein Blick in seinen Stream zeigt dir, was er getan hat.

01 / Diskussion

Hier gibt es keine Kommentarspalte. Diskussionen laufen auf X.

Max Nardit@mnardit

02 / Weitere Artikel