Claude Code 実践検証 Day 16|Subagents検証―Windows環境でのHooks発火ギャップ
昨日のSubagents基礎編を踏まえて、実際に動作を検証してみました。
結論から言うと、Subagent内のツール使用はPreToolUse/PostToolUseで監視できません。
これ、Hooksで監査ログを取ろうとしている場合は重大な問題です。Subagent経由の操作はログに残らない「監査ギャップ」が生まれます。
正直、最初は「Hooksを設定すれば全部ログ取れるでしょ」と思ってたんですよね。甘かったです。
検証結果まとめ
| 検証項目 | 結果 | 補足 |
|---|---|---|
| SubagentでのHooks発火 | △ | SubagentStopのみ発火 |
| コンテキスト継承 | × | 会話履歴は渡されない |
| CLAUDE.md適用 | ○ | Subagentにも完全に適用 |
| Type別の速度差 | ○ | Explore 25秒、general-purpose 41秒 |
| allowedTools制限 | △ | Bash経由で迂回可能 |
| SubagentStop stdin | △ | typeやresultは直接含まれない |
| 戻り値の取得 | ○ | transcript_pathから取得可能 |
予想通りだったのはCLAUDE.mdの適用。最初は「SubagentStopでは戻り値が取れない」と思いましたが、transcript_pathのJSONLファイルを読み込むことで取得できることがわかりました。
検証環境
| 項目 | 値 |
|---|---|
| OS | Windows 11 Pro |
| Claude Code | v2.0.69 |
| モデル | Opus 4.5 |
| 検証日 | 2025-12-14 |
検証1:Hooks発火の範囲
Subagent内でツールが使われたとき、Hooksは発火するのか。これが一番気になっていたポイントです。
やったこと
settings.local.jsonにPreToolUse、PostToolUse、SubagentStopの3つのHooksを設定。
{
"hooks": {
"PreToolUse": [{"hooks": [{"type": "command", "command": "powershell.exe -Command \"Add-Content 'events.log' 'PreToolUse'\""}]}],
"PostToolUse": [{"hooks": [{"type": "command", "command": "powershell.exe -Command \"Add-Content 'events.log' 'PostToolUse'\""}]}],
"SubagentStop": [{"hooks": [{"type": "command", "command": "powershell.exe -Command \"Add-Content 'events.log' 'SubagentStop'\""}]}]
}
}
その後、Exploreエージェントを使ってファイル検索を実行。
Exploreエージェントを使って、このプロジェクトのファイル構成を調べてください。
結果
[events.log の内容]
SubagentStop
SubagentStop
SubagentStop
PreToolUseもPostToolUseも記録されていない。SubagentStopだけが3回発火。
正直に告白すると、ここで30分くらい「設定ミスったかな」と悩みました。何度やっても同じ結果。設定は間違っていなかったんです。
わかったこと
Subagent内でのツール使用(Glob、Read、Grepなど)は、PreToolUse/PostToolUseイベントを発火しません。これは仕様のようです。
おそらく、SubagentはメインセッションとはHooksの適用範囲が異なる設計なんだと思います。ドキュメントには明記されていないですが。
検証2:コンテキストの継承
Subagentはメインエージェントとの会話内容を知っているのか。
やったこと
2ステップで検証。
ステップ1: 「SECRET_VALUE = 12345」という情報を覚えておいてください。
ステップ2: Exploreエージェントを使って、SECRET_VALUEの値を教えてください。
結果
Subagentの回答は「SECRET_VALUEという変数は見つかりませんでした」。
メインエージェントとの会話内容は、Subagentに渡されていません。
わかったこと
公式ドキュメントには「access to current context」を持つエージェントがあると書いてありますが、これは会話履歴のことではないようです。
たぶん、ここで言う「context」はCLAUDE.mdやプロジェクト構造の認識であって、会話の流れではないんでしょうね。
セキュリティ的には利点かもしれません。秘密情報を誤ってSubagentに漏らす心配がない。
検証3:Type別の処理速度
ExploreとGeneral-purposeで、同じタスクにどれくらい時間差があるか。
やったこと
同じプロンプトを両方のTypeで実行して時間を計測。
{Type}エージェントを使って、CLAUDE.mdファイルを探してください。
結果
| Type | 実行時間 | 検索範囲 |
|---|---|---|
| Explore | 25秒 | プロジェクト内のみ |
| general-purpose | 41秒 | 広範囲(ユーザーレベルも発見) |
Exploreの方が16秒速い。ただし、検索範囲も狭い。
わかったこと
Exploreは「速く、狭く」、general-purposeは「遅く、広く」という特性があります。
単純なファイル検索ならExplore、複数ディレクトリをまたぐ調査ならgeneral-purposeという使い分けが有効そうです。
検証4:allowedToolsによるSubagent制限
Subagentは内部的にTask toolで起動されます。このTask toolにはallowedToolsパラメータがあり、Subagentが使えるツールを制限できます。
たとえば「ファイル読み取りは禁止したい」という場合、Read toolを許可リストから外せばいい…はずですが、実際どうか。
やったこと
以下のコマンドで、親セッションのallowedToolsをTask,Bashのみに制限して実行しました。
claude -p "Task toolを使ってSubagentを起動してください。allowedToolsはBashのみに制限し、CLAUDE.mdファイルの内容を取得してください。" --allowedTools "Task,Bash"
結果(Subagent transcript: agent-a5b3066.jsonl)
SubagentはRead toolを使わず、Bashのcatコマンドでファイルを読み取りました。
{
"name": "Bash",
"input": {
"command": "cat \"C:\\development\\projects\\day15-16-subagents\\CLAUDE.md\"",
"description": "Read project CLAUDE.md file"
}
}
結果、ファイル内容を取得できてしまいました。
# Subagents検証用 CLAUDE.md
このファイルはSubagentsがCLAUDE.mdを読み込むかどうかの検証用です。
...
わかったこと
allowedToolsは「このツールを使わせない」という制限であって、「この操作をさせない」という制限ではありません。
できること:
- 特定ツール(Read、Grep等)の使用を禁止する
できないこと:
- Bashが許可されている場合のファイル読み取り防止(
catで代替可能) - Subagent内のコマンド監視(Hooksが発火しないため)
結論として、allowedToolsによるSubagentのセキュリティ制御には限界があります。Bashを許可する限り、事実上ほとんどの操作が可能です。
公式ドキュメントとの比較
| 項目 | 公式の説明 | 実際の動作 | 差異 |
|---|---|---|---|
| コンテキスト継承 | 「access to current context」あり | 会話履歴は継承されない | 中程度 |
| Hooks発火 | 明記なし | Subagent内ではPreToolUse/PostToolUse発火しない | 大きな差異 |
| SubagentStop stdin | 明記なし | subagent_type, resultは含まれない | 中程度 |
公式ドキュメントには「Subagent内でHooksがどう動くか」の記載がありません。ここは明記してほしいところですね。
検証5:SubagentStopで取得できるデータと戻り値の監視
SubagentStopのHookで、どんな情報が取れるのか確認しました。さらに、Subagentの戻り値を取得できるかも検証しました。
やったこと
SubagentStopのstdinをファイルに出力するHookを設定。
{
"hooks": {
"SubagentStop": [
{
"hooks": [
{
"type": "command",
"command": "powershell.exe -Command \"$input | Out-File -Append 'subagent-detail.log'\""
}
]
}
]
}
}
結果(stdinの内容)
{
"session_id": "dfce79b9-0f11-4e86-b050-e02c8f69d151",
"cwd": "C:\\development\\projects\\day15-16-subagents",
"hook_event_name": "SubagentStop",
"agent_id": "a5a97ba",
"agent_transcript_path": "C:\\Users\\User\\.claude\\projects\\...\\agent-a5a97ba.jsonl"
}
SubagentStopのstdinには以下が含まれません:
-
subagent_type(Explore/general-purposeの区別不可) -
result/output(実行結果そのもの)
しかし、agent_transcript_pathを使えば戻り値を取得できます。
実際にtranscriptファイルを読み込んでみました。
# transcriptファイルから最後のassistantメッセージを取得
$lines = Get-Content $json.agent_transcript_path
$lastAssistant = $lines | ForEach-Object { $_ | ConvertFrom-Json } | Where-Object { $_.type -eq 'assistant' } | Select-Object -Last 1
$lastAssistant.message.content
取得できた戻り値:
## ディレクトリ構造とファイル一覧
現在のディレクトリ **C:\development\projects\day15-16-subagents** の構成は以下の通りです...
わかったこと
- SubagentStopのstdinに直接結果は含まれないが、
agent_transcript_pathのJSONLファイルを読み込めばSubagentの戻り値を取得できる - transcriptファイルはJSONL形式で、
type: "assistant"のエントリにSubagentの出力が含まれる - Hook内でこの処理を追加すれば、Subagentの入出力をリアルタイムでログに記録可能
実務での推奨パターン
検証結果を踏まえて、Subagentsを使う際の推奨設定をまとめます。
推奨する使い方
-
自動選択に任せる: 明示的なType指定は基本不要。Claude Codeが適切に選んでくれます。
-
CLAUDE.mdでルールを定義: Subagentにも適用されるので、コーディング規約やセキュリティルールはCLAUDE.mdに書いておく。
-
SubagentStopでtranscript_pathを記録: Subagentの入出力を事後確認したい場合、
agent_transcript_pathをログに残しておくと便利です。
{
"hooks": {
"SubagentStop": [
{
"hooks": [
{
"type": "command",
"command": "powershell.exe -Command \"$json = $input | ConvertFrom-Json; Add-Content 'subagent.log' ('[' + (Get-Date) + '] ' + $json.agent_id + ' -> ' + $json.agent_transcript_path)\""
}
]
}
]
}
}
出力例:
[12/14/2025 14:32:15] a5bb872 -> C:\Users\User\.claude\projects\...\agent-a5bb872.jsonl
- Subagentの戻り値を監視: transcript_pathのJSONLファイルを読み込めば、Subagentの入出力を取得できます。
{
"hooks": {
"SubagentStop": [
{
"hooks": [
{
"type": "command",
"command": "powershell.exe -Command \"$json = $input | ConvertFrom-Json; $transcript = Get-Content $json.agent_transcript_path | ForEach-Object { $_ | ConvertFrom-Json }; $lastMessage = $transcript | Where-Object { $_.type -eq 'assistant' } | Select-Object -Last 1; Add-Content 'subagent-output.log' ('[' + (Get-Date) + '] ' + $json.agent_id + ': ' + ($lastMessage.message.content | Out-String).Substring(0,200))\""
}
]
}
]
}
}
出力例:
[12/14/2025 14:32:15] a5bb872: プロジェクトのファイル構成は以下の通りです。src/ディレクトリにTypeScriptファイルが...
このHookは、Subagent終了時にtranscriptファイルを読み込み、最後のassistantメッセージ(=Subagentの戻り値)を抽出してログに記録します。
注意点:
- transcriptファイルはJSONL形式(1行1JSON)
-
type: "assistant"の最後のエントリがSubagentの出力 - 長い出力は切り詰めるなどの工夫が必要(上記例では200文字)
避けた方がいいこと
- PreToolUse/PostToolUseだけに頼った監査: Subagent経由の操作は記録されません。
- allowedToolsでの厳格なセキュリティ制御: Bash経由で迂回可能です。
- Subagentへの情報の受け渡し期待: コンテキストは継承されないので、プロンプトに明示的に含める必要があります。
まとめ
今回の検証でわかったこと:
- Subagent内のツール使用はHooksで監視できない(SubagentStopのみ発火)
- 会話履歴は継承されないが、CLAUDE.mdは適用される
- SubagentStopでtranscript_pathを取得し、ファイルを読めば入出力を監視可能
- allowedToolsによる制限はBash経由で迂回可能
Subagentは便利な機能ですが、「裏で何が起きているか」をリアルタイムで把握するには一工夫必要です。agent_transcript_pathのJSONLファイルを読み込むHookを設定すれば、Subagentの戻り値も監視できます。
セキュリティや監査を重視する環境では、この点を理解した上で設計する必要がありますね。同じところで悩んでる人の参考になれば。
この検証結果を使うと
Subagentの動作特性を理解しておくと、「なぜHooksでログが取れないのか」「なぜ秘密情報がSubagentに渡らないのか」がわかります。特にHooksで監査ログを取ろうとしている場合、この監査ギャップを前提にした設計が必要です。
親セッションのtranscript生データ(dfce79b9-...jsonl)
transcript_path で取得できる親セッションのファイル。メインエージェントとユーザーのやり取りが記録されています。
// Line 2: ユーザーの指示
{"parentUuid":null,"isSidechain":false,"userType":"external","cwd":"C:\\development\\projects\\day15-16-subagents","sessionId":"dfce79b9-0f11-4e86-b050-e02c8f69d151","version":"2.0.69","gitBranch":"","type":"user","message":{"role":"user","content":"Exploreエージェントを使って、ファイル一覧を取得してください。"},"uuid":"146e809e-f04e-46b1-9179-27a72d26e5af","timestamp":"2025-12-14T07:46:09.978Z"}
// Line 4: Task toolの呼び出し
{"parentUuid":"d6be5598-380a-4f03-8ec5-d18bf0669604","isSidechain":false,"userType":"external","cwd":"C:\\development\\projects\\day15-16-subagents","sessionId":"dfce79b9-0f11-4e86-b050-e02c8f69d151","version":"2.0.69","gitBranch":"","message":{"model":"claude-opus-4-5-20251101","id":"msg_013p4SF7pwe1dpzvpdy1A1cc","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_017G46JLkT7kLfHGN1R6FfGX","name":"Task","input":{"subagent_type":"Explore","prompt":"現在のディレクトリ(C:\\development\\projects\\day15-16-subagents)のファイル一覧を取得してください。ディレクトリ構造とすべてのファイルを確認してください。","description":"ファイル一覧の取得"}}],"stop_reason":null,"stop_sequence":null,"usage":{...}},"requestId":"req_011CW6D3T13dNtw84egjExMJ","type":"assistant","uuid":"dbf5336b-6b17-47fa-b864-c5b0496cd8f4","timestamp":"2025-12-14T07:46:14.607Z"}
// Line 5: Subagentからの戻り値(toolUseResultに含まれる)
{"parentUuid":"dbf5336b-6b17-47fa-b864-c5b0496cd8f4","isSidechain":false,"userType":"external","cwd":"C:\\development\\projects\\day15-16-subagents","sessionId":"dfce79b9-0f11-4e86-b050-e02c8f69d151","version":"2.0.69","gitBranch":"","slug":"atomic-wishing-flurry","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_017G46JLkT7kLfHGN1R6FfGX","type":"tool_result","content":[{"type":"text","text":"## ディレクトリ構造とファイル一覧\n\n現在のディレクトリ **C:\\development\\projects\\day15-16-subagents** の構成は以下の通りです:\n\n### ディレクトリ構造\n\n```\nC:\\development\\projects\\day15-16-subagents/\n├── .claude/\n│ └── settings.local.json\n├── CLAUDE.md\n...\n```\n\n[CLAUDE.MD-LOADED]"},{"type":"text","text":"agentId: a5a97ba (for resuming to continue this agent's work if needed)"}]}]},"uuid":"3a7bf3e8-5463-4cbd-a35e-edf368fcd66d","timestamp":"2025-12-14T07:46:33.219Z","toolUseResult":{"status":"completed","prompt":"現在のディレクトリ...","agentId":"a5a97ba","content":[...],"totalDurationMs":18527,"totalTokens":16294,"totalToolUseCount":5,"usage":{...}}}
Subagentのtranscript生データ(agent-a5a97ba.jsonl)
agent_transcript_path で取得できるSubagent専用のファイル。Subagent内部でどのツールが使われたかが記録されています。
// Line 1: 親からのプロンプト(Subagentへの入力)
{"parentUuid":null,"isSidechain":true,"userType":"external","cwd":"C:\\development\\projects\\day15-16-subagents","sessionId":"dfce79b9-0f11-4e86-b050-e02c8f69d151","version":"2.0.69","gitBranch":"","agentId":"a5a97ba","type":"user","message":{"role":"user","content":"現在のディレクトリ(C:\\development\\projects\\day15-16-subagents)のファイル一覧を取得してください。ディレクトリ構造とすべてのファイルを確認してください。"},"uuid":"900845bd-759a-4c33-b614-81cca3dd0c8d","timestamp":"2025-12-14T07:46:14.718Z"}
// Line 3: Bashツールの呼び出し(モデル: claude-haiku-4-5)
{"parentUuid":"3ca376a5-a697-48b6-a075-0a58dce33f9b","isSidechain":true,"userType":"external","cwd":"C:\\development\\projects\\day15-16-subagents","sessionId":"dfce79b9-0f11-4e86-b050-e02c8f69d151","version":"2.0.69","gitBranch":"","agentId":"a5a97ba","message":{"model":"claude-haiku-4-5-20251001","id":"msg_01XbL5c6usZW7C8xisRZ9ETw","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01TzL2cnoxe1D15omXiLQQrF","name":"Bash","input":{"command":"ls -la \"C:\\development\\projects\\day15-16-subagents\"","description":"List all files and directories in current project"}}],"stop_reason":null,"stop_sequence":null,"usage":{...}},"requestId":"req_011CW6D3oQ8uoZfvSD4EDQjj","type":"assistant","uuid":"42955295-e7a4-4ec7-9afb-da7b70e2e9db","timestamp":"2025-12-14T07:46:16.349Z"}
// Line 5: Bashの実行結果
{"parentUuid":"42955295-e7a4-4ec7-9afb-da7b70e2e9db","isSidechain":true,"userType":"external","cwd":"C:\\development\\projects\\day15-16-subagents","sessionId":"dfce79b9-0f11-4e86-b050-e02c8f69d151","version":"2.0.69","gitBranch":"","agentId":"a5a97ba","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01TzL2cnoxe1D15omXiLQQrF","type":"tool_result","content":"total 26\ndrwxr-xr-x 1 User 197121 0 12月 14 16:46 ./\ndrwxr-xr-x 1 User 197121 0 12月 14 14:48 ../\ndrwxr-xr-x 1 User 197121 0 12月 14 16:45 .claude/\n-rw-r--r-- 1 User 197121 796 12月 14 14:41 CLAUDE.md\n-rw-r--r-- 1 User 197121 46 12月 14 16:44 nul\n-rw-r--r-- 1 User 197121 84 12月 14 16:46 subagent-events.log\n-rw-r--r-- 1 User 197121 973 12月 14 16:46 subagent-stop-detail.log\n-rw-r--r-- 1 User 197121 4947 12月 14 14:52 verification-prompts.md","is_error":false}]},"uuid":"cef2f202-f430-4d83-b19a-c890376db224","timestamp":"2025-12-14T07:46:18.311Z","toolUseResult":{"stdout":"total 26\ndrwxr-xr-x 1 User 197121...","stderr":"","interrupted":false,"isImage":false}}
// Line 8: Readツールの呼び出し(CLAUDE.mdを読み込み)
{"parentUuid":"2cfca46c-ad23-46e6-803b-c3a3f546165e","isSidechain":true,"userType":"external","cwd":"C:\\development\\projects\\day15-16-subagents","sessionId":"dfce79b9-0f11-4e86-b050-e02c8f69d151","version":"2.0.69","gitBranch":"","agentId":"a5a97ba","message":{"model":"claude-haiku-4-5-20251001","id":"msg_01UNrk9XcNgehVGvthddUwGT","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_013iFWHnjVenMbV3PLFroTpK","name":"Read","input":{"file_path":"C:\\development\\projects\\day15-16-subagents\\CLAUDE.md"}}],"stop_reason":null,"stop_sequence":null,"usage":{...}},"requestId":"req_011CW6D48aYgNqNVZRz3B2aU","type":"assistant","uuid":"42a5e0ff-15d9-4338-860f-27e649d664e1","timestamp":"2025-12-14T07:46:20.939Z"}
// Line 14: Subagentの最終出力(これが戻り値として親に返る)
{"parentUuid":"8ef064f4-2822-4499-834e-5fcf1500776d","isSidechain":true,"userType":"external","cwd":"C:\\development\\projects\\day15-16-subagents","sessionId":"dfce79b9-0f11-4e86-b050-e02c8f69d151","version":"2.0.69","gitBranch":"","agentId":"a5a97ba","slug":"atomic-wishing-flurry","message":{"model":"claude-haiku-4-5-20251001","id":"msg_01JngGykYLXiT1ScMofMDfPS","type":"message","role":"assistant","content":[{"type":"text","text":"## ディレクトリ構造とファイル一覧\n\n現在のディレクトリ **C:\\development\\projects\\day15-16-subagents** の構成は以下の通りです:\n\n### ディレクトリ構造\n\n```\nC:\\development\\projects\\day15-16-subagents/\n├── .claude/\n│ └── settings.local.json\n├── CLAUDE.md\n├── nul\n├── subagent-events.log\n├── subagent-stop-detail.log\n└── verification-prompts.md\n```\n\n### ファイル詳細\n\n| ファイル名 | サイズ | 説明 |\n|-----------|--------|------|\n| **CLAUDE.md** | 796 bytes | プロジェクト用CLAUDE.md(Subagents検証用ルール定義) |\n...\n\n[CLAUDE.MD-LOADED]"}],"stop_reason":null,"stop_sequence":null,"usage":{...}},"requestId":"req_011CW6D4d4osX1ebofYMPBb9","type":"assistant","uuid":"0c8b6dc7-1dfc-4726-a53b-41e019dc52c6","timestamp":"2025-12-14T07:46:32.829Z"}
2つのtranscriptファイルの違い
| 項目 | 親セッション(transcript_path) | Subagent(agent_transcript_path) |
|---|---|---|
| 記録内容 | メインエージェントの動作 | Subagent内部の動作 |
| ツール使用 | Task toolの呼び出しのみ | Bash、Read等の実際のツール |
| 戻り値 | toolUseResult.contentに含まれる | 最後のassistantエントリ |
| 用途 | 全体の流れの監査 | Subagent内部の詳細監査 |
| モデル情報 | claude-opus-4-5(親のモデル) | claude-haiku-4-5(Subagentのモデル) |
| isSidechain | false | true |
ポイント: PreToolUse/PostToolUse Hookで監視できないSubagent内のツール使用も、agent_transcript_pathを読めば事後確認が可能です。
シリーズを追いかける
12月25日まで毎日更新。フォローしておくと通知が届きます。「うちではこう動いた」などのコメントもお待ちしてます。
次回:Day 17「Subagentsオーケストレーションの基礎―複数エージェントの協調動作」
Discussion