Skip to content

Claude Agent SDK 的 canUseTool 回调在两种情况下暂停执行:Claude 需要工具使用许可(如删除文件、运行命令)或通过 AskUserQuestion 工具提出澄清问题。你的应用必须向用户展示请求并返回决定,否则 Claude 会一直等待。关键操作:在 query options 中传入 canUseTool 回调,返回 PermissionResultAllow(可修改输入)或 PermissionResultDeny(带原因消息);处理澄清问题时,检查 tool_name == "AskUserQuestion" 并返回 answers 映射。若 canUseTool 从未触发,检查是否已配置自动批准规则或模式,或在 Python 中是否缺少 PreToolUse 钩子。Agent SDK 版本需 0.1.80+ 才支持 suggestions 字段。

Agent SDK 用户审批与输入回调配置

向用户展示 Claude 的审批请求和澄清问题,然后将用户决定返回给 SDK。

Claude 在执行任务时有时需要与用户确认——例如删除文件前需要权限,或者新建项目时要问用哪个数据库。你的应用需要把这些请求展示给用户,Claude 才能拿到输入继续执行。

Claude 会在两种情况下请求用户输入:需要工具使用许可(如删除文件、运行命令)时,和通过 AskUserQuestion 工具提出澄清问题时。两种情况都会触发你的 canUseTool 回调,执行暂停直到你返回响应。这与普通对话回合不同——普通回合中 Claude 会完成输出并等待你的下一条消息。

对于澄清问题,Claude 生成问题和选项。你的角色是呈现给用户并返回其选择。你不能在此流程中自己添加问题;如果需要自己问用户,请在应用逻辑中独立处理。

回调可以无限期保持挂起状态。执行一直暂停直到回调返回,SDK 仅在查询本身被取消时才取消等待。如果用户可能响应时间超过进程合理运行时间,返回 defer 钩子决策,让进程退出并通过持久化会话稍后恢复。

本页介绍如何检测每种请求类型并正确响应。

检测 Claude 何时需要输入

在 query options 中传入 canUseTool 回调。每当 Claude 需要用户输入时,回调被触发,接收工具名称和输入参数:

python
async def handle_tool_request(tool_name, input_data, context):
    # 提示用户并返回允许或拒绝
    ...

options = ClaudeAgentOptions(can_use_tool=handle_tool_request)
typescript
async function handleToolRequest(toolName, input, options) {
  // options 包含 { signal: AbortSignal, suggestions?: PermissionUpdate[] }
  // 提示用户并返回允许或拒绝
}

const options = { canUseTool: handleToolRequest };

回调在两种情况下触发:

  1. 工具需要审批:Claude 要使用一个没有被权限规则或模式自动批准的工具。检查 tool_name 确定工具(例如 "Bash""Write")。
  2. Claude 提问:Claude 调用 AskUserQuestion 工具。检查 tool_name == "AskUserQuestion" 以不同方式处理。如果你指定了 tools 数组,必须包含 AskUserQuestion 才能生效。详见处理澄清问题

若想自动允许或拒绝工具而不提示用户,请使用钩子。钩子在 canUseTool 之前执行,可根据自定义逻辑允许、拒绝或修改请求。也可使用 PermissionRequest 钩子在 Claude 等待审批时发送外部通知(Slack、邮件、推送)。

处理工具审批请求

在 query options 中传入 canUseTool 回调后,每当 Claude 想使用一个未被自动批准的工具时回调被触发。你的回调接收三个参数:

参数描述
toolNameClaude 要使用的工具名称(例如 "Bash""Write""Edit"
inputClaude 传递给工具的参数。内容因工具而异。
options (TS) / context (Python)额外上下文,包含可选的 suggestions(建议的 PermissionUpdate 条目以避免重复提示)和取消信号。TypeScript 中 signalAbortSignal;Python 中该字段保留给未来使用。详情见 Python ToolPermissionContext

input 对象包含工具特定字段。常见示例:

工具输入字段
Bashcommanddescriptiontimeout
Writefile_pathcontent
Editfile_pathold_stringnew_string
Readfile_pathoffsetlimit

完整输入模式见 SDK 参考:Python | TypeScript

你可以将这些信息展示给用户,让他们决定是否允许或拒绝操作,然后返回适当响应。

以下示例让 Claude 创建并删除一个测试文件。当 Claude 尝试每个操作时,回调在终端打印工具请求并提示输入 y/n 审批。

python
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")

# 必需变通:dummy 钩子保持流开启,以便 can_use_tool 生效
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())
typescript
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 的输入都视为拒绝。实际场景中,你可能构建更丰富的 UI,让用户修改请求、提供反馈或完全重定向 Claude。详见响应工具请求了解所有响应方式。

响应工具请求

