Claude Media
Agent SDK canUseToolで承認と確認質問を処理する

Agent SDK canUseToolで承認と確認質問を処理する

Agent SDKのcanUseToolコールバックでツール承認リクエストとAskUserQuestionの確認質問をアプリ側から処理する方法を解説します。

canUseToolが呼ばれるのは何が決まらなかったときか

Claudeがユーザーの判断を待つ場面は2つあります。ツールを使う許可が必要なときと、AskUserQuestionツールで確認質問をするときです。どちらもcanUseToolコールバックを呼び、コールバックが値を返すまで実行は止まります。Claudeが応答を終えて次のメッセージを待つ通常の会話ターンとは別の挙動です。

確認質問の質問と選択肢を作るのはClaudeです。アプリ側は、それをユーザーへ見せて選ばれた回答を返すだけです。アプリ独自の質問をこの流れに足すことはできません。

コールバックは無期限に保留できます。権限プロンプトにタイムアウトはなく、コールバックが値を返すまで実行は止まります。ユーザーの返事がプロセスの稼働時間より遅れそうなときは、コールバックで待たずPreToolUseフックからdefer判定を返します。プロセスを終了し、保存済みのセッションから後で再開する形です。deferが効くのは-p付きの非対話モードだけです。対話セッションでは警告が出て、フックの結果は無視されます。

呼ばれない呼び出しを先に見分ける

canUseToolは権限評価の最後の段です。手前の段で承認か拒否が決まった呼び出しには届きません。評価は次の順に進みます。

手順

権限評価の順序

  1. 1

    フック

    最初に走ります。PreToolUseフックは呼び出しを拒否できます。フックがallowを返しても、後続の拒否ルールと確認ルールは評価されます。

  2. 2

    拒否ルール

    disallowed_toolsやsettings.jsonのdenyに一致すれば、bypassPermissionsでもブロックされます。

  3. 3

    確認ルール

    askルールに一致した呼び出しはコールバックへ回ります。bypassPermissionsでも同じです。

  4. 4

    権限モード

    bypassPermissionsは、ここへ届いた呼び出しをほぼ承認します。acceptEditsはファイル操作を承認し、planは編集系とシェル書き込み系をコールバックへ回します。touchやrmのようなシェル書き込み系が届くのは、Claude Code v2.1.212以降です。

  5. 5

    許可ルール

    allowed_toolsやsettings.jsonのallowに一致すれば承認されます。作業ディレクトリ内の読み取りや読み取り専用のBashコマンドは、ルールなしでもここで承認されます。

  6. 6

    canUseToolコールバック

    ここまでで決まらなかった呼び出しだけが届きます。

この順序から、見落としやすい例外が読み取れます。dontAsk以外のモードでは、AskUserQuestionは許可ルールに一致しても必ずコールバックへ届きます。planモードでは、許可ルールがあっても編集系ツールがコールバックへ回ります。逆にdontAskモードは、本来プロンプトになる呼び出しをコールバックなしで拒否します。許可済みの呼び出しは実行され、AskUserQuestionは拒否側です。

MCPサーバーのツールにも、同じ扱いになるものがあります。_meta["anthropic/requiresUserInteraction"]を設定したサーバーのツールは、許可ルールがあってもコールバックへ届きます。この注釈にはClaude Code v2.1.199以降が必要です。rmとrmdirが重要なパスを対象にする場合は、許可ルールでもフックのallowでも承認されません。bypassPermissionsでもコールバックへ回り、autoモードのSDKセッションでは既定でコールバックなしに拒否されます。

canUseToolが手前の段に隠れる設定は、TypeScript SDKが検出します。クエリの構築時にNode.jsのプロセス警告を1回出し、警告コードはCLAUDE_SDK_CAN_USE_TOOL_SHADOWEDです。permissionMode: 'bypassPermissions'の場合と、allowedToolsに"Read"のような名前だけの項目がある場合に出ます。PythonのSDKにも、can_use_toolをallowed_toolsやbypassPermissionsと併用すると出る実行時警告があります。

TypeScript SDKにはpermissionPrompts: 'none'というオプションもあります。この場合、最後の段でもコールバックは呼ばれず、PermissionRequestフックが判断し、フックが決めなければ拒否されます。Claude Code v2.1.259以降が前提です。

組織がaskに設定したclaude.aiコネクタのツールも、確認ルールの段でコールバックへ回ります。コールバックが受け取る理由文はYour organization requires approval for this toolです。

