承認とユーザー入力を処理する
Claude の承認リクエストと確認質問をユーザーに表示し、その決定を SDK に返します。
タスクに取り組んでいる間、Claude はユーザーに確認を取る必要がある場合があります。ファイルを削除する前に許可が必要な場合もあれば、新しいプロジェクト用にどのデータベースを使用するかを尋ねる必要がある場合もあります。アプリケーションはこれらのリクエストをユーザーに表示して、Claude がユーザーの入力で続行できるようにする必要があります。
Claude がユーザー入力をリクエストするのは 2 つの状況です。ツールを使用する許可が必要な場合(ファイルの削除やコマンドの実行など)と、確認質問がある場合(AskUserQuestion ツール経由)です。どちらも canUseTool コールバックをトリガーし、応答を返すまで実行を一時停止します。これは Claude が終了して次のメッセージを待つ通常の会話ターンとは異なります。
確認質問については、Claude が質問とオプションを生成します。あなたの役割は、それらをユーザーに提示して、ユーザーの選択を返すことです。このフローに独自の質問を追加することはできません。ユーザーに何か尋ねる必要がある場合は、アプリケーションロジックで別途実行してください。
コールバックは無期限に保留中のままにすることができます。実行はコールバックが返されるまで一時停止したままであり、SDK はクエリ自体がキャンセルされた場合にのみ待機をキャンセルします。ユーザーがプロセスが合理的に実行し続けることができるより長く応答するのに時間がかかる可能性がある場合、defer フック決定を返します。これにより、プロセスを終了して、後で永続化されたセッションから再開できます。
このガイドでは、各タイプのリクエストを検出し、適切に応答する方法を示します。
Claude が入力を必要とする場合を検出する
クエリオプションで canUseTool コールバックを渡します。Claude がユーザー入力を必要とするたびにコールバックが発火し、ツール名と入力を引数として受け取ります。
async def handle_tool_request(tool_name, input_data, context):
# ユーザーにプロンプトを表示して、許可または拒否を返す
...
options = ClaudeAgentOptions(can_use_tool=handle_tool_request)
async function handleToolRequest(toolName, input, options) {
// options には { signal: AbortSignal, suggestions?: PermissionUpdate[] } が含まれます
// ユーザーにプロンプトを表示して、許可または拒否を返す
}
const options = { canUseTool: handleToolRequest };
コールバックは 2 つのケースで発火します。
- ツールが承認を必要とする場合:Claude が 許可ルールまたはモードによって自動承認されていないツールを使用したい場合。
tool_nameでツール(例:"Bash"、"Write")を確認します。 - Claude が質問をする場合:Claude が
AskUserQuestionツールを呼び出します。tool_name == "AskUserQuestion"をチェックして、異なる方法で処理します。tools配列を指定する場合は、これが機能するようにAskUserQuestionを含めます。詳細は 確認質問を処理するを参照してください。
ユーザーにプロンプトを表示せずにツールを自動的に許可または拒否するには、代わりに フックを使用します。フックは canUseTool の前に実行され、独自のロジックに基づいてリクエストを許可、拒否、または変更できます。また、PermissionRequest フックを使用して、Claude が承認を待っているときに外部通知(Slack、メール、プッシュ)を送信することもできます。
ツール承認リクエストを処理する
クエリオプションで canUseTool コールバックを渡すと、Claude が自動承認されていないツールを使用したい場合に発火します。コールバックは 3 つの引数を受け取ります。
| 引数 | 説明 |
|---|---|
toolName |
Claude が使用したいツールの名前(例:"Bash"、"Write"、"Edit") |
input |
Claude がツールに渡しているパラメーター。内容はツールによって異なります。 |
options(TS)/ context(Python) |
再度プロンプトを表示しないための提案された PermissionUpdate エントリを含むオプション suggestions とキャンセル信号を含む追加コンテキスト。TypeScript では、signal は AbortSignal です。Python では、信号フィールドは将来の使用のために予約されています。Python については ToolPermissionContextを参照してください。 |
input オブジェクトにはツール固有のパラメーターが含まれます。一般的な例:
| ツール | 入力フィールド |
|---|---|
Bash |
command、description、timeout |
Write |
file_path、content |
Edit |
file_path、old_string、new_string |
Read |
file_path、offset、limit |
完全な入力スキーマについては SDK リファレンスを参照してください。Python | TypeScript。
この情報をユーザーに表示して、アクションを許可するか拒否するかを決定してから、適切な応答を返すことができます。
次の例では、Claude にテストファイルを作成して削除するよう要求します。Claude が各操作を試みるたびに、コールバックはツールリクエストをターミナルに出力し、y/n 承認を求めます。
import asyncio
from claude_agent_sdk import ClaudeAgentOptions, ResultMessage, query
from claude_agent_sdk.types import (
HookMatcher,
PermissionResultAllow,
PermissionResultDeny,
ToolPermissionContext,
)
async def can_use_tool(
tool_name: str, input_data: dict, context: ToolPermissionContext
) -> PermissionResultAllow | PermissionResultDeny:
# ツールリクエストを表示する
print(f"\nTool: {tool_name}")
if tool_name == "Bash":
print(f"Command: {input_data.get('command')}")
if input_data.get("description"):
print(f"Description: {input_data.get('description')}")
else:
print(f"Input: {input_data}")
# ユーザーの承認を取得する
response = input("Allow this action? (y/n): ")
# ユーザーの応答に基づいて許可または拒否を返す
if response.lower() == "y":
# 許可:ツールは元の(または変更された)入力で実行される
return PermissionResultAllow(updated_input=input_data)
else:
# 拒否:ツールは実行されず、Claude はメッセージを見る
return PermissionResultDeny(message="User denied this action")
# 必須の回避策:ダミーフックはストリームを canUseTool 用に開いたままにします
async def dummy_hook(input_data, tool_use_id, context):
return {"continue_": True}
async def prompt_stream():
yield {
"type": "user",
"message": {
"role": "user",
"content": "Create a test file in /tmp and then delete it",
},
}
async def main():
async for message in query(
prompt=prompt_stream(),
options=ClaudeAgentOptions(
can_use_tool=can_use_tool,
hooks={"PreToolUse": [HookMatcher(matcher=None, hooks=[dummy_hook])]},
),
):
if isinstance(message, ResultMessage) and message.subtype == "success":
print(message.result)
asyncio.run(main())
import { query } from "@anthropic-ai/claude-agent-sdk";
import * as readline from "readline";
// ターミナルでユーザー入力を求めるヘルパー
function prompt(question: string): Promise<string> {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
return new Promise((resolve) =>
rl.question(question, (answer) => {
rl.close();
resolve(answer);
})
);
}
for await (const message of query({
prompt: "Create a test file in /tmp and then delete it",
options: {
canUseTool: async (toolName, input) => {
// ツールリクエストを表示する
console.log(`\nTool: ${toolName}`);
if (toolName === "Bash") {
console.log(`Command: ${input.command}`);
if (input.description) console.log(`Description: ${input.description}`);
} else {
console.log(`Input: ${JSON.stringify(input, null, 2)}`);
}
// ユーザーの承認を取得する
const response = await prompt("Allow this action? (y/n): ");
// ユーザーの応答に基づいて許可または拒否を返す
if (response.toLowerCase() === "y") {
// 許可:ツールは元の(または変更された)入力で実行される
return { behavior: "allow", updatedInput: input };
} else {
// 拒否:ツールは実行されず、Claude はメッセージを見る
return { behavior: "deny", message: "User denied this action" };
}
}
}
})) {
if ("result" in message) console.log(message.result);
}
Python では、can_use_tool は ストリーミングモードと {"continue_": True} を返す PreToolUse フックが必要で、ストリームを開いたままにします。このフックがないと、許可コールバックが呼び出される前にストリームが閉じます。
この例では y/n フローを使用しており、y 以外の入力は拒否として扱われます。実際には、ユーザーがリクエストを変更したり、フィードバックを提供したり、Claude を完全にリダイレクトしたりできるより豊富な UI を構築する可能性があります。すべての応答方法については ツールリクエストに応答するを参照してください。
ツールリクエストに応答する
コールバックは 2 つの応答タイプのいずれかを返します。
| 応答 | Python | TypeScript |
|---|---|---|
| 許可 | PermissionResultAllow(updated_input=...) |
{ behavior: "allow", updatedInput } |
| 拒否 | PermissionResultDeny(message=...) |
{ behavior: "deny", message } |
許可する場合、ツール入力(元の、または変更された)を渡します。拒否する場合、理由を説明するメッセージを提供します。Claude はこのメッセージを見て、アプローチを調整する可能性があります。
from claude_agent_sdk.types import PermissionResultAllow, PermissionResultDeny
# ツールの実行を許可する
return PermissionResultAllow(updated_input=input_data)
# ツールをブロックする
return PermissionResultDeny(message="User rejected this action")
// ツールの実行を許可する
return { behavior: "allow", updatedInput: input };
// ツールをブロックする
return { behavior: "deny", message: "User rejected this action" };
許可または拒否を超えて、ツールの入力を変更したり、Claude がアプローチを調整するのに役立つコンテキストを提供したりできます。
- 承認:ツールを Claude がリクエストしたとおりに実行させる
- 変更を加えて承認:実行前に入力を変更する(例:パスをサニタイズ、制約を追加)
- 承認して記憶:提案された許可ルールをエコーバックして、一致する呼び出しが次回プロンプトをスキップするようにする
- 拒否:ツールをブロックして Claude に理由を伝える
- 代替案を提案:ブロックするが、ユーザーが望むものに向かって Claude をガイドする
- 完全にリダイレクト:ストリーミング入力を使用して Claude に完全に新しい指示を送信する
ユーザーはアクションをそのまま承認します。コールバックから input をそのまま渡し、ツールは Claude がリクエストしたとおりに実行されます。
async def can_use_tool(tool_name, input_data, context):
print(f"Claude wants to use {tool_name}")
approved = await ask_user("Allow this action?")
if approved:
return PermissionResultAllow(updated_input=input_data)
return PermissionResultDeny(message="User declined")
canUseTool: async (toolName, input) => {
console.log(`Claude wants to use ${toolName}`);
const approved = await askUser("Allow this action?");
if (approved) {
return { behavior: "allow", updatedInput: input };
}
return { behavior: "deny", message: "User declined" };
};
ユーザーは承認しますが、最初にリクエストを変更したいと考えています。ツールが実行される前に入力を変更できます。Claude は結果を見ますが、何かを変更したことは伝えられません。パラメーターをサニタイズ、制約を追加、またはアクセスをスコープするのに役立ちます。
async def can_use_tool(tool_name, input_data, context):
if tool_name == "Bash":
# ユーザーが承認しましたが、すべてのコマンドをサンドボックスにスコープします
sandboxed_input = {**input_data}
sandboxed_input["command"] = input_data["command"].replace(
"/tmp", "/tmp/sandbox"
)
return PermissionResultAllow(updated_input=sandboxed_input)
return PermissionResultAllow(updated_input=input_data)
canUseTool: async (toolName, input) => {
if (toolName === "Bash") {
// ユーザーが承認しましたが、すべてのコマンドをサンドボックスにスコープします
const sandboxedInput = {
...input,
command: input.command.replace("/tmp", "/tmp/sandbox")
};
return { behavior: "allow", updatedInput: sandboxedInput };
}
return { behavior: "allow", updatedInput: input };
};
ユーザーが承認し、この種の呼び出しについて再度尋ねられたくない場合。3 番目のコールバック引数は suggestions を含み、これは準備完了した PermissionUpdate エントリの配列です。updatedPermissions で 1 つをエコーバックして適用します。localSettings 宛先を持つ提案は、ルールを .claude/settings.local.json に書き込むため、将来のセッションは一致する呼び出しのプロンプトをスキップします。
Python の例には claude-agent-sdk 0.1.80 以降が必要です。
async def can_use_tool(tool_name, input_data, context):
choice = await ask_user(f"Allow {tool_name}?", ["once", "always", "no"])
if choice == "always":
persist = [
s for s in context.suggestions if s.destination == "localSettings"
]
return PermissionResultAllow(
updated_input=input_data, updated_permissions=persist
)
if choice == "once":
return PermissionResultAllow(updated_input=input_data)
return PermissionResultDeny(message="User declined")
canUseTool: async (toolName, input, { suggestions = [] }) => {
const choice = await askUser(`Allow ${toolName}?`, ["once", "always", "no"]);
if (choice === "always") {
const persist = suggestions.filter(
(s) => s.destination === "localSettings"
);
return {
behavior: "allow",
updatedInput: input,
updatedPermissions: persist
};
}
if (choice === "once") {
return { behavior: "allow", updatedInput: input };
}
return { behavior: "deny", message: "User declined" };
};
ユーザーはこのアクションが実行されることを望んでいません。ツールをブロックして、理由を説明するメッセージを提供します。Claude はこのメッセージを見て、別のアプローチを試す可能性があります。
async def can_use_tool(tool_name, input_data, context):
approved = await ask_user(f"Allow {tool_name}?")
if not approved:
return PermissionResultDeny(message="User rejected this action")
return PermissionResultAllow(updated_input=input_data)
canUseTool: async (toolName, input) => {
const approved = await askUser(`Allow ${toolName}?`);
if (!approved) {
return {
behavior: "deny",
message: "User rejected this action"
};
}
return { behavior: "allow", updatedInput: input };
};
ユーザーはこの特定のアクションを望んでいませんが、別のアイデアがあります。ツールをブロックして、メッセージにガイダンスを含めます。Claude はこれを読んで、フィードバックに基づいて進行方法を決定します。
async def can_use_tool(tool_name, input_data, context):
if tool_name == "Bash" and "rm" in input_data.get("command", ""):
# ユーザーは削除を望んでいません。代わりにアーカイブに圧縮することを提案します
return PermissionResultDeny(
message="User doesn't want to delete files. They asked if you could compress them into an archive instead."
)
return PermissionResultAllow(updated_input=input_data)
canUseTool: async (toolName, input) => {
if (toolName === "Bash" && input.command.includes("rm")) {
// ユーザーは削除を望んでいません。代わりにアーカイブに圧縮することを提案します
return {
behavior: "deny",
message:
"User doesn't want to delete files. They asked if you could compress them into an archive instead."
};
}
return { behavior: "allow", updatedInput: input };
};
方向の完全な変更(単なるナッジではなく)の場合は、ストリーミング入力を使用して Claude に新しい指示を直接送信します。これは現在のツールリクエストをバイパスし、Claude に完全に新しい指示に従うように指示します。
確認質問を処理する
Claude が複数の有効なアプローチを持つタスクについてさらに方向性が必要な場合、AskUserQuestion ツールを呼び出します。これは toolName が AskUserQuestion に設定された canUseTool コールバックをトリガーします。入力には Claude の質問が複数選択肢として含まれており、これらをユーザーに表示して、ユーザーの選択を返します。
確認質問は特に plan モードで一般的です。Claude はコードベースを探索し、計画を提案する前に質問をします。これにより、プラン モードは Claude が変更を加える前に要件を収集したい対話的なワークフローに最適です。
次のステップは、確認質問を処理する方法を示しています。
canUseTool コールバックを渡す
クエリオプションで canUseTool コールバックを渡します。デフォルトでは、AskUserQuestion が利用可能です。Claude の機能を制限するために tools 配列を指定する場合(例:Read、Glob、Grep のみを持つ読み取り専用エージェント)、その配列に AskUserQuestion を含めます。そうしないと、Claude は確認質問をすることができません。
async for message in query(
prompt="Analyze this codebase",
options=ClaudeAgentOptions(
# ツールリストに AskUserQuestion を含める
tools=["Read", "Glob", "Grep", "AskUserQuestion"],
can_use_tool=can_use_tool,
),
):
print(message)
for await (const message of query({
prompt: "Analyze this codebase",
options: {
// ツールリストに AskUserQuestion を含める
tools: ["Read", "Glob", "Grep", "AskUserQuestion"],
canUseTool: async (toolName, input) => {
// ここで確認質問を処理する
}
}
})) {
console.log(message);
}
AskUserQuestion を検出する
コールバックで、toolName が AskUserQuestion と等しいかどうかをチェックして、他のツールとは異なる方法で処理します。
async def can_use_tool(tool_name: str, input_data: dict, context):
if tool_name == "AskUserQuestion":
# ユーザーから回答を収集するための実装
return await handle_clarifying_questions(input_data)
# 他のツールを通常どおり処理する
return await prompt_for_approval(tool_name, input_data)
canUseTool: async (toolName, input) => {
if (toolName === "AskUserQuestion") {
// ユーザーから回答を収集するための実装
return handleClarifyingQuestions(input);
}
// 他のツールを通常どおり処理する
return promptForApproval(toolName, input);
};
質問入力を解析する
入力には Claude の質問が questions 配列に含まれています。各質問には question(表示するテキスト)、options(選択肢)、multiSelect(複数選択が許可されているかどうか)があります。
{
"questions": [
{
"question": "How should I format the output?",
"header": "Format",
"options": [
{ "label": "Summary", "description": "Brief overview" },
{ "label": "Detailed", "description": "Full explanation" }
],
"multiSelect": false
},
{
"question": "Which sections should I include?",
"header": "Sections",
"options": [
{ "label": "Introduction", "description": "Opening context" },
{ "label": "Conclusion", "description": "Final summary" }
],
"multiSelect": true
}
]
}
完全なフィールド説明については 質問形式を参照してください。
ユーザーから回答を収集する
質問をユーザーに提示して、ユーザーの選択を収集します。これをどのように行うかは、アプリケーションによって異なります。ターミナルプロンプト、Web フォーム、モバイルダイアログなど。
Claude に回答を返す
answers オブジェクトをレコードとして構築します。各キーは question テキストで、各値は選択されたオプションの label です。
| 質問オブジェクトから | 使用方法 |
|---|---|
question フィールド(例:"How should I format the output?") |
キー |
選択されたオプションの label フィールド(例:"Summary") |
値 |
複数選択質問の場合、ラベルの配列を渡すか、", " で結合します。自由テキスト入力をサポートする場合は、ユーザーのカスタムテキストを値として使用します。
return PermissionResultAllow(
updated_input={
"questions": input_data.get("questions", []),
"answers": {
"How should I format the output?": "Summary",
"Which sections should I include?": ["Introduction", "Conclusion"],
},
}
)
return {
behavior: "allow",
updatedInput: {
questions: input.questions,
answers: {
"How should I format the output?": "Summary",
"Which sections should I include?": "Introduction, Conclusion"
}
}
};
質問形式
入力には Claude が生成した質問が questions 配列に含まれています。各質問には以下のフィールドがあります。
| フィールド | 説明 |
|---|---|
question |
表示する完全な質問テキスト |
header |
質問の短いラベル(最大 12 文字) |
options |
2~4 個の選択肢の配列。各選択肢には label と description があります。TypeScript:オプションで preview(下記の オプションプレビューを参照) |
multiSelect |
true の場合、ユーザーは複数のオプションを選択できます |
コールバックが受け取る構造:
{
"questions": [
{
"question": "How should I format the output?",
"header": "Format",
"options": [
{ "label": "Summary", "description": "Brief overview of key points" },
{ "label": "Detailed", "description": "Full explanation with examples" }
],
"multiSelect": false
}
]
}
オプションプレビュー(TypeScript)
toolConfig.askUserQuestion.previewFormat は各オプションに preview フィールドを追加して、アプリがラベルと一緒に視覚的なモックアップを表示できるようにします。この設定がない場合、Claude はプレビューを生成せず、フィールドは存在しません。
previewFormat |
preview に含まれるもの |
|---|---|
| 未設定(デフォルト) | フィールドは存在しません。Claude はプレビューを生成しません。 |
"markdown" |
ASCII アートとフェンスコードブロック |
"html" |
スタイル付き <div> フラグメント(SDK はコールバックが実行される前に <script>、<style>、<!DOCTYPE> を拒否します) |
形式はセッション内のすべての質問に適用されます。Claude は視覚的な比較が役立つオプション(レイアウト選択、配色)に preview を含め、役立たないオプション(はい/いいえの確認、テキストのみの選択)では省略します。レンダリング前に undefined をチェックしてください。
import { query } from "@anthropic-ai/claude-agent-sdk";
for await (const message of query({
prompt: "Help me choose a card layout",
options: {
toolConfig: {
askUserQuestion: { previewFormat: "html" }
},
canUseTool: async (toolName, input) => {
// input.questions[].options[].preview は HTML 文字列または undefined です
return { behavior: "allow", updatedInput: input };
}
}
})) {
// ...
}
HTML プレビュー付きのオプション:
{
"label": "Compact",
"description": "Title and metric value only",
"preview": "<div style=\"padding:12px;border:1px solid #ddd;border-radius:8px\"><div style=\"font-size:12px;color:#666\">Active users</div><div style=\"font-size:28px;font-weight:600\">1,284</div></div>"
}
応答形式
各質問の question フィールドを選択されたオプションの label にマップする answers オブジェクトを返します。
| フィールド | 説明 |
|---|---|
questions |
元の質問配列をパススルーする(ツール処理に必須) |
answers |
キーが質問テキストで、値が選択されたラベルであるオブジェクト |
複数選択質問の場合、ラベルの配列を渡すか、", " で結合します。自由テキスト入力の場合は、ユーザーのカスタムテキストを直接使用します。
{
"questions": [
// ...
],
"answers": {
"How should I format the output?": "Summary",
"Which sections should I include?": ["Introduction", "Conclusion"]
}
}
自由テキスト入力をサポートする
Claude の定義済みオプションがユーザーが望むものをカバーしていない場合があります。ユーザーが独自の回答を入力できるようにするには:
- Claude のオプションの後に追加の「その他」選択肢を表示して、テキスト入力を受け入れます
- ユーザーのカスタムテキストを回答値として使用します(「その他」という単語ではなく)
完全な実装については、下記の 完全な例を参照してください。
完全な例
Claude は、タスクを進めるためにユーザー入力が必要な場合に確認質問をします。たとえば、モバイルアプリのテックスタックを決定するのに役立つよう求められた場合、Claude はクロスプラットフォーム対応 vs ネイティブ、バックエンド設定、またはターゲットプラットフォームについて質問する可能性があります。これらの質問は、Claude がユーザーの設定に合致する決定を下すのに役立ちます。推測ではなく。
この例は、ターミナルアプリケーションでこれらの質問を処理します。各ステップで何が起こるかは以下の通りです。
- リクエストをルーティングする:
canUseToolコールバックはツール名が"AskUserQuestion"であるかどうかをチェックし、専用ハンドラーにルーティングします - 質問を表示する:ハンドラーは
questions配列をループして、各質問を番号付きオプションで出力します - 入力を収集する:ユーザーはオプションを選択する番号を入力するか、自由テキストを直接入力できます(例:「jquery」、「i don't know」)
- 回答をマップする:コードは入力が数値(オプションのラベルを使用)か自由テキスト(テキストを直接使用)かをチェックします
- Claude に返す:応答には元の
questions配列とanswersマッピングの両方が含まれます
import asyncio
from claude_agent_sdk import ClaudeAgentOptions, ResultMessage, query
from claude_agent_sdk.types import HookMatcher, PermissionResultAllow
def parse_response(response: str, options: list) -> str:
"""ユーザー入力をオプション番号または自由テキストとして解析します。"""
try:
indices = [int(s.strip()) - 1 for s in response.split(",")]
labels = [options[i]["label"] for i in indices if 0 <= i < len(options)]
return ", ".join(labels) if labels else response
except ValueError:
return response
async def handle_ask_user_question(input_data: dict) -> PermissionResultAllow:
"""Claude の質問を表示してユーザーの回答を収集します。"""
answers = {}
for q in input_data.get("questions", []):
print(f"\n{q['header']}: {q['question']}")
options = q["options"]
for i, opt in enumerate(options):
print(f" {i + 1}. {opt['label']} - {opt['description']}")
if q.get("multiSelect"):
print(" (Enter numbers separated by commas, or type your own answer)")
else:
print(" (Enter a number, or type your own answer)")
response = input("Your choice: ").strip()
answers[q["question"]] = parse_response(response, options)
return PermissionResultAllow(
updated_input={
"questions": input_data.get("questions", []),
"answers": answers,
}
)
async def can_use_tool(
tool_name: str, input_data: dict, context
) -> PermissionResultAllow:
# AskUserQuestion を質問ハンドラーにルーティングする
if tool_name == "AskUserQuestion":
return await handle_ask_user_question(input_data)
# この例では他のツールを自動承認する
return PermissionResultAllow(updated_input=input_data)
async def prompt_stream():
yield {
"type": "user",
"message": {
"role": "user",
"content": "Help me decide on the tech stack for a new mobile app",
},
}
# 必須の回避策:ダミーフックはストリームを canUseTool 用に開いたままにします
async def dummy_hook(input_data, tool_use_id, context):
return {"continue_": True}
async def main():
async for message in query(
prompt=prompt_stream(),
options=ClaudeAgentOptions(
can_use_tool=can_use_tool,
hooks={"PreToolUse": [HookMatcher(matcher=None, hooks=[dummy_hook])]},
),
):
if isinstance(message, ResultMessage) and message.subtype == "success":
print(message.result)
asyncio.run(main())
import { query } from "@anthropic-ai/claude-agent-sdk";
import * as readline from "readline/promises";
// ターミナルでユーザー入力を求めるヘルパー
async function prompt(question: string): Promise<string> {
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
const answer = await rl.question(question);
rl.close();
return answer;
}
// ユーザー入力をオプション番号または自由テキストとして解析する
function parseResponse(response: string, options: any[]): string {
const indices = response.split(",").map((s) => parseInt(s.trim()) - 1);
const labels = indices
.filter((i) => !isNaN(i) && i >= 0 && i < options.length)
.map((i) => options[i].label);
return labels.length > 0 ? labels.join(", ") : response;
}
// Claude の質問を表示してユーザーの回答を収集する
async function handleAskUserQuestion(input: any) {
const answers: Record<string, string> = {};
for (const q of input.questions) {
console.log(`\n${q.header}: ${q.question}`);
const options = q.options;
options.forEach((opt: any, i: number) => {
console.log(` ${i + 1}. ${opt.label} - ${opt.description}`);
});
if (q.multiSelect) {
console.log(" (Enter numbers separated by commas, or type your own answer)");
} else {
console.log(" (Enter a number, or type your own answer)");
}
const response = (await prompt("Your choice: ")).trim();
answers[q.question] = parseResponse(response, options);
}
// Claude に回答を返す(ツール処理に元の質問を含める必須)
return {
behavior: "allow",
updatedInput: { questions: input.questions, answers }
};
}
async function main() {
for await (const message of query({
prompt: "Help me decide on the tech stack for a new mobile app",
options: {
canUseTool: async (toolName, input) => {
// AskUserQuestion を質問ハンドラーにルーティングする
if (toolName === "AskUserQuestion") {
return handleAskUserQuestion(input);
}
// この例では他のツールを自動承認する
return { behavior: "allow", updatedInput: input };
}
}
})) {
if ("result" in message) console.log(message.result);
}
}
main();
制限事項
- サブエージェント:
AskUserQuestionは現在、Agent ツール経由で生成されたサブエージェントでは利用できません - 質問の制限:各
AskUserQuestion呼び出しは 1~4 個の質問と 2~4 個のオプションをサポートします
ユーザー入力を取得する他の方法
canUseTool コールバックと AskUserQuestion ツールはほとんどの承認と明確化のシナリオをカバーしていますが、SDK はユーザーから入力を取得する他の方法を提供しています。
ストリーミング入力
以下が必要な場合は ストリーミング入力を使用します。
- エージェントをタスク途中で中断する:Claude が作業中にキャンセル信号を送信するか、方向を変更する
- 追加コンテキストを提供する:Claude が尋ねるのを待たずに、Claude が必要とする情報を追加する
- チャットインターフェースを構築する:長時間実行される操作中にユーザーがエージェントと対話できるようにする
ストリーミング入力は、承認チェックポイントだけでなく、実行全体を通じてユーザーがエージェントと対話する会話型 UI に最適です。
カスタムツール
以下が必要な場合は カスタムツールを使用します。
- 構造化入力を収集する:
AskUserQuestionの複数選択形式を超えたフォーム、ウィザード、または複数ステップのワークフローを構築する - 外部承認システムを統合する:既存のチケット、ワークフロー、または承認プラットフォームに接続する
- ドメイン固有の対話を実装する:コードレビューインターフェースやデプロイメントチェックリストなど、アプリケーションのニーズに合わせたツールを作成する
カスタムツールは対話を完全に制御できますが、組み込みの canUseTool コールバックを使用するよりも実装作業が必要です。
関連リソース
- 権限を設定する:権限モードとルールを設定する
- フックで実行を制御する:エージェントライフサイクルの重要なポイントでカスタムコードを実行する
- TypeScript SDK リファレンス:完全な canUseTool API ドキュメント