SpyBara
Go Premium

agent-sdk/user-input.md 2026-11-10 21:57 UTC to 2026-11-11 22:59 UTC

No changes for this document in this history range.

2026
Wed 4 23:59 Tue 10 21:57

承認とユーザー入力を処理する

Claude の承認リクエストと確認質問をユーザーに表示し、その決定を SDK に返します。

タスクに取り組んでいる間、Claude はユーザーに確認を取る必要がある場合があります。ファイルを削除する前に許可が必要な場合もあれば、新しいプロジェクト用にどのデータベースを使用するかを尋ねる必要がある場合もあります。アプリケーションはこれらのリクエストをユーザーに表示して、Claude がユーザーの入力で続行できるようにする必要があります。

Claude がユーザー入力をリクエストするのは 2 つの状況です。ツールを使用する許可が必要な場合(ファイルの削除やコマンドの実行など)と、確認質問がある場合(AskUserQuestion ツール経由)です。どちらも canUseTool コールバックをトリガーし、応答を返すまで実行を一時停止します。これは Claude が終了して次のメッセージを待つ通常の会話ターンとは異なります。

確認質問については、Claude が質問とオプションを生成します。あなたの役割は、それらをユーザーに提示して、ユーザーの選択を返すことです。このフローに独自の質問を追加することはできません。ユーザーに何か尋ねる必要がある場合は、アプリケーションロジックで別途実行してください。

コールバックは無期限に保留中のままにすることができます。実行はコールバックが返されるまで一時停止したままです。ユーザーがプロセスが合理的に実行し続けることができるより長く応答するのに時間がかかる可能性がある場合、PreToolUse フックを登録して、defer 決定を返すことで、プロセスを終了して、後で永続化されたセッションから再開できます。

このガイドでは、各タイプのリクエストを検出し、適切に応答する方法を示します。

Claude が入力を必要とするタイミングを検出する

クエリオプションで canUseTool コールバックを渡してください。このコールバックは Claude がユーザー入力を必要とするたびに発火し、ツール名と入力を引数として受け取ります。

from claude_agent_sdk import ClaudeAgentOptions


async def handle_tool_request(tool_name, input_data, context):
    # ユーザーにプロンプトを表示して、許可または拒否を返す
    ...


options = ClaudeAgentOptions(can_use_tool=handle_tool_request)
import type { CanUseTool } from "@anthropic-ai/claude-agent-sdk";

const handleToolRequest: CanUseTool = async (toolName, input, options) => {
  // options には { signal: AbortSignal, suggestions?: PermissionUpdate[] } が含まれます
  // ここでユーザーにプロンプトを表示してから、許可または拒否を返す
  return { behavior: "deny", message: "User declined" };
};

const options = { canUseTool: handleToolRequest };

コールバックは 2 つのケースで発火します。

  1. ツールが承認を必要とする場合: Claude が 権限ルール または権限モードで自動承認されていないツールを使用したいとき。tool_name でツール(例:"Bash"、"Write")を確認してください。
  2. Claude が質問する: Claude は AskUserQuestion ツールを呼び出します。tool_name == "AskUserQuestion" かどうかを確認して、異なる方法で処理します。tools 配列を指定する場合、これが機能するには AskUserQuestion を含めてください。詳細については、明確化のための質問を処理するを参照してください。

[!WARNING]

canUseTool が実行される前に、それより前の段階で呼び出しが許可または拒否される場合があります。 権限評価フロー は、コールバックの前に拒否ルール、権限モード、許可ルールをチェックします。

ルールまたは権限モードによってすでに許可された呼び出しに対して独自のチェックを実行するには、PreToolUse フック を使用してください。このフックはそれらの呼び出しに対しても実行され、呼び出しを許可、拒否、または変更できます。

また、PermissionRequest フック を使用して、Claude が承認を待っているときに外部通知(Slack、メール、プッシュ)を送信することもできます。

ツール承認リクエストを処理する