全ツール呼び出しに一様に効かせたい処理は、PreToolUseフックに書きます。承認待ちの間にSlackやメールで人へ通知したいときはPermissionRequestフックが使えます。

canUseToolを登録して中身を読む

クエリのオプションにcanUseToolを渡します。Claudeがユーザー入力を必要としたとき、ツール名と入力を引数にして呼ばれます。

async function handleToolRequest(toolName, input, options) {
  // optionsには { signal: AbortSignal, suggestions?: PermissionUpdate[] } が含まれる
  // ユーザーへ提示し、allowかdenyを返す
}
 
const options = { canUseTool: handleToolRequest };

ツール名(TypeScriptではtoolName、Pythonではtool_name)が"Bash"や"Write"なら承認待ちのツールで、"AskUserQuestion"なら確認質問です。tools配列でツールを絞っている場合は、AskUserQuestionを含めてください。含めないと、Claudeは確認質問自体ができません。

コールバックはtoolName・input・options(Pythonではcontext)の3引数を受け取ります。TypeScriptのoptionsにはsignalとsuggestionsが入ります。ほかにblockedPath・decisionReason・toolUseID・agentID・requestIdもあります。defaultToNoはv0.3.268以降、mcpServerはv0.3.274以降のフィールドです。

戻り値はPermissionResultかnullです。nullを返してよいのは、自前でcontrol_responseを送った場合だけです。それ以外でnullを返すと、ツール呼び出しは無期限にブロックされたままになります。

inputの中身はツールごとに違います。

ツール入力フィールド
Bash入力フィールドcommand・description・timeout
Write入力フィールドfile_path・content
Edit入力フィールドfile_path・old_string・new_string
Read入力フィールドfile_path・offset・limit

この内容を見せて、許可か拒否かをユーザーに選んでもらいます。

async def can_use_tool(tool_name, input_data, context):
    print(f"Tool: {tool_name}")
    if tool_name == "Bash":
        print(f"Command: {input_data.get('command')}")
    response = input("Allow this action? (y/n): ")
 
    if response.lower() == "y":
        return PermissionResultAllow(updated_input=input_data)
    else:
        return PermissionResultDeny(message="User denied this action")

応答の型は許可と拒否の2つです。PythonではPermissionResultAllowとPermissionResultDenyを返します。TypeScriptでは{ behavior: "allow", updatedInput }と{ behavior: "deny", message }です。許可するとき、Claudeの要求した入力をそのまま使う必要はありません。updatedInput(Python: updated_input)に書き換えた入力を渡せます。

v2.1.207より前のClaude Codeは、updatedInputを省いた許可応答をバリデーションエラーとして拒否していました。拒否のときは理由をmessageに入れます。

Pythonの公式サンプルは、有限のメッセージジェネレータをquery(prompt=generator)で渡します。その場合は、{"continue_": True}を返すだけのダミーのPreToolUseフックも登録しています。claude-agent-sdk 0.2.140以降は、文字列プロンプトのquery()でもcan_use_toolが使えます。

もう1つの方法は、ClaudeSDKClient.connect()をプロンプトなしで呼び、ClaudeSDKClient.query()でメッセージを送る形です。ストリーミングと単発クエリの違いはストリーミング入力とシングルメッセージ入力の使い分けにあります。

応答は5通りから選べる

許可と拒否の2択に加えて、入力の書き換えやClaudeへの文脈の伝達ができます。

まとめ

コールバックの5つの応答

  • 承認

    Claudeが要求したinputをそのまま返し、要求どおりに実行します。

  • 変更して承認

    パスのサニタイズや制約の追加など、inputを書き換えて実行します。Claudeには書き換えたことが伝わらず、結果だけを見ます。

  • 承認して記憶する

    第3引数のsuggestionsからPermissionUpdateを選び、updatedPermissionsに載せて返します。同種の呼び出しは次回からプロンプトが出ません。

  • 拒否

    ツールをブロックし、messageで理由をClaudeへ伝えます。

  • 代案を提示して拒否

    ブロックしつつ、ユーザーの希望をmessageに書きます。たとえば「削除ではなくアーカイブへ圧縮してほしい」と伝えると、Claudeが別の進め方を選びます。

「承認して記憶する」でdestinationがlocalSettingsのPermissionUpdateを返すと、ルールは.claude/settings.local.jsonへ書き込まれます。以降のセッションでも、一致する呼び出しのプロンプトはスキップされます。Pythonではclaude-agent-sdk 0.1.80以降が必要です。

