Claude Media
Elicitation/ElicitationResultフックでMCP入力要求を横取りする

Elicitation/ElicitationResultフックでMCP入力要求を横取りする

MCPサーバーがユーザー入力を求めるElicitationと、応答を仲介するElicitationResult。2つのフックで自動応答・記録・機密情報要求のブロックまでを扱います。

MCPサーバーがツール呼び出しの途中でユーザーに追加入力を求めると、Claude Codeは既定で対話ダイアログを表示します。Elicitationフックはこの要求そのものを横取りして自動応答に置き換え、ElicitationResultフックはその応答が実際にサーバーへ送り返される直前でもう一度観察・上書き・ブロックできます。CIやheadless運用でダイアログを人手で処理できない場面や、どのサーバーが何を求めたかを監査ログに残したい場面で、この2つが実質的な唯一の自動化経路です。要求そのものがフォームモードかURLモードかという仕様面の違いはMCPのElicitation、フォームとURLで何が違うかで扱っており、本記事はその要求をClaude Code側でどう横取り・検査するかというフック実装に絞ります。

Elicitation/ElicitationResultフックとは何をする仕組みか

Elicitationは、MCPサーバーがツール呼び出しを処理している最中に「情報が足りない」と判断したとき、クライアント経由でユーザーに追加入力を求めるMCPの機能です。Claude Codeのフックライフサイクル図では、ElicitationElicitationResultはどちらもMCPツール実行の内側にネストされた非同期イベントとして描かれています。Elicitationはサーバーが入力を要求した時点で発火し、ElicitationResultはユーザー(または自動応答したフック)が応答した後、その内容が実際にサーバーへ送り返される前に発火します。

この2つには他の主要フックと違う制約があります。PreToolUse hookでツール実行前に許可・拒否・改変するで扱ったPreToolUseStopはコマンド・HTTP・MCPツール・プロンプト・エージェントの5種類すべてのハンドラー型に対応しますが、ElicitationElicitationResultが対応するのはcommandhttpmcp_toolの3種類だけです。LLMに判断させるprompt型・agent型のフックハンドラーはこの2つのイベントでは使えません。認証情報のやり取りに関わる以上、判定ロジックは決定的なスクリプトで書くしかない設計になっています。

もう一点押さえておきたいのは、この2つのフックがsystemMessagecontinueを捨てるという点です。他のイベントならJSON出力のsystemMessageでユーザーに警告を表示できますが、公式ドキュメントは「Claude CodeはhookSpecificOutputだけを見て、systemMessagecontinueは捨てる」と明記しています。ブロックした理由をユーザーに伝えたいなら、フック自身でログに書くか、後述のように別チャネルへ通知する必要があります。

どこで定義でき、遅いスクリプトはどう扱われるか

Elicitation/ElicitationResultは他のcommand/http/mcp_tool対応イベントと同じ場所に定義できます。ユーザー単位の~/.claude/settings.json、プロジェクト単位の.claude/settings.json、Skillやサブエージェントのfrontmatterまで、スコープの選び方は他のフックと変わりません(他のフックイベント全体の定義場所や設定階層はClaude Code Hooks完全ガイドにまとめています)。とくに相性が良いのはサブエージェントのfrontmatterです。特定のサブエージェントだけが機密性の高いMCPサーバーを呼ぶ設計なら、そのサブエージェントの定義ファイルにフックを閉じ込めておけば、メインの会話には影響が及びません。サブエージェント内で発火した場合、共通入力フィールドにagent_idagent_typeが追加されるので、ログ側でどのサブエージェント経由の要求かを区別できます。

ハンドラー型はcommandだけでなくhttpmcp_toolも選べます。http型を選べば、複数プロジェクトの判定ロジックを1台のポリシーサーバーに集約でき、2xxレスポンスに同じhookSpecificOutput形式のJSONボディを返すだけでcommand型と同じ制御ができます。mcp_tool型を選べば、判定そのものを別の(すでに接続済みの)MCPサーバーのツール呼び出しに委譲することもできます——例えば社内の権限管理システムをMCPサーバー化しておき、そのツールに「このサーバーのこの要求は許可してよいか」を問い合わせる構成です。

タイムアウトの扱いには注意が必要です。async: trueを付けない限り、スクリプトがtimeoutに達すると出力は破棄されます。公式ドキュメントはこの2イベントについてタイムアウト時の挙動を個別に記載していないため、決定が下されないという一般則から読むことになります。厳格にブロックすることを目的にスクリプトを書くなら、外部APIへの問い合わせのような時間のかかる処理を入れる前に、タイムアウト値を明示的に伸ばしておくという選択肢があります。