你的回调返回以下两种响应类型之一:

响应PythonTypeScript
允许PermissionResultAllow(updated_input=...){ behavior: "allow", updatedInput }
拒绝PermissionResultDeny(message=...){ behavior: "deny", message }

允许时,传入工具输入(原始或修改过的)。拒绝时,提供解释原因的消息。Claude 会看到该消息并可能调整方法。

python
from claude_agent_sdk.types import PermissionResultAllow, PermissionResultDeny

# 允许工具执行
return PermissionResultAllow(updated_input=input_data)

# 阻止工具
return PermissionResultDeny(message="User rejected this action")
typescript
// 允许工具执行
return { behavior: "allow", updatedInput: input };

// 阻止工具
return { behavior: "deny", message: "User rejected this action" };

除了允许或拒绝,你还可以修改工具输入或提供帮助 Claude 调整方法的上下文:

  • 批准:让工具按 Claude 请求的原样执行
  • 批准并修改:在执行前修改输入(例如清理路径、添加约束)
  • 批准并记住:回显建议的权限规则,使下次匹配调用跳过提示
  • 拒绝:阻止工具并告诉 Claude 原因
  • 建议替代方案:阻止但引导 Claude 按用户想要的去做
  • 完全重定向:使用流式输入给 Claude 发送全新的指令

批准

用户批准原操作。传入回调中的 input 不变,工具按 Claude 请求的原样执行。

python
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")
typescript
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 看到结果但不会被通知你改了东西。适用于清理参数、添加约束或限定访问范围。

python
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)
typescript
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 };
};

批准并记住

用户批准且不想再被问此类调用。第三个回调参数携带 suggestions,这是一个现成的 PermissionUpdate 条目数组。回显其中一个到 updatedPermissions 以应用它。目的地为 localSettings 的建议将该规则写入 .claude/settings.local.json,使未来会话跳过匹配调用的提示。

Python 示例需要 claude-agent-sdk 0.1.80 或更高。

python
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")
typescript
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 看到消息后会尝试其他方法。

python
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)
typescript
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 会读取并基于你的反馈决定如何继续。

python
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)
typescript
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 工具。这会触发你的 canUseTool 回调,toolNameAskUserQuestion。输入中包含 Claude 生成的多选题,你展示给用户并返回其选择。

澄清问题在 plan 模式中尤其常见,Claude 会在提出计划前探索代码库并提问。这使得 plan 模式非常适合交互式工作流——你想让 Claude 在修改前收集需求。

以下步骤说明如何处理澄清问题:

步骤 1:传入回调并包含 AskUserQuestion 工具

在 query options 中传入 canUseTool 回调。默认情况下 AskUserQuestion 可用。如果你指定了 tools 数组来限制 Claude 的能力(例如只读 agent 只用 ReadGlobGrep),必须把 AskUserQuestion 也包含在该数组中。否则 Claude 无法提出澄清问题:

python
async for message in query(
    prompt="Analyze this codebase",
    options=ClaudeAgentOptions(
        # 在 tools 列表中包含 AskUserQuestion
        tools=["Read", "Glob", "Grep", "AskUserQuestion"],
        can_use_tool=can_use_tool,
    ),
):
    print(message)
typescript
for await (const message of query({
  prompt: "Analyze this codebase",
  options: {
    // 在 tools 列表中包含 AskUserQuestion
    tools: ["Read", "Glob", "Grep", "AskUserQuestion"],
    canUseTool: async (toolName, input) => {
      // 此处处理澄清问题
    },
  },
})) {
  console.log(message);
}

步骤 2:在回调中检测并路由

在回调中检查 toolName 是否等于 AskUserQuestion,以与非 AskUserQuestion 工具区分处理:

python
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)
typescript
canUseTool: async (toolName, input) => {
  if (toolName === "AskUserQuestion") {
    // 你的实现:从用户收集答案
    return handleClarifyingQuestions(input);
  }
  // 正常处理其他工具
  return promptForApproval(toolName, input);
};

步骤 3:理解输入格式

输入包含 Claude 的问题,位于 questions 数组中。每个问题有 question(显示文本)、options(选项)和 multiSelect(是否允许多选):

