Agent SDK canUseToolで承認と確認質問を処理する
Agent SDKのcanUseToolコールバックでツール承認リクエストとAskUserQuestionの確認質問をアプリ側から処理する方法を解説します。
canUseToolが呼ばれるのは何が決まらなかったときか
Claudeがユーザーの判断を待つ場面は2つあります。ツールを使う許可が必要なときと、AskUserQuestionツールで確認質問をするときです。どちらもcanUseToolコールバックを呼び、コールバックが値を返すまで実行は止まります。Claudeが応答を終えて次のメッセージを待つ通常の会話ターンとは別の挙動です。
確認質問の質問と選択肢を作るのはClaudeです。アプリ側は、それをユーザーへ見せて選ばれた回答を返すだけです。アプリ独自の質問をこの流れに足すことはできません。
コールバックは無期限に保留できます。権限プロンプトにタイムアウトはなく、コールバックが値を返すまで実行は止まります。ユーザーの返事がプロセスの稼働時間より遅れそうなときは、コールバックで待たずPreToolUseフックからdefer判定を返します。プロセスを終了し、保存済みのセッションから後で再開する形です。deferが効くのは-p付きの非対話モードだけです。対話セッションでは警告が出て、フックの結果は無視されます。
呼ばれない呼び出しを先に見分ける
canUseToolは権限評価の最後の段です。手前の段で承認か拒否が決まった呼び出しには届きません。評価は次の順に進みます。
権限評価の順序
- 1
フック
最初に走ります。
PreToolUseフックは呼び出しを拒否できます。フックがallowを返しても、後続の拒否ルールと確認ルールは評価されます。 - 2
拒否ルール
disallowed_toolsやsettings.jsonのdenyに一致すれば、bypassPermissionsでもブロックされます。 - 3
確認ルール
askルールに一致した呼び出しはコールバックへ回ります。bypassPermissionsでも同じです。 - 4
権限モード
bypassPermissionsは、ここへ届いた呼び出しをほぼ承認します。acceptEditsはファイル操作を承認し、planは編集系とシェル書き込み系をコールバックへ回します。touchやrmのようなシェル書き込み系が届くのは、Claude Code v2.1.212以降です。 - 5
許可ルール
allowed_toolsやsettings.jsonのallowに一致すれば承認されます。作業ディレクトリ内の読み取りや読み取り専用のBashコマンドは、ルールなしでもここで承認されます。 - 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通りです。
previewFormat | previewの中身 |
|---|---|
| 未設定(既定) | 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実例カタログで補えます。