matcherはMCPサーバー名だけを見る — ツール名パターンとは別物

PreToolUseのようなツールイベントはmcp__<server>__<tool>という命名でMCPツールを識別し、mcp__memory__.*のようなワイルドカードでサーバー単位にまとめて絞り込みます。ElicitationElicitationResultのmatcherはこの命名規則に乗りません。matcherが直接評価するのはツール名ではなくMCPサーバー名そのものです。

{
  "hooks": {
    "Elicitation": [
      {
        "matcher": "internal-crm",
        "hooks": [{ "type": "command", "command": "/path/to/handle-crm-elicitation.sh" }]
      }
    ]
  }
}

internal-crmのようにサーバー名を裸で書けば完全一致になり、mcp__internal-crm__.*と書いても一致しません。全サーバーを対象にしたいならmatcherを省略するか"*"にします。文字種によって完全一致(英数字・アンダースコア・ハイフン・空白・カンマ・|のみ)か正規表現かが決まるのは他のイベントと同じ規則で、server-a|server-bのようにカンマや|で複数サーバーを列挙できます。

Elicitationフックの入力と出力 — accept/decline/cancelで応答を横取りする

Elicitationフックは共通入力フィールドに加えてmcp_server_namemessageと、モードに応じた任意フィールドを受け取ります。フォームモードならmode: "form"requested_schema(要求するデータのJSON Schema)、URLモードならmode: "url"urlが渡ります。

{
  "hook_event_name": "Elicitation",
  "mcp_server_name": "my-mcp-server",
  "message": "Please provide your credentials",
  "mode": "form",
  "requested_schema": {
    "type": "object",
    "properties": { "username": { "type": "string", "title": "Username" } }
  }
}

応答を横取りするには、hookSpecificOutputaction(accept / decline / cancel)を入れてJSONを返します。contentはフォームの入力値で、actionacceptのときだけ意味を持ちます。ダイアログを一切表示せずに済ませられる代わりに、exit 2で要求そのものを拒否することもできます。ただしexit 2を使った場合、そのとき同時に出力したhookSpecificOutputは無視される点に注意してください——ブロックしたい場合はhookSpecificOutputを書かずexit 2だけで済ませるのが素直です。

ElicitationResultフックは応答を送り返す直前の最後の関所

ElicitationResultは、ユーザー(またはElicitationフック)が応答した後、その内容がMCPサーバーへ実際に送信される直前に発火します。入力はmcp_server_nameactionに加え、content(フォーム値)を任意で受け取ります。出力側でactioncontentを上書きすれば、いったん確定した応答を差し替えられ、exit 2を返すと有効なアクションが強制的にdeclineに変わります。

{
  "hookSpecificOutput": {
    "hookEventName": "ElicitationResult",
    "action": "decline",
    "content": {}
  }
}

この位置づけが効くのは、サーバーが仕様に反してフォームモードでパスワードやAPIキーを要求してきた場合です。フォームモードでの機密情報要求はMCPの仕様上禁止されていますが、実装がその制約を破っていないという保証はどこにもありません。ユーザーが気づかずにacceptしてしまった応答でも、contentの中身をElicitationResultで検査してから実際の送信をブロックできれば、クライアント側に置ける最後の防御線として機能します。公式ドキュメントは「ユーザーが応答した後」としか書いておらず、Elicitationフックが自動応答した場合にも同様に発火するかは明記されていません。両方のフックを重ねて仕込んでおけば、どちらの経路でも取りこぼしにくい構成になります。

レシピ: すべての要求と応答をログに記録する

監査目的なら、応答を変更せずにログだけ残すのが最も安全な使い方です。matcherを省略してすべてのMCPサーバーを対象にし、exit 0で通常のダイアログ挙動をそのまま通します。

#!/bin/bash
# Elicitation/ElicitationResult: 要求と応答をJSON Lines形式で記録する
input=$(cat)
log_dir="$HOME/.claude/elicitation-log"
mkdir -p "$log_dir"
 
jq -c --arg ts "$(date -u +%FT%TZ)" '. + {logged_at: $ts}' <<<"$input" \
  >> "$log_dir/$(date -u +%F).jsonl"
 
exit 0