クエリオプションで canUseTool コールバックを渡すと、Claude が以前の許可フローで承認されていないツールを使用したい場合に発火します。dontAsk モードなどの一部の設定では、Claude Code はこれを呼び出しません。許可がどのように評価されるかの最後のステップにそれらが記載されており、代わりに呼び出しに何が起こるかが説明されています。

コールバックは 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);
}

この例では y/n フローを使用しており、y 以外の入力は拒否として扱われます。実際には、ユーザーがリクエストを変更したり、フィードバックを提供したり、Claude を完全にリダイレクトしたりできるより豊富な UI を構築する可能性があります。すべての応答方法については ツールリクエストに応答するを参照してください。

ツールリクエストに応答する

コールバックは 2 つの応答タイプのいずれかを返します。

応答 Python TypeScript
許可 PermissionResultAllow(updated_input=...) { behavior: "allow", updatedInput }
拒否 PermissionResultDeny(message=...) { behavior: "deny", message }

許可する場合、Claude がリクエストした入力でツールが実行されます。ただし、変更された入力を返す場合は、TypeScript では updatedInput、Python では updated_input を返すことができます。v2.1.207 より前では、Claude Code は updatedInput を省略した許可結果を拒否し、検証エラーでツール呼び出しを拒否していました。

拒否する場合、理由を説明するメッセージを提供します。Claude はこのメッセージを見て、アプローチを調整する可能性があります。

許可または拒否を超えて、ツールの入力を変更したり、Claude がアプローチを調整するのに役立つコンテキストを提供したりできます。

  • 承認:ツールを Claude がリクエストしたとおりに実行させる
  • 変更を加えて承認:実行前に入力を変更する(例:パスをサニタイズ、制約を追加)
  • 承認して記憶:提案された許可ルールをエコーバックして、一致する呼び出しが次回プロンプトをスキップするようにする
  • 拒否:ツールをブロックして Claude に理由を伝える
  • 代替案を提案:ブロックするが、ユーザーが望むものに向かって Claude をガイドする
  • 完全にリダイレクト:ストリーミング入力を使用して Claude に完全に新しい指示を送信する

次のスニペットの ask_user および askUser ヘルパーは、アプリケーション独自のプロンプト UI の代わりになります。

[tab] 承認

ユーザーはアクションをそのまま承認します。コールバックから 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" };
};

[tab] 変更を加えて承認

ユーザーは承認しますが、最初にリクエストを変更したいと考えています。ツールが実行される前に入力を変更できます。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 };
};

[tab] 承認して記憶

ユーザーが承認し、この種の呼び出しについて再度尋ねられたくない場合。3 番目のコールバック引数は suggestions を含み、これは準備完了した PermissionUpdate エントリの配列です。updatedPermissions で 1 つをエコーバックして適用します。localSettings 宛先を持つ提案は、ルールを .claude/settings.local.json に書き込むため、将来のセッションは一致する呼び出しのプロンプトをスキップします。

TypeScript では、オプションが suppressAlwaysAllowRule: true を含むリクエストについて、常に許可の選択肢をスキップしてください。このヒントには Agent SDK v0.3.268 以降が必要であり、Python context はこれを含みません。

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" };
};

[tab] 拒否

ユーザーはこのアクションが実行されることを望んでいません。ツールをブロックして、理由を説明するメッセージを提供します。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 };
};

[tab] 代替案を提案

ユーザーはこの特定のアクションを望んでいませんが、別のアイデアがあります。ツールをブロックして、メッセージにガイダンスを含めます。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 };
};

[tab] 完全にリダイレクト

方向の完全な変更(単なるナッジではなく)の場合は、ストリーミング入力を使用して Claude に新しい指示を直接送信します。これは現在のツールリクエストをバイパスし、Claude に完全に新しい指示に従うように指示します。

確認質問を処理する

Claude が複数の有効なアプローチを持つタスクについてさらに方向性が必要な場合、AskUserQuestion ツールを呼び出します。これは toolName が AskUserQuestion に設定された canUseTool コールバックをトリガーします。入力には Claude の質問が複数選択肢として含まれており、これらをユーザーに表示して、ユーザーの選択を返します。

