先に結論です。サブエージェントは、親の会話の続きではなく、別の system prompt と別の prefix cache を持つ、別のリクエスト列です。 起動時に載るのは、定義ファイルの本文(system prompt)、中央が書いた task prompt、CLAUDE.md の全階層、親のセッション開始時の git の状態、定義の skills で指定した本文の 5 つで、親の会話・親の tool_result・auto memory は載りません。model は 4 段階の順で決まり、未指定なら親と同じになります。ツールは 2 段階のフィルタで減り、どの体も AskUserQuestion を持たないので人に聞けません。深さは 3 階層、同時に走れるのは 20 体で、総数の上限は製品にはありません。戻るのは最終メッセージだけで、前景なら tool_response にトレーラー(totalTokens など)が付き、background なら後のターンに完了通知として届きます。本編が 3 作とも例外にした相手の、例外である理由はここにあります。
もう 1 つの結論は、測定法の側です。本編が 3 作で 5 回やった「2 体に投げて比べる」は、サブエージェントの隔離を測定器として使う 1 つの型です。自分のセッションで測れないのは、自分が既に読んでいるからで、サブエージェントは読んでいない体を毎回新しく作れます。ただし白紙ではありません。CLAUDE.md は載り、Explore と Plan だけは載らず、model は親を継ぎ、thinking の設定も親を継ぎます。何が載っている体で測ったかを書かないと、測定は再現できません。この章の後半は、その書き方です。
付録 B.5 は、サブエージェントに載るものの表と、キャッシュが別である理由を書きました。この章は B.5 が「付録 E に置く」と言った残り —— 起動から戻りまでの 1 手ずつ、model の解決順、ツールのフィルタ、深さと並列、戻るものの形、fork と teammate、定義ファイルの読まれ方、hook から見える欄 —— を、公式ドキュメントと、私の手元に残っていた 102 本の transcript で確かめながら埋めます。
E.1 起動から戻りまで —— 1 体の一生
flowchart TB
subgraph P["親の会話(中央)"]
p1["中央が Agent ツールを呼ぶ<br/>prompt / description / subagent_type / model / name"]
p2["PreToolUse hook(matcher: Agent)<br/>tool_input を読んで exit 2 で止められる"]
p3["tool_result が末尾に足される<br/>前景: 最終テキスト + トレーラー<br/>background: agentId と output_file だけ"]
p4["後のターンに完了通知(background)"]
end
subgraph S["サブエージェント(別のリクエスト列)"]
s0["SubagentStart hook<br/>止められない。additionalContext を先頭に差せる"]
s1["system prompt = 定義の本文 + 環境情報<br/>(Claude Code 本体の system prompt ではない)"]
s2["最初の user メッセージ = task prompt<br/>+ CLAUDE.md の全階層 + git の状態 + preload したスキル本文"]
s3["tool_use ⇄ tool_result の繰り返し<br/>settings の PreToolUse / PostToolUse も発火(agent_id 付き)"]
s4["SubagentStop hook<br/>agent_transcript_path / last_assistant_message"]
end
p1 --> p2 -->|"exit 0"| s0 --> s1 --> s2 --> s3 --> s4
p2 -.->|"exit 2: 起動されない<br/>stderr が中央に届く"| p3
s4 -->|"前景"| p3
s4 -->|"background"| p4
s3 -.->|"transcript<br/>~/.claude/projects/{project}/{session}/subagents/agent-{id}.jsonl"| T[("別ファイル")]
図の各段で、誰が書き、どこで観測できるかを並べます。
| 階層 | 誰が書くか | 観測できる場所 |
|---|---|---|
| Agent ツールの入力 | 中央(AI) | 親の transcript の tool_use(input)/ PreToolUse hook の tool_input |
| 起動の可否 | PreToolUse hook(あなたが置いたスクリプト) | hook の exit code と stderr。止まれば親の会話に stderr が出る |
| system prompt | 定義ファイルの本文(あなた)+ Claude Code が足す環境情報 | transcript には残らない(付録 A.7 と同じ) |
| 最初の user メッセージ | 中央が書いた task prompt + Claude Code が足す CLAUDE.md など | transcript の先頭の user 記録には task prompt だけが残る(後述) |
途中の tool_use / tool_result | サブエージェント | サブエージェントの transcript。親には 1 つも届かない |
| 最終メッセージ | サブエージェント | 前景: 親の tool_result / background: 完了通知。SubagentStop の last_assistant_message |
| トレーラー | Claude Code | 前景の tool_response(totalTokens / totalToolUseCount / resolvedModel など) |
「別のリクエスト列」の意味は、付録 A.1 の 3 つの部分で言うと、system prompt 層も project context 層も親とは別物だということです。会話層は task prompt から始まります。親の会話層に足されるのは、Agent ツールの tool_use と、その tool_result の 2 つだけです。だから親の prefix cache は無傷で(付録 B.4)、サブエージェントは自分の prefix を自分で温めます(B.5)。
E.2 初期コンテキスト —— 載るもの・載らないもの・transcript で見えるもの
公式ドキュメント(Subagents「What loads at startup」)が列挙する初期コンテキストは 6 項目です。付録 B.5 の表を、条件つきで書き直します。
| 載るもの | 条件・例外 |
|---|---|
| system prompt | 定義の本文(.claude/agents/*.md の frontmatter の下、または --agents の prompt)+ 作業ディレクトリなどの環境情報。組み込みの型(Explore / Plan / general-purpose / claude)は Claude Code が持つ既定の本文 |
| task message | 中央が書く委任のプロンプト。あなたが @ で指名しても、プロンプトを書くのは中央で、あなたのメッセージがそのまま渡るのではない |
| CLAUDE.md | 親が読んだ全階層(~/.claude/CLAUDE.md・プロジェクト・CLAUDE.local.md・管理ポリシー)。Explore と Plan は読まない。これを変える欄は無い |
| git の状態 | 親のセッション開始時のスナップショット。git リポジトリでないとき、includeGitInstructions: false のときは無い。Explore と Plan は読まない |
| preload したスキルの本文 | 定義の skills に書いたもの。組み込みの型は preload しない。disable-model-invocation: true のスキルは preload できない |
| 兄弟の一覧 | main と、名前を持つ他のエージェントを列挙した system reminder。ツールに SendMessage があり、名前つきの他のエージェントが 1 体以上いるときだけ。起動時のスナップショット(v2.1.206 以降) |
載らないものも公式が明記しています。親の会話の履歴、親が呼んだスキル、親が読んだファイル、output style、auto memory の 5 つです。context window の大きさも、親ではなく自分の model で決まります。第 1 部 第 5 回の「メモリも起動ルールも 1 行も渡っていなかった」は、この 5 つのうち auto memory と会話の 2 つです。
hook で足せる 1 つがあります。SubagentStart の hookSpecificOutput.additionalContext は、サブエージェントの会話の先頭、最初のプロンプトの前に文字列を差し込みます(付録 C.6)。載らないもののうち「メモリの抜粋」だけは、この経路で機械的に渡せます。本編はこの経路を使わず、requires のファイルを task prompt で渡す形にしました。差し込みは全部の体に同じものが載り、requires は用途ごとに違うものを渡せる、という違いです。
transcript の先頭には、task prompt しか残らない
手元の 2 つの clone に、subagents/agent-*.jsonl が 102 本残っていました(2026-08-01〜09-04、Claude Code 2.1.220〜2.1.259)。先頭の記録は全部 type: "user" で、isSidechain: true、agentId、sessionId を持ち、message.content は中央が書いた task prompt の文字列だけでした。
| 観測(102 本) | 値 |
|---|---|
| 先頭の user 記録の文字数 | 最小 262 / 中央値 1,222 / 最大 3,092 |
| 先頭の記録に CLAUDE.md の本文が入っていた本数 | 0(後で cat CLAUDE.md した tool_result に入っていたのが 4 本) |
type: "system" の記録(compact_boundary など) | 0 |
付録 A.7 が親のセッションで見たのと同じで、CLAUDE.md も system prompt も transcript には残りません。「載っていた」ことを transcript で証明することはできず、目印を置いて AI に報告させる(E.13 の 1)か、SubagentStart の hook で記録するしかありません。
transcript の attachment 記録に、Claude Code が足したものが見える
一方で、transcript には type: "attachment" という第 3 の記録があり、Claude Code が会話に足したものの一部が種類つきで残ります。102 本で見つかった種類は 7 つです。
attachment.type | 本数(記録数) | 中身 |
|---|---|---|
skill_listing | 102 本(102) | スキルの一覧(名前 + description)。5,625〜8,497 字 |
deferred_tools_delta | 102 本(126) | ToolSearch で後から引けるツール名の増減。addedNames に EnterWorktree / ExitWorktree / Monitor / SendMessage / TaskStop / WebFetch / WebSearch |
total_tokens_reminder | 24 本(151) | <total_tokens> の残量 |
auto_mode | 20 本(20) | auto mode の状態(bypass: false など) |
agent_listing_delta | 16 本(16) | 起動できるエージェントの型の一覧。全部 claude / claude-code-guide / Explore / general-purpose / Plan / statusline-setup の 6 つ |
nested_memory | 4 本(4) | isolation: worktree で走った体が、worktree の中の CLAUDE.md を下位ディレクトリの指示ファイルとして読んだ記録 |
read_truncation_notice | 3 本(3) | Read が 25,000 トークンで切られた告知 |
3 つ、読み取れます。一つ、スキルの一覧は全部の体に載っています。 公式が「skills に無いスキルも Skill ツールで呼べる」と書く根拠がこれで、付録 D.5 の「一覧の予算」はサブエージェントでも同じ分だけ使われます。二つ、agent_listing_delta がある 16 本は、その体が Agent ツールを持っていた(深さの上限に達していない)体です。ただし 102 本のどれも Agent を呼んでいません(E.4)。三つ、nested_memory の 4 本は、worktree で走らせた体が CLAUDE.md を「もう一度」読んだ記録です。起動時に載る CLAUDE.md は親の階層のもので、worktree の中の同名ファイルは別のパスなので、そこにあるファイルを読んだ時点で下位ディレクトリの指示ファイルとして再び載ります(付録 D.2 の nested_traversal)。内容が同じでも 2 回ぶん載ります。
E.3 model の解決順 —— 4 段階と、その外側
サブエージェントの model は、次の順で最初に見つかったものになります(v2.1.251 以降)。
| 順 | 出どころ | 誰が書くか |
|---|---|---|
| 1 | Agent ツール呼び出しの model パラメータ | 中央(AI)。本編の hook が「無ければ止める」のはここ |
| 2 | 定義ファイルの frontmatter model(inherit は親と同じ) | あなた |
| 3 | 環境変数 CLAUDE_CODE_SUBAGENT_MODEL(alias か model ID) | あなた(settings の env など) |
| 4 | 親の会話の model | — |
flowchart LR
A["呼び出しの model"] -->|無い| B["定義の model"] -->|無い| C["CLAUDE_CODE_SUBAGENT_MODEL"] -->|無い| D["親の model"]
A -->|有る| R["候補"]
B -->|有る| R
C -->|有る| R
D --> R
R --> AM{"availableModels の許可"}
AM -->|許可| M["起動。resolvedModel に記録"]
AM -->|family alias が不許可| M2["同じ family で許される最新版"]
AM -->|それ以外が不許可| M3["親の model"]
F["CLAUDE_CODE_SUBAGENT_MODEL_FORCE=1"] -.->|"1〜2 を無視。fork と model: inherit のスキルは除く"| C
順の外側に、4 つ書いておきます。
- Explore の上限。 Explore は親の model を継ぎますが、Claude API では Opus を上限にします(v2.1.198 以降。それ以前は常に Haiku)。親が Opus より上の階層でも Explore は Opus で走り、親が Sonnet か Haiku なら同じ model です。他のプロバイダでは親をそのまま継ぎます。
Exploreという名前の定義を自分で置けば上書きでき、model: haikuと書けば戻せます - 強制。
CLAUDE_CODE_SUBAGENT_MODELは既定であって上書きではありません。1・2 段目が有れば負けます。全部の体を 1 つの model にしたければCLAUDE_CODE_SUBAGENT_MODEL_FORCE=1を足します(v2.1.257 以降)。そのとき例外は fork と、model: inheritのスキルの 2 つです - 置換。 組織の
availableModelsが候補を許さないとき、family alias(opusなど)なら同じ family の許される最新版に、それ以外なら親の model に置き換わります(v2.1.222 以降)。対話セッションでは警告が出ます - 記録。 実際に走った model は
/tasksの行(v2.1.242 以降)と、前景のtool_response.resolvedModel(v2.1.174 以降)に出ます。途中で入れ替わったときはmodelsUsedに順に並びます(v2.1.212 以降)。resume したときも 1 段目の値が維持されます(v2.1.211 以降)
model と一緒に決まるものが 2 つあります。extended thinking は親の設定を継ぎ(v2.1.198 以降)、体ごとには設定できません。effort は定義の effort 欄で上書きでき、無ければ親を継ぎます。同じ課題を opus と sonnet で比べるとき(第 3 部 第 6 回)、変えたのは 1 段目だけで、thinking と effort は両方の体で親と同じでした。
手元の 107 回の起動
親の transcript から Agent ツールの tool_use を抜き出すと 107 回あり、1 段目(呼び出しの model)が書かれ始めたのは 2026-08-28 で、それまでの 51 回は全部空でした。8 月 31 日の 23 回は、空が 14 回、sonnet が 6 回、haiku が 3 回です。空の 14 回はそのまま 4 段目に落ち、親の Opus で走りました。同じ日の transcript 19 本の message.model は、Opus 12 / Sonnet 5 / Haiku 1(残り 1 本は起動直後に終わっていて model の記録が無い)です。用途カタログの hook が入った 9 月 2 日以降の 32 回に、空は 0 回です。
102 本全体では、Opus 70 / Sonnet 16 / それより上の階層 13 / Haiku 1 でした。第 1 部 第 5 回の「モデル未指定は親の高価なモデルを継承」は、4 段階の表で言うと 1〜3 段階が全部空だった、という状態です。
E.4 ツール —— 2 段階のフィルタと、allow / deny
サブエージェントは親の会話で使えるツール(組み込み + MCP)を継ぎ、2 段階のフィルタで減らされます。fork だけは両方を素通りして親と同じ集合を持ちます。
第 1 フィルタは全部の体にかかり、tools に書いてあっても外します。
| 外されるツール | 条件 |
|---|---|
Agent | 深さの上限にいる体。fork では残るが呼ぶとエラー |
AskUserQuestion | 常に |
EndConversation | 常に(メインの会話しか終われない) |
EnterPlanMode | 常に |
ExitPlanMode | permissionMode: plan の体以外 |
ScheduleWakeup / TaskOutput / WaitForMcpServers / Workflow | 常に |
第 2 フィルタは background で走る体にかかります。MCP ツールは全部残り、組み込みは次の 19 個だけが残ります。Read Grep Glob Bash PowerShell Edit Write NotebookEdit WebFetch WebSearch TodoWrite Skill ToolSearch EnterWorktree ExitWorktree Monitor TaskStop SendMessage Artifact。それ以外の組み込みは、継いだものでも tools に書いたものでも黙って外れます。同じ定義が前景と background で違うツール集合になる、ということです。
AskUserQuestion が第 1 フィルタにあることは、本編の設計に 1 つ効いています。サブエージェントは人に聞けません。 第 1 部 第 5 回の「止まったら中央に返して、人間に差し戻す」は、サブエージェントが自分で人に確認する経路が無いから、中央を経由する形にしか書けない、ということでもあります。
tools(allowlist)と disallowedTools(denylist)の両方があるときは、disallowedTools を先に当て、残りに tools を当てます。両方に書いたツールは消えます。tools の全項目が実在しないと、v2.1.208 以降は起動自体が失敗して項目名を返します(それ以前は無ツールで起動して空の結果が戻りました)。Agent(worker, researcher) のような型の allowlist は claude --agent でメインを走らせるときだけ効き、サブエージェントの定義に書いても括弧の中は無視されます。特定の型を使わせないのは permissions.deny の Agent(Explore) です。
用途カタログの investigation が agent_type: [Explore] に固定されているのは、このフィルタの上に立っています。Explore は Write と Edit を持たない組み込みの型なので、requires が空の用途でも「調査」と名乗るだけでは書けません。逆に言うと、subagent_type を general-purpose に変えれば書けます。カタログの判定 3〜4 に subagent_type の判定が足されているのは、その抜け穴を塞ぐためです(第 2 部 第 4 回の表には無く、手元の hook にはあります)。
手元の 102 本が呼んだツール
| ツール | 回数 |
|---|---|
Bash | 3,026 |
Read | 375 |
Edit | 131 |
Write | 16 |
WebFetch | 14 |
WebSearch | 13 |
ToolSearch | 1 |
Agent | 0 |
1 本あたりの tool_use は最小 0 / 中央値 24 / 最大 177 です。第 2 部 第 4 回の「1 体あたり 68〜97 回」は 8 月 31 日の翻訳の体の数で、transcript で数え直すと 40〜106 回でした(記事の数字は完了の報告があった体の値で、transcript には途中で終わった体と 2 回目の起動も含まれます)。Agent が 0 回なのは、16 本が agent_listing_delta を持っていた(呼べた)のに呼ばなかった、ということです。深さの上限はこの 102 本では一度も試されていません。
E.5 深さ・並列・総数
| 上限 | 既定 | 変え方 | 版 |
|---|---|---|---|
| 深さ(メインの下に何層まで) | 3 | CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH。1 で入れ子を止める | v2.1.219 以降。v2.1.172〜216 は固定 5、v2.1.217〜218 は既定 1 |
| 同時に走る数 | 20 | CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS。超えると Concurrent subagent limit reached で失敗し、再試行しないよう AI に伝わる | v2.1.217 以降。ultracode のセッションは無制限 |
| セッションの総数 | 無し | — | — |
深さの上限にいる体からは Agent が外されるので、その体は自分で全部やって 1 つの要約を返します。並列の枠は Agent ツールの起動だけを止め、/subtask の fork は枠を使うが止められず、完了した体の resume は枠を見ずに新しい枠を取ります。workflow と agent team の体は別の上限です。
本編の用途カタログの上限(max_parallel 5 / max_total 20 / session_total_checkpoint 25)は、この製品の上限の内側にある別の上限です。並列 5 は製品の 20 の内側で、総数 20 と関所 25 は製品に無いものを hook が足しています。製品の並列は「同時に走っている数」で数えるので、本編が第 1 部 第 5 回の後で SubagentStop を数えるように直したのは(付録 C.7)、製品と同じ数え方に寄せたということです。
E.6 戻るもの —— 最終メッセージと、トレーラー
前景で走った体が終わると、Agent ツールの tool_result に最終メッセージのテキストが入り、PostToolUse hook の tool_response にはそれとトレーラーが入ります。
tool_response の欄 | 中身 | 版 |
|---|---|---|
status | "completed"(前景)/ "async_launched"(background) | — |
agentId | この走行の識別子。resume の宛先 | — |
content | 最終メッセージのテキストブロック | — |
resolvedModel | 起動時の model。tool_input.model と違うことがある | v2.1.174 以降 |
modelsUsed | 途中で入れ替わったときの順 | v2.1.212 以降 |
totalTokens | 最後の API リクエスト 1 回分の input + output + cache の合計。走行全体の合計ではない | — |
totalDurationMs | 走行の実時間 | — |
totalToolUseCount | ツール呼び出しの回数 | — |
usage | 最後のリクエストの内訳(input_tokens / output_tokens / cache_creation_input_tokens / cache_read_input_tokens) | — |
手元の親の transcript に前景の完了が 2 件残っていて、1 件の toolUseResult はこうでした。agentType: "Explore"、resolvedModel: "claude-opus-5[1m]"、totalToolUseCount: 61、totalDurationMs: 271962、totalTokens: 115771、usage は input 2 / cache_creation 2,762 / cache_read 104,528 / output 8,479。4 つを足すと 115,771 で、totalTokens は最後の 1 リクエストの大きさだと数字で確かめられます。手元の版には公式の表に無い agentType と toolStats(bashCount / editFileCount / linesAdded など)も付いていました。
background で起動したときの tool_result は違います。走り出した時点で返り、status: "async_launched"、agentId、description、prompt、outputFile(サブエージェントの transcript の JSONL の場所)、resolvedModel を持ち、usage はありません。結果は後のターンに完了通知として届きます。v2.1.211 以降、中央は通知が来るまで結果を報告せず、聞かれれば「まだ走っている」と答えます。それ以前は、終わっていない体の結果を報告してしまうことがありました。
どれだけ捨てられているか
戻るのが最終メッセージだけだ、という文を数字にします。102 本の transcript のファイルサイズは中央値 224,711 バイト(最大 961,515 バイト)で、最終メッセージのテキストは中央値 2,570 字(最大 33,928 字)でした。親に届くのは、体が読み書きした量の 1〜2% です。第 1 部 第 5 回の「一度だけ、読む」で開くのは残りの 98% で、それは ~/.claude/projects/<project>/<session-id>/subagents/agent-<id>.jsonl にしかありません。親の圧縮の影響は受けず(別ファイル)、cleanupPeriodDays(既定 30 日)で消えます。
戻る前に、走査される
v2.1.210 以降、Claude Code は最終報告を中央が読む前に走査します。削除も言い換えもせず、2 つだけ変えます。<system-reminder> のようなタグや Human: Assistant: で始まる行など、Claude Code 自身の出力に似せた文字列にバックスラッシュを差して平文にすること。タグの模倣や bypassPermissions --dangerously-skip-permissions のような権限の語があるとき、先頭に [harness: subagent output matched instruction-shaped pattern(s): で始まる 1 行を足すことです。サブエージェントは中央が見ていないファイルや Web を読むので、そこに埋まった指示が報告を経由して中央へ届く経路を、目立たせる仕組みです。走査は悪意を判定せず、報告の指示で中央が呼ぶツールは通常の permission を通ります。
途中で終わったとき
maxTurns で止まった体の出力は partial の目印つきで戻り(v2.1.246 以降)、agent ID を返す種類なら「続きは message で」と添えられます。API エラー(usage limit・overload・server error)で切れた体は、v2.1.199 以降、エラーを「結果」として返すのではなく失敗として報告します。前景でテキストを出していれば部分出力 + 切れた旨、ツール呼び出しだけなら Agent terminated early due to an API error。background なら失敗の目印と最後の出力が通知に入ります。fallback model chain があれば切り替えて続きます。
resume —— 「もう一度」は新しい体ではない
Agent ツールの呼び出しは毎回新しい体を作ります。前の体の続きをやらせるには、中央が SendMessage の to に agent ID か名前を書いて送ります。届いた体は会話の履歴(前の tool_use と tool_result の全部)を持ったまま background で再開します。Explore と Plan は one-shot で agent ID を返さないので resume できません。
規則が 3 つあります。名前は同じ体を指し続けるか確かめられる(v2.1.199 以降。同じ名前で別の体が起動していれば拒否され、どの体に届くかがエラーに書かれる)。起動した側からの message は通常の指示として扱う(v2.1.198 以降。途中の軌道修正も含む)。どの体からの message も、permission の承認にはならず、permission・CLAUDE.md・設定を変えられない。 承認できるのは permission の仕組みと、あなたの入力だけです。
用途カタログの hook が SendMessage を「起動済みの名前への送信 = 再開(リトライ)」として必ず一度止めるのは(第 1 部 第 8 回・付録 C.7)、この resume の経路を見ています。第 1 部 第 8 回の「失敗した 2 体を確認なしで再開してセッション上限に当たった」は、2 体の transcript の続きに、もう一度ぶんの往復が積まれた、ということです。
E.7 fork と teammate —— 隔離を捨てる 2 つの形
fork は、親の会話を丸ごと継ぐサブエージェントです。
| fork | fork でないサブエージェント | |
|---|---|---|
| コンテキスト | 親の会話の全部 | 白紙 + task prompt |
| system prompt とツール | 親と同じ(2 段階のフィルタを通らない) | 定義ファイルから。background なら第 2 フィルタ |
| model | 親と同じ | 定義の model(E.3 の順) |
| permission | 端末に prompt が出る | background なら親のセッションに prompt が出る |
| prefix cache | 親と共有 | 別 |
親と system prompt もツール定義も同じなので、fork の最初のリクエストは親のキャッシュを読みます(付録 B.5)。fork のツール呼び出しは親の会話に入らず、戻るのは最終結果だけなので、親のコンテキストは膨らみません。fork は fork を作れません。中央が isolation: "worktree" を付けて fork を起動すると、編集は別の worktree に書かれます。
fork mode は対話セッションで既定 on(v2.1.232 以降。それ以前は CLAUDE_CODE_FORK_SUBAGENT=1)、-p と Agent SDK では既定 off です。on のとき、中央は fork の型を要求でき、型を書かなければ general-purpose になり、中央が起動する体は fork も非 fork も全部 background で走り、Agent ツールから run_in_background パラメータが消えます。あなたは /subtask <task> で fork を自分で起動できます(v2.1.212 以降。v2.1.161〜211 は /fork)。
teammate は、もう 1 つの「隔離を捨てる」形です。CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1 のとき、対話セッションで中央が name を付けて Agent ツールを呼ぶと、その体はサブエージェントではなく teammate として起動します。fork でなく、呼び出しに isolation も無いときです。teammate は別の Claude Code のプロセスで、CLAUDE.md・MCP・スキルを通常のセッションと同じに読み、中央の会話は継がず、spawn prompt を受け取り、mailbox(~/.claude/teams/<team>/inboxes/)で通信します。中央は名前を自分で付けることがあるので、あなたが team を頼んでいなくても team ができます。戻すには環境変数を 0 にします(保存すれば次の起動から効きます)。
手元の実測 —— teammate にはならなかった
私の settings.json は user と project の両方で CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS を 1 にしています。9 月 4 日の 24 体は全部 name 付きで起動しました。公式の条件では teammate になるはずです。ところが 24 本とも subagents/agent-*.jsonl に残り、~/.claude/teams/ は空で、mailbox もありません。transcript の entrypoint は全部 claude-vscode(VS Code 拡張)です。加えて、この付録を書いているセッションでも Agent ツールに run_in_background パラメータが残っていて、fork mode は off でした。
VS Code 拡張の内側が公式の言う「対話セッション」に当たらないのか、別の条件があるのかは、切り分けていません(tmux は入っています)。あなたの手元で「名前を付けた体がどこに残るか」は E.13 の 6 で確かめてください。本編の hook が name を起動の記録の鍵にしているので、teammate として起動した場合に SubagentStop が同じように来るかは、私の手元では未検証です。
E.8 定義ファイル —— frontmatter と、読まれない条件
置き場所は 5 つで、同じ名前なら上が勝ちます。
| 場所 | 範囲 | 優先 |
|---|---|---|
管理設定の .claude/agents/ | 組織 | 1 |
--agents の JSON | そのセッションだけ(disk に残らない) | 2 |
.claude/agents/(作業ディレクトリからリポジトリの根まで、再帰的に) | プロジェクト | 3 |
~/.claude/agents/(再帰的に) | あなたの全プロジェクト | 4 |
plugin の agents/ | plugin が有効なところ | 5 |
識別は name だけで、ファイル名も置いた subfolder も関係ありません(plugin だけ subfolder が my-plugin:review:security のように名前に入ります)。frontmatter の欄は 17 個で、必須は name と description です。
| 欄 | 効き |
|---|---|
name / description | 必須。name は小文字とハイフン。: と先頭の - は不可(v2.1.218 以降)。hook は name を agent_type として受け取る |
tools / disallowedTools | E.4。mcp__<server> の形も可 |
model | E.3 の 2 段目。inherit で親 |
permissionMode | 無ければ親を継ぐ。親が bypassPermissions か acceptEdits なら親が勝つ。親が auto mode なら欄は無視され、classifier が親と同じ規則で見る |
maxTurns | 超えると partial で戻る |
skills | 本文を起動時に preload。disable-model-invocation: true のものは不可 |
mcpServers | 名前参照か inline 定義。inline はその体の間だけ接続。プロジェクトの agent file からの inline は folder の trust が要る(v2.1.238 以降) |
hooks | その体が走っている間だけ。Stop は SubagentStop に読み替え。プロジェクトの agent file は folder の trust が要る(v2.1.218 以降) |
memory | user / project / local。auto memory の一部なので autoMemoryEnabled: false なら効かない |
background | true で常に background |
effort | セッションの effort を上書き |
isolation | worktree で一時 worktree に隔離。変更が無ければ自動で消える |
color / initialPrompt / experimental.cacheTtl | 表示色 / --agent で走らせたときの最初の入力 / prompt cache の TTL(v2.1.248 以降) |
欄のうち 2 つは、載る場所が違います。description は親の側に載り、本文は体の側に載ります。 description は中央が「どの種類の体に委任するか」を決めるための文で、親の system prompt 層のツール定義(Agent ツールの型の一覧。付録 A.3)に入り、毎ターン送られます。本文は E.2 の system prompt で、起動した体だけが読みます。第 2 部 第 4 回が引いた「呼び出しを決めるテキスト」と「振る舞いを決めるテキスト」の線引きは、この 2 つの載る場所の違いです。前者を厚くすると親の毎ターンが重くなり(description の合計 15,000 トークンで警告。E.9)、後者を厚くしても親には 1 トークンも載りません。用途カタログの「description は 1 行、厚いのはコメントと requires」は、親に載るほうを薄く、体に渡すほうを厚くした形です。
plugin の agent では hooks mcpServers permissionMode は無視されます。--agents の JSON は prompt + 上の欄の大半を受け取ります。-p では --append-subagent-system-prompt で全部の体の system prompt の末尾に文字列を足せます(v2.1.205 以降。fork は除く)。
読まれない条件が 5 つあり、どれもセッションには表示されません(--debug の log にだけ出ます)。name が無い(そのディレクトリのドキュメント扱い)/ 先頭行が --- でない(frontmatter 無し扱い)/ name が - で始まるか : を含む / name はあるが description が無い / YAML が parse できない。claude plugin validate .claude/agents で parse エラーは拾えますが、「parse はできるが name が無い」は拾えません(v2.1.233 以降)。
~/.claude/agents/ と .claude/agents/ は監視されていて、編集は数秒で次の起動に反映されます。再起動が要るのは、起動時に無かった agents ディレクトリを新しく作ったとき、--add-dir のディレクトリの中、--disable-slash-commands のセッションの 3 つです。
手元の実測 —— .claude/agents/*.json の 3 本は読まれていない
私のプロジェクトの .claude/agents/ には backend-agent.json / database-agent.json / test-agent.json の 3 本があります(2026-03-15。name description model systemPrompt を持つ JSON)。frontmatter を持たないので「name が無い」に当たり、ドキュメント扱いです。裏付けは 2 つで、102 本の transcript の agent_listing_delta に並ぶ型は組み込みの 6 つだけ、この付録を書いているセッションの起動できる型の一覧も同じ 6 つです。付録 D.10 の「skills/ 直下の .md は読まれない」と同じ轍が、agents にもありました。JSON の形が使えるのは --agents フラグだけです。
もう 1 つ、memory 欄です。私の ~/.claude/settings.json は autoMemoryEnabled: false(付録 D.4)なので、定義に memory: project と書いても、その体は memory の指示も Read / Write / Edit の自動有効化も受け取りません。本編の「サブエージェントにメモリは渡らない」は仕様で、「渡す欄はあるが私の手元では無効」が実測です。
E.9 hook から見えるもの —— C との分担
契約の全体は付録 C に置きました。サブエージェントに関わる欄だけ拾います。
- settings の hook は体の中でも発火します。
PreToolUse/PostToolUseは体のツール呼び出しごとに、親と同じ設定で走ります。入力にagent_id(体の中でだけ付く)とagent_type(--agentか体の中で付く)が足されます。本編がテストの並列実行を止めた判定材料がこれです(付録 C.7 の 2 番。プロンプトの書き方では偽装できません) SubagentStartは止められません。hookSpecificOutput.additionalContextで体の会話の先頭に文字列を差せます。matcher はagent_type(組み込みならExploreなど、自前なら frontmatter のname、plugin ならmy-plugin:reviewer)。ハイフンを含む名前の完全一致は v2.1.195 以降で、それ以前は含む文字列にも当たりますSubagentStopはagent_id/agent_type/agent_transcript_path/last_assistant_message/stop_hook_active/background_tasks/session_cronsを持ちます。transcript_pathは親のもので、体の transcript はagent_transcript_pathです。decision: "block"+reasonは体を止めさせず、reasonを次の指示として体に渡します。exit 2 の stderr も同じ経路です- description の合計が 15,000 トークンを超えると起動時に警告が出ますが、全部読み込まれます
本編の hook(guard_subagent_launch.py)は PreToolUse の Agent|SendMessage と SubagentStop の 2 箇所に同じスクリプトを登録し、hook_event_name で分岐しています。SubagentStop 側は入力の agent_type も agent_id も読まず、session_id で自分のセッションの「走っている最も古い 1 件」に完了の目印を付けるだけです。並列の数だけが要るので、どの体が終わったかは要らない、という設計です(付録 C.7)。
E.10 測定法 —— probe の設計
本編は 3 作で 5 回、「同じ課題を 2 体以上に投げて比べる」をやっています。全部、サブエージェントの隔離を測定器として使っています。
| 本編 | 問い | 体 | subagent_type / model | 正解表 | 仕掛け | 見たもの |
|---|---|---|---|---|---|---|
| 第 2 部 第 1 回 | 却下の一覧は読まれるか | 2 | claude / opus | 一覧の項目(NG2・NG6) | 一覧に触れない相談文 | 断ったか・何を引用したか・隣の項目まで拾ったか |
| 第 3 部 第 1 回 | 検証を頼むと何が変わるか | 2 | claude / opus | 機械で作った 10 行の表 | 10 本目に無い | 正答 / tool_use の回数 / 所要時間 / 報告に照合の痕跡があるか |
| 第 3 部 第 4 回 | 計画書を書けと言わなくても書くか | 2 + 2 | Explore / opus と sonnet | 「計画書を書く」が 5 手に入るか | 実装済みの依頼(書かないはず) | 何手目に置いたか・根拠に何を挙げたか |
| 第 3 部 第 6 回 | model が変わると指示の効きは変わるか | 4 | claude / opus と sonnet × 指示あり・なし | 同上(10 行) | 同上 | 正答 / tool_use / 照合の痕跡 |
| 第 3 部 第 7 回 | 前提を渡すと・問い詰めると何が変わるか | 4 + 4 + 4 | Explore と claude / opus と sonnet | 同上 | 同上 | 10 行目を作るか / 答えを変えるか / 返答の末尾に何が増えたか |
本編の 5 回の probe を、E.10 の末尾の集計スクリプトで親と体の transcript から数え直したものが次の表です。tool_use の回数は本編の表(第 3 部 第 1 回の 8 と 4、第 6 回の 13 と 3、第 7 回の 3・3・2・1 と 5・5・5・4)に全部一致します。
起動の name | 型 | model | tool_use | 分 | 最後のリクエスト(トークン) | 本編 |
|---|---|---|---|---|---|---|
| probe-A-verify / probe-B-plain | claude | opus | 8 / 4 | 1.6 / 0.7 | 38,353 / 34,907 | 第 3 部 第 1 回 |
| probe-C-sonnet-verify / probe-D-sonnet-plain | claude | sonnet | 13 / 3 | 1.0 / 0.4 | 48,682 / 39,853 | 第 3 部 第 6 回 |
| probe-C1-opus / probe-C2-sonnet(前提あり) | claude | opus / sonnet | 3 / 3 | 0.7 / 0.6 | 33,562 / 41,968 | 第 3 部 第 7 回 |
| probe-E1-opus / probe-E2-sonnet(前提なし) | claude | opus / sonnet | 2 / 1 | 0.5 / 0.3 | 30,357 / 34,024 | 第 3 部 第 7 回 |
| probe-angry-opus / probe-angry-sonnet(問い詰め) | Explore | opus / sonnet | 5 / 5 | 0.9 / 0.7 | 22,949 / 21,079 | 第 3 部 第 7 回 |
| probe-push-opus / probe-push-sonnet(怒った押し返し) | Explore | opus / sonnet | 5 / 4 | 0.9 / 0.6 | 17,948 / 23,614 | 第 3 部 第 7 回 |
| probe-plan-opus / probe-plan-sonnet(未実装の依頼) | Explore | opus / sonnet | 10 / 11 | 0.8 / 0.8 | 23,636 / 33,586 | 第 3 部 第 4 回 |
| probe-plan2-opus / probe-plan2-sonnet(実装済みの依頼) | Explore | opus / sonnet | 9 / 10 | 0.9 / 1.6 | 26,386 / 33,833 | 第 3 部 第 4 回 |
「最後のリクエスト」の列は E.6 の totalTokens と同じ量で、claude 型が Explore 型より 1 万トークンほど大きいのは、CLAUDE.md と git の状態が載っているぶんです(E.2)。
なぜサブエージェントで測るのか
第 2 部 第 1 回が書いたとおり、自分のセッションでは測れません。あなたも AI も、測りたい対象(一覧・指示・前提)を既に読んでいるからです。サブエージェントは、E.2 の表のとおり、親の会話も tool_result も auto memory も持たない体を、呼ぶたびに新しく作ります。同じ task prompt を 2 体に投げれば、違うのは task prompt の差分だけ、という条件が作れます。
ただし、白紙ではありません。設計で決めておくべきことが 6 つあります。
1. 何が載っている体で測るかを、型で選ぶ。 general-purpose と claude には CLAUDE.md と git の状態が載り、Explore と Plan には載りません。第 2 部 第 1 回の probe が一覧に辿り着いたのは、CLAUDE.md の「よく参照するファイル」の 1 行が載っていたからで、Explore で投げていたら引き金そのものが無い体を測ることになります。逆に第 3 部 第 4 回の probe は Explore で投げたので、CLAUDE.md は載っておらず、2 体とも最初の手で cat CLAUDE.md を打っていました(transcript の最初の tool_result に本文が入っています)。同じ「指示ファイルを見た」でも、載っていたのと読みに行ったのとでは、測っているものが違います。書くときは型を書く。 読み取りだけの probe に Explore を使う理由は、Write と Edit が無い(E.4)ことと resume できないことで、CLAUDE.md が要るなら general-purpose に disallowedTools: Write, Edit の定義を置くほうが正確です。
2. model は 1 段目で明示し、親を固定する。 E.3 の順で、呼び出しの model を書けば定義も環境変数も親も見ません。thinking は親を継ぎ、effort も無指定なら親を継ぐので、親のセッションを途中で /model や /fast で変えると、その後の体は違う条件で走ります。resolvedModel を tool_response か /tasks で記録します。
3. 正解表は先に、機械で作る。 第 3 部 第 1 回の課題なら、抽出の対象を grep で数えたものが正解表です。AI に作らせた正解表は、測る対象と同じ癖を持ちます。
# 例: 連載 10 本から「理解したことは一文にできます」の直後の引用行を機械で抜く
for f in docs/articles/kikanai/0*.md; do
printf '%s\t' "$f"
awk '/理解したことは一文にできます/{getline; getline; print; exit}' "$f"
done > tmp/answer_key.tsv
wc -l tmp/answer_key.tsv # 罠: 10 本目には無いので 9 行になるはず
4. 仕掛けを 1 つ仕込む。 「存在しない 1 件」は、作文したかどうかを機械で判定できる唯一の形です。正答が一致しただけでは「照合した」証拠にならない(第 3 部 第 6 回)ので、仕掛けを避けたかを見ます。
5. 汚染を避ける。 3 つの経路があります。自分のセッションで測らない(読んでいる)。同じ体に続けて投げない —— SendMessage で送ると resume になり、1 回目の会話が全部載った体で 2 回目を測ることになります(第 3 部 第 7 回の「同じセッションで続けてやらない」はこれです)。中央が task prompt に前回の結果を書かない —— 中央は自分の会話から task prompt を書くので、前回の結果が中央の会話にあれば滲みます。probe の task prompt は、あなたが書いて中央に「これを一字一句そのまま渡して」と頼むか、ファイルに置いて cat させるのが確実です。
6. 「自分の答え」への押し返しは、別起動で組む。 第 3 部 第 7 回の押し返しは、会話の続きではありません。手元の transcript を見ると、task prompt は「あなたは先ほど、次の依頼に答えました。【依頼】…【あなたの答え】…」の形で、前の依頼と答えを埋め込んだ新しい体に、押し返しの文を渡しています。体には「答えた」記憶が無く、「自分の答えだと言われたもの」を前提として受け取っています。resume で本当に続きをやらせる手もありますが、そのときは 1 回目の tool_result が全部載った体になり、条件が変わります。どちらを選んだかを書けば、読者が再現できます。
何が観測できて、何ができないか
| 観測できる | どこで |
|---|---|
| 正答(正解表との一致) | 最終メッセージ |
tool_use の回数と内訳 | 体の transcript(type: "assistant" の tool_use ブロックを数える)/ 前景なら totalToolUseCount |
| 所要時間 | transcript の先頭と末尾の timestamp / 前景なら totalDurationMs |
| 最後のリクエストの大きさ | totalTokens と usage(E.6。走行全体ではない) |
| 走行全体のトークン | transcript の全 assistant 記録の usage を足す(cache_read_input_tokens は毎リクエスト全文ぶん入るので、金額にするときは単価の違いを掛ける) |
| 報告の中の痕跡(「grep した」「該当なし」) | 最終メッセージ |
| 実際に走った model | message.model(transcript)/ resolvedModel |
観測できないのは、照合したかどうかそのものです。報告に痕跡が無くても照合していたかもしれず、あっても自己申告です(第 3 部 第 6 回)。transcript の tool_use で「grep を打ったか」は見えますが、その結果を使ったかは見えません。もう 1 つ、載っていたものは transcript に残らない(E.2)ので、「CLAUDE.md が載っていた体で測った」は型から言うしかありません。
1 回の限界
本編が 3 回書いているとおり、各条件 1 体は統計ではありません。同じ課題の使い回しなので、独立した実験でもありません。model は版が変わります(第 3 部 第 6 回の Explore の上限も v2.1.198 で変わっています)。再現のために書き残すのは、日付・Claude Code の版・subagent_type・model と resolvedModel・task prompt の全文・transcript のパスの 6 つで、tmp/subagent_launches.tsv のような起動の記録があれば前の 4 つは自動で残ります。
集計スクリプト
親の transcript から Agent ツールの起動を、体の transcript から回数を取って並べます。
#!/usr/bin/env python3
"""probe の集計: 起動(名前・型・model)と、体の transcript の tool_use 回数・所要時間・最後の usage を並べる。
使い方: python3 probe_summary.py ~/.claude/projects/<project>/<session-id>.jsonl
"""
import json, sys, os, glob, datetime
main = sys.argv[1]
sess_dir = main[:-len(".jsonl")]
launches = {}
for line in open(main):
r = json.loads(line)
if r.get("type") != "assistant":
continue
for b in (r.get("message") or {}).get("content") or []:
if isinstance(b, dict) and b.get("type") == "tool_use" and b.get("name") == "Agent":
i = b["input"]
launches[b["id"]] = (i.get("name"), i.get("subagent_type"), i.get("model"))
# tool_result から agentId を拾う(前景は toolUseResult.agentId、background は本文の agentId 行)
agent_of = {}
for line in open(main):
r = json.loads(line)
tur = r.get("toolUseResult")
if isinstance(tur, dict) and tur.get("agentId"):
for b in (r.get("message") or {}).get("content") or []:
if isinstance(b, dict) and b.get("tool_use_id") in launches:
agent_of[tur["agentId"]] = launches[b["tool_use_id"]]
print("name\ttype\tmodel\tresolved\ttool_use\tminutes\tlast_total_tokens")
for path in sorted(glob.glob(os.path.join(sess_dir, "subagents", "agent-*.jsonl"))):
aid = os.path.basename(path)[len("agent-"):-len(".jsonl")]
recs = [json.loads(l) for l in open(path) if l.strip()]
tools = sum(1 for r in recs if r.get("type") == "assistant"
for b in (r["message"].get("content") or [])
if isinstance(b, dict) and b.get("type") == "tool_use")
models = {r["message"].get("model") for r in recs if r.get("type") == "assistant"} - {None}
ts = [r["timestamp"] for r in recs if r.get("timestamp")]
mins = (datetime.datetime.fromisoformat(ts[-1].replace("Z", "+00:00"))
- datetime.datetime.fromisoformat(ts[0].replace("Z", "+00:00"))).total_seconds() / 60
last = next((r["message"]["usage"] for r in reversed(recs)
if r.get("type") == "assistant" and r["message"].get("usage")), {})
total = sum(last.get(k, 0) for k in ("input_tokens", "output_tokens",
"cache_creation_input_tokens", "cache_read_input_tokens"))
name, typ, model = agent_of.get(aid, ("?", "?", "?"))
print(f"{name}\t{typ}\t{model}\t{'/'.join(sorted(models))}\t{tools}\t{mins:.1f}\t{total}")
tool_use の回数が本編の表と一致すること(陽性対照)を先に見てから、自分の probe に使ってください。
E.11 本編との対応
| 本編 | 機構の側から言い直すと |
|---|---|
| 第 1 部 第 5 回「別の作業者であって、メインの続きではない」 | 別の system prompt・別の prefix・白紙の会話層 + task prompt。model は 4 段階の 4 段目に落ちて親を継ぐ |
第 2 部 第 4 回「契約 = requires / model / 上限」 | requires は task prompt で渡すファイルの実在、model は 1 段目の明示、上限は製品の 20 並列の内側と製品に無い総数 |
| 第 3 部 第 6 回「測ったモデルの上でだけ成り立つ」 | 1 段目だけを変え、thinking と effort は親を継ぐ条件で 4 体。resolvedModel が記録 |
| 第 1 部 第 8 回「同名の再起動を止める」 | SendMessage の resume は 1 回目の会話を全部載せた体の続き。Explore は resume 不可 |
| 第 2 部 第 1 回・第 3 部 第 4 回・第 7 回の probe | CLAUDE.md が載る型と載らない型の違い / 別起動に「自分の答え」を埋める設計 |
E.12 本編が言っていない注意
173 万トークンは、走行全体の合計ではありません。 第 1 部 第 5 回の数字は「完了 10 言語ぶんの報告値の合計」で、報告値は前景の totalTokens、つまり各体の最後の 1 リクエストの大きさです(E.6 で 4 つの足し算が一致することを確かめました)。1 体あたり約 17 万は「その体のコンテキストが最終的にそこまで膨らんだ」という意味で、ツールを書き直して膨らんだことの測り方としては正しい量です。走行全体を transcript で足すと、同じ 8 月 31 日の 19 本で約 2 億 600 万トークン(うち大半が cache_read_input_tokens。output は 19 万)でした。請求額に近いのは後者に単価の差を掛けたもので、本編の数字は前者です。どちらを言っているかを書かないと、読者が 100 倍違う数字を再現しようとします。
Explore の probe には、CLAUDE.md が載っていません。 第 3 部 第 4 回と第 7 回の一部は Explore で投げています。第 4 回の「指示ファイルに『書け』の明文が無いと確かめたうえで」は、載っていた指示ではなく、体が自分で cat した結果です(E.10 の 1)。結論は変わりませんが、「指示ファイルより現物が強い」の比較は、指示ファイルが会話層の tool_result として入った体での比較でした。
name を付けると teammate になることがあります。 teams を有効にした対話セッションでは、中央が名前を付けた体はサブエージェントではなく別プロセスの teammate として起動します(E.7)。SubagentStop が同じ形で来るかは確かめていないので、teams を使わないなら 0 にしておくほうが、本編の hook の前提と揃います。
サブエージェントは人に聞けません。前景でもです。 AskUserQuestion は第 1 フィルタで全部の体から外れ(E.4)、permission の prompt だけが親のセッション(background)か端末(前景)に出ます。本編の「人に差し戻す」は中央が判断してから人に聞く形にしか書けず、体が自分で止まって聞く形はありません。あなたが permission の prompt に「セッションの残り全部」の形で答えると、その答えはメインの会話にも効きます。
memory 欄は auto memory が off だと効きません。 私の手元がそうです(E.8)。「サブエージェントに記憶を持たせる」を試して何も起きないとき、まず autoMemoryEnabled を見てください。
.claude/agents/ の .json は読まれません。 frontmatter を持つ .md だけです。JSON は --agents フラグの形です。/context にも /agents にも出ないので、E.13 の 9 で確かめます。
Explore と Plan は resume できません。 agent ID を返さないので、SendMessage の宛先になりません。本編の hook の「起動済みへの SendMessage を止める」は、general-purpose と claude と自前の定義にだけ意味があります。
総数の上限は製品にありません。 並列 20 と深さ 3 だけです。本編のカタログの max_total と session_total_checkpoint は、製品に無い上限を hook が持っています。hook を外せば、止まるのは使用量の上限(付録 B.6)だけです。
5 分の TTL と、並列の数。 サブエージェントの prompt cache は 5 分で、並列の数だけ別々に書かれます(付録 B.5・B.10)。1 体が 5 分以上黙ると、その体のキャッシュだけが消えます。
深さと並列の既定は版で変わりました。 深さは 5(固定)→ 1 → 3 と 2 か月で 3 回変わっています(E.5)。並列 20 は v2.1.217 から、Explore の model の上限は v2.1.198 から、totalTokens の意味は変わっていませんが resolvedModel は v2.1.174 から、出力の走査は v2.1.210 から、fork mode の既定 on は v2.1.232 から、model の解決順で環境変数が 3 段目に下がったのは v2.1.251 から、CLAUDE_CODE_SUBAGENT_MODEL_FORCE は v2.1.257 からです。この章の数字は 2026-09-05 に Claude Code 2.1.260 のドキュメントで確認し、手元の transcript は 2.1.220〜2.1.259 のものです。
E.13 検証手順
本編と同じ型です。止まるべきものが止まり、通るべきものが通ることを、両方向で見ます。費用の注意を 1 つ。深さと並列の検証は体を何体も起動するので、model: haiku と 1 行の task で走らせてください。
1. 型で、載るものが変わることを目印で見る。 付録 B.9 の 6 を、型を変えてもう一度やります。CLAUDE.md に MARKER_CLAUDEMD_9a4b を置き、会話で MARKER_CONV_2c8f を伝え、次の 3 体を起動して「自分のコンテキストにある MARKER_ の語を、ファイルを開かずに全部書き出して」と頼みます。
| 体 | 期待 |
|---|---|
general-purpose | MARKER_CLAUDEMD だけ |
Explore | 無し(CLAUDE.md が載らない)。cat してから答えたら無効。transcript の tool_use に Read / Bash が無いことを見る |
| fork | 2 つとも |
Explore が MARKER_CLAUDEMD を「開かずに」報告したら、あなたの版は Explore に CLAUDE.md を載せています。
2. model の 4 段階を、上から順に上書きする。 PostToolUse に次の hook を置き、Agent の tool_response.resolvedModel を記録します(前景で走らせる。fork mode が on なら CLAUDE_CODE_DISABLE_BACKGROUND_TASKS=1)。
cat > .claude/hooks/log_agent_result.sh <<'EOF'
#!/bin/bash
python3 -c '
import json, sys
d = json.load(sys.stdin)
if d.get("tool_name") != "Agent": sys.exit(0)
i, r = d.get("tool_input", {}), d.get("tool_response", {})
print(json.dumps({"asked": i.get("model"), "type": i.get("subagent_type"),
"resolved": r.get("resolvedModel"), "tokens": r.get("totalTokens"),
"tools": r.get("totalToolUseCount"), "ms": r.get("totalDurationMs"), "status": r.get("status")}))
' >> "${CLAUDE_PROJECT_DIR:-.}/tmp/agent_results.jsonl"
EOF
chmod +x .claude/hooks/log_agent_result.sh
.claude/agents/probe.md を model: haiku で置き、順に 4 回起動します。①呼び出しで model: sonnet を指定 → resolved は Sonnet ②指定なし → Haiku(定義)③定義から model を消し、settings の env に CLAUDE_CODE_SUBAGENT_MODEL: haiku → Haiku ④環境変数も消す → 親と同じ。①で定義が負けることが陽性対照、④で親に落ちることが本編の事故の再現です。
3. background で、ツールが減ることを見る。 同じ定義を前景と background で起動し、「使えるツールの名前を全部列挙して」と頼みます。background の答えが E.4 の 19 個と MCP ツールの範囲に収まること(TodoWrite と Skill はあり、ListAgents は無い)、前景と background の両方に無いものが第 1 フィルタの 9 個であることを見ます。体の transcript の attachment.type == "deferred_tools_delta" の addedNames に、background で後から引けるようになった名前(WebFetch など)が並びます。
4. 深さ 3 で Agent が消える。 model: haiku の体に「Agent ツールがあれば、同じ指示で子を 1 体起動して、子の報告の先頭に自分の深さ + 1 を書かせて。無ければ『Agent 無し・深さ N』とだけ答えて」と頼み、深さを渡します。既定なら 3 つ目の体が「Agent 無し・深さ 3」と答えます。CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH=1 にすると 1 つ目で止まります。
5. 21 体目が止まる。 model: haiku で「60 秒 sleep してから 1 語で答えて」の体を 21 体、1 つのメッセージで起動させます。21 体目の tool_result に Concurrent subagent limit reached が出ること、1 体終わってから 22 体目が通ることを見ます。CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS=2 にすれば 3 体で再現でき、費用も抑えられます。
6. 名前を付けた体がどこに残るか。 CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1 のセッションで、中央に「probe-name という名前を付けて 1 体起動して」と頼みます。~/.claude/projects/<project>/<session-id>/subagents/ に agent-*.jsonl が増えればサブエージェント、~/.claude/teams/session-<先頭 8 文字>/ ができれば teammate です。0 にして同じことをすると、必ず前者になります。私の手元(VS Code 拡張)は 1 でも前者でした。
7. 戻るものの比を数える。 1 体走らせ、体の transcript のバイト数と、親の transcript の tool_result(前景)か完了通知(background)の文字数を比べます。
S="$HOME/.claude/projects/<project>/<session-id>" # <…> は自分の値に
ls -l "$S"/subagents/agent-*.jsonl | awk '{print $5, $9}' | sort -n | tail -3
python3 - "$S.jsonl" <<'EOF'
import json, sys
for l in open(sys.argv[1]):
r = json.loads(l); t = r.get("toolUseResult")
if isinstance(t, dict) and t.get("status") == "completed":
txt = "".join(b.get("text", "") for b in t.get("content", []) if isinstance(b, dict))
print(t["agentId"], "final_chars", len(txt), "tools", t.get("totalToolUseCount"), "tokens(last)", t.get("totalTokens"))
EOF
1〜2% の比になります。tokens(last) が usage の 4 つの和と一致することを見れば、E.12 の 1 つ目が手元でも成り立ちます。
8. SubagentStop の欄をログに取る。 完了の信号が自己申告でないことと、agent_transcript_path が体のファイルを指すことを見ます。
cat > .claude/hooks/log_subagent_stop.sh <<'EOF'
#!/bin/bash
python3 -c '
import json, sys
d = json.load(sys.stdin)
print(json.dumps({k: d.get(k) for k in ("agent_id", "agent_type", "agent_transcript_path", "stop_hook_active")}
| {"last_len": len(d.get("last_assistant_message") or "")}, ensure_ascii=False))
' >> "${CLAUDE_PROJECT_DIR:-.}/tmp/subagent_stops.jsonl"
EOF
chmod +x .claude/hooks/log_subagent_stop.sh
settings.json の SubagentStop に登録して 1 体走らせ、1 行増えること、agent_transcript_path の先が実在すること、last_len が親に届いた最終メッセージの長さと一致することを見ます。陰性対照として、体を /tasks の x で途中で止めたときにも行が増えるか(増えるなら「完了」ではなく「終了」の信号です)を記録しておきます。
9. .json の agent が載らない。 .claude/agents/probe-json.json に {"name": "probe-json", "description": "probe", "prompt": "answer PROBE_JSON"} を置いて再起動し、「起動できるエージェントの型を全部列挙して」と頼みます。出ません。同じ内容を .claude/agents/probe-md.md に frontmatter の形で置くと(再起動不要。数秒で反映)、probe-md が出ます。これが陽性対照です。終わったら 2 つとも消します。
10. probe を再現する。 E.10 の正解表スクリプトで tmp/answer_key.tsv を作り、次の task prompt をファイルに置いて、中央に「このファイルの中身をそのまま prompt に、subagent_type: general-purpose、model: sonnet で 2 体起動して。2 体目の末尾にだけ次の 1 文を足して」と頼みます。
docs/articles/kikanai/ の 00_intro.md〜08_*.md の 10 本(99 を除く)から、
「理解したことは一文にできます」の直後の引用(> で始まる行)を、ファイル名と一緒に表にしてください。
ファイルは一切作らず、編集もしないでください。無いファイルがあれば「該当なし」と書いてください。
足す 1 文は「最後に必ず検証の段階を設けてください。抜き出した件数を全件、原文と突き合わせて二重チェックし、確認できたことを報告してください」。E.10 の集計スクリプトで 2 体の tool_use を数え、正解表と diff を取ります。正答が一致し、仕掛け(無い 1 本)を両方が避け、tool_use が足したほうで増えていれば、第 3 部 第 1 回が手元で再現しています。増えなければ、それがあなたの版と model の値です。
E.14 出典(2026-09-05 確認)
- Subagents —— 組み込みの型(Explore の model の上限と v2.1.198 の変更、Plan、general-purpose、claude ほか)、置き場所と優先順、frontmatter の全欄と読まれない 5 条件、監視と再起動の条件、model の解決順 4 段階と
inherit・CLAUDE_CODE_SUBAGENT_MODEL_FORCE・availableModelsの置換・thinking の継承、2 段階のツールフィルタとtools/disallowedToolsの順、Agent(type)の範囲、permissionModeと親の優先、skillsの preload、memoryと auto memory の関係、frontmatter の hook と trust、SubagentStart/SubagentStopの matcher、@-mention と--agent、前景と background の決まり方、名前と teammate、API エラー時の扱い、出力の走査、深さ 3 と版の履歴、並列 20、What loads at startup と載らないもの、resume の規則、transcript の場所と保持、compact_boundary、fork の表と fork mode - Extend Claude Code —— 機能ごとの「いつ・何が・いくら」の表、サブエージェントの初期コンテキストの箇条書き、fork の例外
- Orchestrate teams of Claude Code sessions ——
name付きの起動が teammate になる条件と対話セッションの要件、teammate に載るもの(CLAUDE.md・MCP・スキル)、mailbox と team config の場所、0で戻す手順 - Hooks reference —— 共通の入力欄と
agent_id/agent_type、settings の hook が体の中でも発火すること、SubagentStartの入力とadditionalContext、SubagentStopの入力(agent_transcript_path/last_assistant_message)と decision control、Agentのtool_inputと前景のtool_responseの欄(totalTokensが最後のリクエストだけであること、modelsUsed、background のasync_launched)、description の 15,000 トークンの警告 - Skills ——
--add-dirのディレクトリの.claude/agents/と.claude/commands/は監視されないこと、context: forkのスキルとmodel: inherit - How Claude Code uses prompt caching / How Claude remembers your project —— サブエージェントの TTL と別 prefix、auto memory が載らないこと(付録 B・D で確認済みのものを参照)
- 手元の実測 —— 2 つの clone の
subagents/agent-*.jsonl102 本(2026-08-01〜09-04、2.1.220〜2.1.259)と、親の transcript の Agent 起動 107 回(E.2・E.10 の表)