同じスクリプトをElicitationElicitationResultの両方に登録すれば、「何を要求されたか」と「実際に何を送り返したか」を突き合わせられます。フォームモードならrequested_schemaと実際のcontentの差、URLモードなら要求されたurlだけがログに残り、送信内容そのものはクライアントの外に出ません。

レシピ: 信頼できないサーバーの機密情報要求を自動でブロックする

許可リストに無いサーバーがフォームモードでパスワードらしき値を求めてきたら、ダイアログを出す前に拒否します。

#!/bin/bash
# Elicitation: 許可リスト外サーバーの機密情報らしき要求を自動拒否する
input=$(cat)
server=$(jq -r '.mcp_server_name' <<<"$input")
mode=$(jq -r '.mode' <<<"$input")
message=$(jq -r '.message' <<<"$input")
trusted=("internal-crm" "billing-server")
 
is_trusted=false
for s in "${trusted[@]}"; do
  [[ "$server" == "$s" ]] && is_trusted=true
done
 
if [[ "$mode" == "form" && "$is_trusted" == false ]] \
  && grep -qiE 'password|api.?key|token|secret' <<<"$message"; then
  echo "Blocked: untrusted server requested sensitive info via form mode" >&2
  exit 2
fi
 
exit 0

このスクリプトはメッセージ文字列だけを見た簡易判定です。requested_schemaのプロパティ名まで見て判定を厳密にする、mode: "url"の場合はurlのドメインを許可リストと照合する、といった拡張が現実的な次の一歩になります。

Elicitation/ElicitationResultの使い分け早見表

観点ElicitationElicitationResult
発火タイミングElicitationサーバーが入力を要求した直後ElicitationResult応答が確定し、送信される直前
対象になる応答Elicitationまだ存在しない(これから作る)ElicitationResult既に決まったaction/content
exit 2の効果Elicitation要求そのものを拒否ElicitationResult応答を強制的にdecline
主な用途Elicitationダイアログを出さず自動応答するElicitationResult事後検査で最後にもう一度止める
対応ハンドラー型Elicitationcommand / http / mcp_toolのみElicitationResultcommand / http / mcp_toolのみ

両方を同じ判定ロジックで二重に仕込むより、Elicitationは「そもそもダイアログを出したくない定型パターン」、ElicitationResultは「想定外の値が紛れ込んでいないかの最終チェック」と役割を分けたほうが、それぞれのスクリプトを単純に保てます。

よくあるつまずき

  • mcp__server__.*形式のmatcherを書いてしまう: Elicitation/ElicitationResultのmatcherはツール名ではなくサーバー名そのものです。プレフィックスを付けると一致しません
  • exit 2hookSpecificOutputを同時に使おうとする: Elicitation/ElicitationResultではexit 2のときだけhookSpecificOutputが無視されます。応答内容を指定して拒否したいのではなく、単に拒否したいだけならexit 2のみで十分です
  • systemMessageでユーザーに通知できると思い込む: この2つのイベントはsystemMessagecontinueを破棄します。通知が必要ならWebhookやログファイルなど別の経路を自分で用意します
  • URLモードのURLをそのまま信頼してブロック判定に使わない: URLモードは機密情報を帯域外でやり取りするための仕組みで、クライアント側はURLの中身を検査できない設計です。ドメイン単位の許可リストで判定するのが現実的な線です
  • Elicitationだけ仕込めば十分だと考える: フックの取りこぼしやサーバー側の仕様違反に備えるなら、ElicitationResultも併せて仕込んでおくほうが取りこぼしを減らせます

まとめ

ElicitationはMCPサーバーからの入力要求そのものを横取りして自動応答に置き換えるフックで、ElicitationResultはその応答が実際にサーバーへ送られる直前にもう一度観察・上書き・ブロックできるフックです。matcherがツール名ではなくサーバー名を見る点、prompt/agent型のハンドラーが使えず判定ロジックを自前のスクリプトで書く必要がある点、exit 2のときはhookSpecificOutputが無視される点は、他のフックイベントと勝手が違うのでそのまま覚えておく価値があります。ダイアログの代わりに自動応答させるだけならログ記録から、機密情報の混入を止めたいならElicitationResultでの事後検査から、それぞれ小さく試すのが安全な始め方です。MCPサーバーの権限設計まで含めて見直したい場合はMCPセキュリティガイドも参考になります。

この記事を共有:XはてブLinkedIn