サブエージェント(Agent ツール)に作業を任せる形も、複数の Claude Code をヘッドレスで起こして仕事を分担させる技術検証では、候補の1つでした。PreToolUse フックの defer で人の承認を待つ関門を作ると、トップレベルのエージェントでは「止まって、承認後に --resume で続ける」往復が成立していました。ところが、サブエージェントの中で承認の要る操作をさせると、関門は止めるところまでは働きましたが、往復は成立しませんでした。

結論

  • サブエージェントの中でもフックは発火し、ツールは止まりました(ファイルの書き換えは起きなかった)。
  • しかしプロセスは tool_deferred で止まらず、end_turn で終わりました。親のエージェントは「失敗した」と解釈して先へ進みました。
  • 承認して --resume しても、保留したツールは実行されませんでした。
  • そこで、承認を求めることのあるエージェントはサブエージェントにせず、独立のプロセスとして起こすことにしました。あわせて、すべてのエージェントから Agent・Task を --disallowedTools で外しました。

試した環境

  • Mac、Claude Code 2.1.170
  • claude -p --output-format stream-json --verbose --include-hook-events --settings <フックの設定> --setting-sources '' で起こしました。フックは、状態ファイルの中身を見て defer・allow・deny のどれかを返すだけの台本です。

試した仕組みは、エージェントが節目ごとに承認の申請を出して止まり、人が承認すると続きから走る作りでした。

トップレベルでは成立していた

前提として、トップレベルのエージェントでは、次のとおり往復が成立していました。

  • フックが defer を返すと、プロセスは exit 0 で終わり、result の stop_reason が tool_deferred になる。保留したツールは deferred_tool_use に入る。
  • 承認して --resume すると、フックがもう一度呼ばれ、allow を返すと保留したツールが実行される。

起こす側は、stop_reason が tool_deferred かどうかで「承認待ち」を見分けます。is_error は false のままなので、こちらでは見分けられません。

サブエージェントの中で試した

同じフックを掛けたまま、エージェントに Agent ツールでサブエージェントを起こさせ、その中でフックに掛かる操作をさせました。起きたことを図にすると、次のようになります。

サブエージェントの中で defer しても、プロセスは承認待ちで止まらない

フックは確かに発火し、ツールも止まりました。サブエージェントがしようとしたファイルの書き換えは起きていません。ここまでは、関門が効いているように見えます。

ところが、プロセスは tool_deferred ではなく end_turn で終わりました。親のエージェントは、サブエージェントの操作が通らなかったことを「失敗した」と受け取り、そのまま次の作業へ進んでいました。そのあと承認して --resume しても、保留していたはずのツールは実行されませんでした。

何が困るか

関門が「止める」ところまでは働くので、一見すると守られているように見えます。ところが、起こす側が stop_reason で承認待ちを見分けていると、この回は end_turn で終わっているので、承認待ちには見えません。承認したとしても、その操作は実行されません。親のエージェントは、承認の要る操作を飛ばしたまま、先の作業を進めてしまいます。

それぞれ自分の机で働く小さなロボットたちと、一体ずつ線で結ばれた中央の机の人

どうしたか

承認を求めることのあるエージェントは、サブエージェントにせず、起こす側から独立のプロセスとして起こすことにしました。そのうえで、すべてのエージェントから Agent と Task を --disallowedTools で外しました。

claude -p "<指示>" --disallowedTools Agent,Task …

--allowedTools に Agent を入れないだけでは足りませんでした。同じ版で測ったところ、エージェントは --allowedTools に無い道具も使い、その中には Agent も入っていました。名前を挙げて外す --disallowedTools は効き、サブエージェントにも引き継がれました。

どう確かめるか

フックに掛かる操作をサブエージェントにさせて、stream-json の最後の result の stop_reason を見ます。tool_deferred ではなく end_turn になっていれば、この記事と同じことが起きています。あわせて、エージェントが使った道具の名前を stream-json から拾い、Agent・Task が呼ばれていないことを確かめます。

確かめていないこと

  • 2.1.170 より後の版でも同じかどうか。

おわりに

サブエージェントの中の defer は、操作を止めることはできても、承認を待ってから続けることはできませんでした。承認を挟む仕事は、独立に起こしたプロセスに持たせるのが確実でした。

※ この記事に書いた Claude Code の振る舞いは、執筆時点で私たちが測ったものです。いまは変わっている可能性があります。