TypeScriptでは、リクエストのoptionsがsuppressAlwaysAllowRule: trueを持つことがあります。その場合は「常に許可」の選択肢を出さないでください。このヒントはAgent SDK v0.3.268以降の機能で、Pythonのcontextには含まれません。

応答の枠を超えて方向転換させる手段もあります。ストリーミング入力で新しい指示を直接送れば、今のツールリクエストごと、Claudeの進路を完全に切り替えられます。

AskUserQuestionの質問と回答の形

複数の妥当な進め方があるタスクで、ClaudeはAskUserQuestionを呼びます。planモードで特に多く、計画を出す前にコードベースを調べながら質問します。コールバックではtool_nameを見て、他のツールと別扱いにします。

入力のquestions配列の各要素は、次のフィールドを持ちます。

フィールド内容
question内容表示する質問文
header内容質問の短いラベル(最大12文字)
options内容2〜4件の選択肢(labelとdescription)
multiSelect内容trueなら複数選択を許す
{
  "questions": [
    {
      "question": "How should I format the output?",
      "header": "Format",
      "options": [
        { "label": "Summary", "description": "Brief overview" },
        { "label": "Detailed", "description": "Full explanation" }
      ],
      "multiSelect": false
    }
  ]
}

応答はanswersオブジェクトで返します。キーは質問のquestionテキストそのもの、値は選ばれた選択肢のlabelです。元のquestions配列も、そのまま一緒に返す必要があります。複数選択ではラベルの配列か、", "で結合した文字列を渡します。

return {
  behavior: "allow",
  updatedInput: {
    questions: input.questions,
    answers: {
      "How should I format the output?": "Summary",
      "Which sections should I include?": "Introduction, Conclusion"
    }
  }
};

構造化された質問カードをユーザーが閉じて自由に返信できる画面を作る場合に限り、responseフィールドを使います。responseを設定すると、Claudeは質問ごとの回答一覧ではなく「ユーザーはこう返信しました」というメッセージを受け取ります。

TypeScript版のtoolConfig.askUserQuestion.previewFormatは、各選択肢にpreviewを足します。設定は次の3通りです。

previewFormatpreviewの中身
未設定(既定)previewの中身フィールド自体が存在しない
"markdown"previewの中身ASCIIアートとコードブロック
"html"previewの中身スタイル付きの<div>断片

"html"では、<script>・<style>・<!DOCTYPE>がコールバックに届く前に拒否されます。設定はセッション内の全質問に適用されます。Claudeはレイアウトや配色のように見た目の比較が役立つ選択肢にだけpreviewを付けるので、描画の前にundefinedを確認してください。

自由記述の回答を受け付ける

Claudeが用意した選択肢が、ユーザーの意図に合わないことはあります。選択肢の後に自由入力の「その他」を表示し、打ち込まれたテキストをそのままanswersの値にします。「Other」という単語を値にしてはいけません。

番号入力と自由記述を両方受け付けるなら、入力が数値として解釈できるかで振り分けます。数値なら対応する選択肢のlabel、そうでなければ入力テキストそのものを使います。複数選択では、カンマ区切りの番号をlabelの配列に変換する処理も要ります。

承認の枠に収まらない入力は別の手段で受ける

canUseToolとAskUserQuestionは、承認と選択式の確認に向いた仕組みです。それ以外の入力は、SDKの別の機能で受けます。

ストリーミング入力は、作業中のエージェントを中断したり、Claudeが尋ねる前に追加の文脈を渡したりするときに使います。作業の途中までユーザーが会話に参加するチャット型のUIにも合います。フォームや複数ステップのウィザードのように、選択式を超える構造化された入力が要るなら、カスタムツールを実装します。

よくあるつまずき

サブエージェントではAskUserQuestionが使えません。Agentツール経由で起動したサブエージェントの中からは、このツールを呼べません。確認質問が必要な処理は、メインのエージェントセッション側に持たせます。

質問数と選択肢数には上限があります。1回の呼び出しで1〜4問、各質問の選択肢は2〜4件です。それ以上の分岐が必要なら、質問を複数回に分けるか、カスタムツールでフォームやウィザードを実装します。

まとめ

canUseToolは、手前の段で決まらなかった呼び出しの最終判断を、アプリ側に委ねる口です。実装の前に、権限評価の順序のどこで自分の呼び出しが決まるかを確かめると、「呼ばれない」と「なぜか呼ばれる」の両方を避けられます。

権限モードと評価フローの全体像はClaude Code Planモード完全ガイドで、PreToolUseフックの実例はClaude Code Hooks実例カタログで補えます。

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