🔬

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を使う際の推奨設定をまとめます。

推奨する使い方

  1. 自動選択に任せる: 明示的なType指定は基本不要。Claude Codeが適切に選んでくれます。

  2. CLAUDE.mdでルールを定義: Subagentにも適用されるので、コーディング規約やセキュリティルールはCLAUDE.mdに書いておく。

  3. 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
  1. 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への情報の受け渡し期待: コンテキストは継承されないので、プロンプトに明示的に含める必要があります。

まとめ

今回の検証でわかったこと:

  1. Subagent内のツール使用はHooksで監視できない(SubagentStopのみ発火)
  2. 会話履歴は継承されないが、CLAUDE.mdは適用される
  3. SubagentStopでtranscript_pathを取得し、ファイルを読めば入出力を監視可能
  4. 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日まで毎日更新。フォローしておくと通知が届きます。「うちではこう動いた」などのコメントもお待ちしてます。

→ 著者ページ(Zenn)


次回:Day 17「Subagentsオーケストレーションの基礎―複数エージェントの協調動作」

Discussion