Max Nardit
← 記事
9 mindev

claude -p での Claude Code フォークサブエージェント:2.1.302 がヘッドレス実行で変えたこと

提供されることと使われることは別です。ヘッドレス実行のモデルはフォークを一度も選ばないかもしれず、公式ドキュメントはまだ古いデフォルトを説明しています。パイプラインがどのサブエージェントを使ったかの確認方法、バックグラウンドのフォークがパーサーに2つの回答を渡す理由、そして無効にする価値がある場面を解説します。

目次

10月16日にリリースされた Claude Code 2.1.302 には、無人で動くすべての実行に関わる1行が含まれていました。フォークサブエージェント、つまり空のコンテキストではなく親の会話全体を持って始まるサブエージェントタイプが、claude -p、Agent SDK、デスクトップアプリ、VS Code でデフォルト有効になったのです。このリリース以前、ヘッドレス実行でフォークを使うには CLAUDE_CODE_FORK_SUBAGENT=1 で明示的に有効にする必要がありました。CI ジョブや cron スクリプト、SDK サービスが今週アップデートされたなら、その Agent ツールは先週デフォルトでは提供していなかったタイプを提供しています。設定のどこにもそれを示す変更はありません。

厄介なのは、デフォルトが変わった場所です。ヘッドレス実行は、入力を先入観なく見るべきジョブを置く場所です。CI のレビュアー、採点役、呼び出し側が期待したものではなくコードにあるものを見つけるべき検索パスなどです。フォークは、呼び出し側の前提をそのまま持ち込むために作られたサブエージェントタイプです。つまりこの変更は、コンテキストを引き継ぐことが最も裏目に出やすいモードに直撃しています。しかも公式のサブエージェントのページには、まだ古いデフォルトが有効だと書かれています。こうしたジョブで何が変わるのかを確かめるため、2.1.302 をローカルで動かしてみました。

Claude Code 2.1.302 はフォークサブエージェントの何を変えたのか

ターミナルの外でも subagent_type: "fork" をデフォルトで使えるようにしました。リリースノートには、フォークサブエージェントは "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." とあります。

ドキュメントによると、フォークはメインセッションと同じ指示、ツール、モデル、メッセージ履歴のすべてを受け取り、親のプロンプトキャッシュを共有します。そのため、同じコンテキストが必要な作業では新規のサブエージェントより安く済みます。2.1.285 以降は親のパーミッションモードで動作します。フォークがさらにフォークを生成することはできません。

サブエージェントのページはこの変更に追いついていません。「Turn fork mode on or off」のセクションには今も、Claude Code は "leaves it off by default in non-interactive mode with -p and in the Agent SDK" と書かれており、ページの別の箇所でも同じことを繰り返しています。ドキュメントを根拠に判断すると、自分のパイプラインは影響を受けないと結論づけてしまいます。ただし、そのページにある環境変数の意味は今も正しいです。1 は -p と SDK でもフォークモードを有効にし、0 はあらゆる種類のセッションで無効にします。

claude -p の Claude Code フォークサブエージェントは会話を引き継ぐのか

引き継ぎます。プロンプトに単語を仕込み、フォークを指定して、何も読まずにその単語を答えるようフォークに頼んでみます。

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

バイパスのフラグを付けているのは、無人実行であり、フォークも何も読まないからです。また、筆者の実行では自分用のプラグインとエージェントが読み込まれていたので、表示されるエージェント一覧は標準状態ではなく各自の環境のものだと考えてください。2.1.302 では、フォークは PERSIMMON と答えました。同じコマンドの前に CLAUDE_CODE_FORK_SUBAGENT=0 を付けると、Agent ツールはそのタイプを拒否しました。

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

モデルはこのエラーをそのまま回答として返し、最終結果は is_error: false、プロセスは終了コード 0 で終わりました。つまり、フォークタイプを明示的に指定しているプロンプトやスキルは、無効化すると何の警告もなく壊れます。ビルドは失敗せず、間違った回答が返るだけです。この変数を設定するなら、先にプロンプト内でタイプ名を検索してください。

ヘッドレス実行で Claude Code は自分からフォークを選ぶのか

筆者のテストでは選びませんでした。フォークモードを有効にし、中立的なプロンプト("use the Agent tool exactly once to delegate this subtask")を与えたところ、Haiku は general-purpose を名指しで要求しました。フォークモードが無効のときと同じタイプです。1つのモデルでの1回の実行は、あなたの環境について何も証明しません。「デフォルト有効」とはフォークタイプが提供されるという意味です。実際に使われるかはモデルとプロンプト次第なので、予測するより確認してください。

claude -p の実行がどのサブエージェントタイプを使ったかを確認する方法

セッション開始時のイベントではなく、stream-json の出力で確認します。init イベントにはセッションで使えるエージェントが並びますが、2.1.302 ではフォークモードを有効にしても fork はその一覧に載りませんでした。何かがフォークを要求したときに初めて現れます。手早く確認するなら次のとおりです。

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

どちらも最初の目安であって、証拠ではありません。1つ目の grep は Agent の呼び出しに加えてバックグラウンドタスクのライフサイクルイベントにも一致し、表示されるのは要求された内容です。拒否されたフォークもここに出てくるので、2つ目の grep でその拒否を拾います。フォークが実際に動いたと確かめるには、そのツール呼び出しに対応する task_started イベントと、対になる完了通知を探してください。子が何を受け取ったかを見たい場合、サブエージェントのトランスクリプトは ~/.claude/projects/ の下、そのセッションの subagents/ フォルダにあります。

claude -p の Claude Code フォークが結果を2つ返す理由

