件名も中身も空の承認の申請が届いたのは、ヘッドレスで何本も起こした Claude Code に仕事を割り振る技術検証で、エージェントに申請用の MCP ツールを持たせたときでした。ツールが引数なし(input: {})で呼ばれていたのです。エラーにはならないので、空の申請がそのまま人の前に並び、何を承認すればよいのか分かりません。

結論

  • Claude Code v2.1.69 以降、MCP ツールは既定で deferred になり、ToolSearch で引いてから呼ばれます。この経路で、申請用の MCP ツールが input: {} で呼ばれました。
  • 毎回は起きませんでした。2回目は、同じ経路で入力が埋まっていました。
  • 子プロセスの環境に ENABLE_TOOL_SEARCH=false を入れると ToolSearch が呼ばれなくなり、1回目から入力が埋まりました。
  • あわせて、PreToolUse フックで必須の項目が空でないかを確かめ、空なら deny で差し戻すようにしました。deny の理由は一字一句そのままモデルに届くので、エージェントが理由を読んで書き直し、出し直すことを狙いました。

試した環境

  • Mac、Claude Code 2.1.170
  • claude -p --output-format stream-json --verbose --include-hook-events … で起こしたエージェントに、承認の申請を出す MCP ツールを渡しました。その MCP ツールの呼び出しには PreToolUse フックを掛け、defer で承認待ちにしていました。

何が起きたか

エージェントが節目で申請用の MCP ツールを呼び、フックがそれを defer で止めて、人の承認を待つ作りでした。あるとき、届いた申請の件名も中身も空でした。stream-json を見ると、エージェントはまず ToolSearch で申請用のツールを引き、そのあとそのツールを input: {} で呼んでいました。

v2.1.69 以降の Claude Code では、MCP ツールは既定で deferred になっています。モデルは ToolSearch でツールを引いてから呼びます。空の入力で呼ばれたのは、この経路を通った回でした。

けれども、同じ経路を通った2回目は、入力がちゃんと埋まっていました。毎回起きるわけではないので、一度うまくいったのを見て、問題が無いと思い込みやすい所です。なぜ空になったのかまでは分かっていません。

空欄のある用紙を窓口で返され、ペンを手に書き直そうとするロボット

どうしたか(1)ToolSearch を使わせない

子プロセスを起こすときの環境に ENABLE_TOOL_SEARCH=false を入れると、ToolSearch が呼ばれなくなり、1回目から入力が埋まりました。MCP ツールのスキーマが、最初から文脈に入るようになります。

子プロセスの環境は、次のように作っていました(抜き出し)。

function safeEnv(extra: Record<string, string> = {}): NodeJS.ProcessEnv {
  const env: NodeJS.ProcessEnv = {
    ...process.env,
    // MCP ツールのスキーマを最初から文脈に入れる
    // (ToolSearch の経路で、申請用のツールが input: {} で呼ばれたため)
    ENABLE_TOOL_SEARCH: 'false',
    ...extra,
  };
  // …(API キーなどを消す処理は省略)
  return env;
}

どうしたか(2)フックで空を差し戻す

あわせて、フックの側にも確かめる処理を置きました。申請用のツールの呼び出しを受け取ったら、フックは必須の項目が空でないかを見ます。空なら deny を返し、理由の文に何が足りないかを書きます。deny の permissionDecisionReason は、ツールの結果として一字一句そのままモデルに届くので、エージェントが理由を読んで書き直し、出し直すことを狙いました。

必須の項目が空なら deny で差し戻し、埋まっていれば defer で承認待ちにする

こうしておけば、空の申請は人の前に並ぶ前に、エージェントの側へ差し戻されます。

どう確かめるか

stream-json を見て、次の2点を確かめます。

  • 申請用のツールの前に ToolSearch が呼ばれていないか(ENABLE_TOOL_SEARCH=false が効いているか)
  • 申請用のツールの呼び出しの input が空でないか(フックが受け取る tool_input でも見られます)

確かめていないこと

  • 空の入力で呼ばれることは毎回は起きなかったので、ENABLE_TOOL_SEARCH=false だけで必ず防げるとまでは言えません。
  • 2.1.170 より後の版でも同じかどうか。

おわりに

MCP ツールが、エラーも出さずに空の入力で呼ばれることがありました。私たちは、ToolSearch を使わせない環境変数と、フックで空の入力を差し戻す仕掛けの二重にして備えました。

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