[!TIP]

確認質問は特に plan モードで一般的です。Claude はコードベースを探索し、計画を提案する前に質問をします。これにより、プラン モードは Claude が変更を加える前に要件を収集したい対話的なワークフローに最適です。

次のステップは、確認質問を処理する方法を示しています。

1. 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) => {
      // すべての呼び出しを許可するプレースホルダー。「AskUserQuestion を検出する」ステップでこれを置き換えます。
      return { behavior: "allow", updatedInput: input };
    }
  }
})) {
  console.log(message);
}

2. 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);
};

3. 質問入力を解析する

入力には 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
    }
  ]
}

完全なフィールド説明については 質問形式を参照してください。

4. ユーザーから回答を収集する

質問をユーザーに提示して、ユーザーの選択を収集します。これをどのように行うかは、アプリケーションによって異なります。ターミナルプロンプト、Web フォーム、モバイルダイアログなど。

5. 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 キーが質問テキストで、値が選択されたラベルであるオブジェクト
response ユーザーが構造化された質問に答える代わりに入力した、オプションの自由形式の返信

複数選択質問の場合、ラベルの配列を渡すか、", " で結合します。自由テキスト入力をサポートに示されているような質問ごとの自由テキスト(例:「その他」オプション)の場合は、ユーザーのテキストを answers[question] に入力します。response は、ユーザーが質問カードを閉じて、特定の質問への回答ではない一般的な返信を入力できる UI の場合にのみ設定します。response が設定されている場合、Claude は質問ごとの回答リストではなく「ユーザーが応答しました:…」を受け取ります。

{
  "questions": [
    // ...
  ],
  "answers": {
    "How should I format the output?": "Summary",
    "Which sections should I include?": ["Introduction", "Conclusion"]
  }
}

自由テキスト入力をサポートする

Claude の定義済みオプションがユーザーが望むものをカバーしていない場合があります。ユーザーが独自の回答を入力できるようにするには:

  • Claude のオプションの後に追加の「その他」選択肢を表示して、テキスト入力を受け入れます
  • ユーザーのカスタムテキストを回答値として使用します(「その他」という単語ではなく)

完全な実装については、下記の 完全な例を参照してください。

完全な例

Claude は、タスクを進めるためにユーザー入力が必要な場合に確認質問をします。たとえば、モバイルアプリのテックスタックを決定するのに役立つよう求められた場合、Claude はクロスプラットフォーム対応 vs ネイティブ、バックエンド設定、またはターゲットプラットフォームについて質問する可能性があります。これらの質問は、Claude がユーザーの設定に合致する決定を下すのに役立ちます。推測ではなく。

この例は、ターミナルアプリケーションでこれらの質問を処理します。各ステップで何が起こるかは以下の通りです。

  1. リクエストをルーティングする:canUseTool コールバックはツール名が "AskUserQuestion" であるかどうかをチェックし、専用ハンドラーにルーティングします
  2. 質問を表示する:ハンドラーは questions 配列をループして、各質問を番号付きオプションで出力します
  3. 入力を収集する:ユーザーはオプションを選択する番号を入力するか、自由テキストを直接入力できます(例:「jquery」、「i don't know」)
  4. 回答をマップする:コードは入力が数値(オプションのラベルを使用)か自由テキスト(テキストを直接使用)かをチェックします
  5. Claude に返す:応答には元の questions 配列と answers マッピングの両方が含まれます

TypeScript バージョンを ask.ts として保存して npx tsx ask.ts で実行するか、Python バージョンを ask.py として保存して python ask.py で実行してください。

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:
    """Parse user input as option number(s) or free text."""
    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:
    """Display Claude's questions and collect user answers."""
    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 type { PermissionResult } 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): Promise<PermissionResult> {
  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 コールバックを使用するよりも実装作業が必要です。

関連リソース