フォークがバックグラウンドで動き、claude -p はもともとバックグラウンドの作業をもう1ターン進める理由として扱うからです。筆者の2回の実行ではどちらも、ツールの結果が "Async agent launched successfully" となり、モデルはフォークがまだ実行中だと返答して、そのターンが終わりました。プロセスは開いたままで、フォークが完了すると、セッションは2ターン目に入って PERSIMMON を伝えました。ヘッドレスのドキュメントは、バックグラウンドのサブエージェント全般についてこの挙動を説明しています。実行は作業の完了を待ち、"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." とあります。

出力形式ごとに見えたものは次のとおりです。

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

stream-json で最初の result イベントを取る、あるいはそこで読むのをやめるパーサーは「still running」を受け取り、ジョブが終わったと判断します。テキスト出力を扱う側には、途中経過の文が回答にくっついて届きます。イベントの数を数えてください。

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

1ターンだと思っていたジョブでこれが 2 を返したら、ストリームを最後まで読んで最後の result を取るか(ドキュメントでは最終行になると保証されています)、json に切り替えてください。フォークを無効にして消えるのは、このバックグラウンド作業の原因の1つだけで、すべてではありません。

Claude Code のパイプラインでフォークサブエージェントを無効にすべき場面

価値が「まっさらな視点」にあるヘッドレスジョブではすべて無効にし、子が親の作業を引き継ぐ場面では有効のままにします。レビュアーや採点役を動かす意味は、作者のフレーミングを共有しないことにあります。そのジョブのコンテキストをフォークすると、呼び出し前にジョブが読んだものがすべて子に渡ります。後で捨てた推測も含めてです。この議論の詳しい版はフォークしたサブエージェントはすでに知りすぎている理由に書きました。引き継いだトランスクリプトからは、どの結論がまだ生きていてどれがもう死んでいるかを見分ける手段がありません。フォークが本領を発揮するのは継続の場面、つまり作りかけの状態そのものが作業である場合です。

大まかな切り替えはジョブ単位か CI のステップ単位です。

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

より細かい方法は deny ルールで、プロジェクトの .claude/settings.json(ローカルで上書きするなら .claude/settings.local.json)の permissions ブロックにマージします。

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

ドキュメントによると、これはフォークモードを有効にしたまま Claude がフォークを生成するのを止めるものです。このルールは試していません。ドキュメントにはフォークモードでは生成されたサブエージェントがバックグラウンドで動くともありますが、筆者のヘッドレス実行では、フォークモード有効の状態で general-purpose の子がフォアグラウンドで動きました。ですから、ルールにも環境変数にもスケジューリングの決定を期待せず、自分のストリームで確認してください。

そのうえで CI では Claude Code のバージョンを固定し、こうしたデフォルト変更を自分が選んだタイミングで取り込めるようにします。これは名前の変わらないモデルでは取れない防御策そのものです。モデルを包む CLI は今もふつうのバージョン管理されたツールで、そうしたツールは固定できます。

Claude Code 2.1.302 が CI に入ったら最初に確認すること

重要なジョブそれぞれを実際に1回動かし、出力を最後まで読みます。result イベントを数え、subagent_type の値を見て、それからジョブ自身の合格基準を確認してください。1回の実行で2つの数字が変わらなかったとしても、その実行が変わらなかったことしか示さず、ジョブに問題がないことの証明にはなりません。入力を新鮮な目で見るべきジョブにフォークが現れたら、グローバルではなくそのステップに CLAUDE_CODE_FORK_SUBAGENT=0 を設定します。

次にパーミッションモードを確認します。リリースノートによると、パーミッションモードが設定されていない claude -p と SDK のセッションは、Bedrock、Vertex、ゲートウェイといったサードパーティプロバイダーでは "with telemetry off or on a fresh install" の場合に default モードで始まるようになりました。--permission-mode auto を指定すれば auto のままです。2.1.285 ではこうしたセッションが auto に変わっていたので、この3週間にモードを明示せずに設定したジョブは、知らないうちに auto に頼っていた可能性があります。これは試していません。モードを明示すれば、この問題は起きません。

提供されることと使われることは別です。このリリースが変えたのはヘッドレス実行に許されることで、そのストリームを一度読めば、実際に何をしたかがわかります。

01 / ディスカッション

コメント欄はありません。議論は X で行っています。

Max Nardit@mnardit

02 / ほかの記事

  1. AI エージェントにとっての MCP と API:API カタログはディレクトリであり、選択を決めるのは間に誰が入るか

    API カタログは MCP サーバーと競合しません。MCP サーバーを掲載できる側です。そこを押さえると、MCP か OpenAPI で記述した HTTP API かの選択は、ホストアプリケーションがモデルのためにサービスを組み込むかどうか、そして誰かがすでにそのサービスを組み込むと決めているかどうかにかかってきます。

  2. Claude Code Review が GA に:周囲のレビュー層と、Team プランなしで真似できる層

    Claude Code チームのプロセスを支えているものの大半は GitHub の設定と文章化されたルールで、どのプランでも動きます。真似できないのは、ルールをどこから読むかです。マネージドサービスはプルリクエストが触れられないブランチからルールを読みます。ローカルのレビューはディスク上にあるものを読み、レビュー対象の変更が書き換えたばかりのルールファイルも例外ではありません。

  3. Claude API の OpenTelemetry:トークンを二重に数えずに SDK と API をトレースする

    スパンの入力トークン数にはキャッシュの読み込みと書き込みがすでに含まれています。そのため、Messages API を知っている人が自然に書くクエリでは、キャッシュされたプレフィックスが二重に数えられます。ローカルのモックで試した SDK 側のセットアップ、ドキュメントに沿った Console のデスティネーション設定、そしてキャッシュされていない入力に戻すための引き算をまとめます。