json
{
  "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:构建并返回 answers 对象

构建 answers 对象,每个键是 question 文本,值是所选选项的 label

来自问题对象用作
question 字段(如 "How should I format the output?"
所选选项的 label 字段(如 "Summary"

对于多选问题,传入标签数组或用 ", " 连接。如果你支持自由文本输入,使用用户自定义文本作为值。

python
return PermissionResultAllow(
    updated_input={
        "questions": input_data.get("questions", []),
        "answers": {
            "How should I format the output?": "Summary",
            "Which sections should I include?": ["Introduction", "Conclusion"],
        },
    }
)
typescript
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 个字符)
options2-4 个选项的数组,每个有 labeldescription。TypeScript 中还可选 preview(见下方)
multiSelect如果 true,用户可以选择多个选项

你的回调接收的结构:

json
{
  "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 不会生成预览,该字段不存在。

previewFormatpreview 包含内容
未设置(默认)字段不存在。Claude 不生成预览。
"markdown"ASCII 艺术和围栏代码块
"html"样式化的 <div> 片段(SDK 在你的回调运行前会拒绝 <script><style><!DOCTYPE>

格式适用于会话中的所有问题。Claude 在视觉比较有帮助的选项(布局选择、配色方案)上包含 preview,并在不需要时省略(是/否确认、纯文本选择)。渲染前检查是否为 undefined

typescript
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 预览的选项:

json
{
  "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>"
}

响应格式

返回一个 answers 对象,将每个问题的 question 字段映射到所选选项的 label

字段描述
questions透传原始 questions 数组(工具处理必需)
answers对象,键为问题文本,值为所选标签

对于多选问题,传入标签数组或用 ", " 连接。对于自由文本输入,直接使用用户自定义文本。

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

支持自由文本输入

Claude 预定义的选项不一定总能覆盖用户想要的。要让用户输入自己的答案:

  • 在 Claude 的选项后显示一个额外的“其他”选项,接受文本输入
  • 使用用户自定义文本作为答案值(而不是单词“Other”)

完整实现见下方的完整示例

完整示例

Claude 在需要用户输入才能继续时会提出澄清问题。例如,当要求帮助决定移动应用的技术栈时,Claude 可能会问跨平台 vs 原生、后端偏好或目标平台。这些问题帮助 Claude 做出符合用户偏好的决定,而非猜测。

以下示例在终端应用中处理这些问题。步骤如下:

  1. 路由请求canUseTool 回调检查工具名称是否为 "AskUserQuestion" 并路由到专门的处理函数
  2. 显示问题:处理函数遍历 questions 数组,为每个问题打印编号选项
  3. 收集输入:用户可以输入数字选择选项,或直接输入自由文本(如 "jquery"、"i don't know")
  4. 映射答案:代码检查输入是否为数字(使用选项的 label)或自由文本(直接使用文本)
  5. 返回给 Claude:响应包含原始 questions 数组和 answers 映射
python
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",
        },
    }

# 必需变通:dummy 钩子保持流开启,以便 can_use_tool 生效
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())
typescript
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(必须包含原始 questions)
  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();

限制

  • 子代理:目前通过 Agent 工具生成子代理时,AskUserQuestion 不可用
  • 问题限制:每次 AskUserQuestion 调用支持 1-4 个问题,每个问题 2-4 个选项

其他获取用户输入的方式

canUseTool 回调和 AskUserQuestion 工具覆盖了大多数审批和澄清场景,但 SDK 还提供其他方式获取用户输入:

流式输入

当以下情况时使用流式输入

  • 在任务中间打断 agent:在 Claude 执行时发送取消信号或更改方向
  • 提供额外上下文:添加 Claude 需要的信息,不必等它提问
  • 构建聊天界面:让用户在长时间运行的操作中发送后续消息

流式输入适用于对话式 UI——用户在整个执行过程中与 agent 交互,而不仅仅在审批检查点。

自定义工具

当以下情况时使用自定义工具

  • 收集结构化输入:构建表单、向导或多步工作流,超出 AskUserQuestion 的多选格式
  • 集成外部审批系统:连接到现有工单、工作流或审批平台
  • 实现领域特定交互:创建适合你应用需求的自定义工具,如代码审查界面或部署检查清单

自定义工具完全控制交互,但相比内置的 canUseTool 回调需要更多实现工作。

相关资源

常见问题

canUseTool 回调从未触发是怎么回事?

最常见的原因是工具已被权限规则自动批准,或者当前模式(如 approval 模式)允许了操作。检查你是否设置了 permission_rules 或使用了自动批准的钩子。在 Python 中还需确认使用了流式模式并提供了返回 {"continue_": True}PreToolUse 钩子。

为什么 AskUserQuestion 没被 Claude 调用?

如果你自己指定了 tools 数组,必须显式包含 "AskUserQuestion"。另外,AskUserQuestion 在当前子代理(通过 Agent 工具生成)中不可用。如果使用了 plan 模式,Claude 更可能在探索后提出问题,可以尝试让 prompt 更开放。

如何让用户输入自由文本而不是从预设选项中选择?

在展示 Claude 的选项时额外添加一个"其他/自定义"选项。当用户选择该选项时,提供一个文本输入框,然后将用户输入的原样文本作为答案值(而不是"其他")。详见支持自由文本输入