Claude Code 学习站

Agent SDK:处理批准请求与用户输入(canUseTool)

介绍 Claude Agent SDK 中 canUseTool 回调如何处理工具批准请求和 AskUserQuestion 澄清性问题,并给出 Python/TypeScript 完整示例。

本页目录13
AI 摘要 · 已核查整理于 2026-08-12原文:Handle approvals and user input(Anthropic)Agent SDKcanUseToolAskUserQuestion权限控制
要点速览
  • canUseTool 回调仅在 Claude 需要工具批准或调用 AskUserQuestion 时触发,已被权限规则/模式自动批准的工具不会经过该回调,需用 PreToolUse hook 才能拦截所有调用。
  • 回调返回 Allow(PermissionResultAllow / { behavior: "allow", updatedInput }) 或 Deny(PermissionResultDeny / { behavior: "deny", message }),可修改输入,也可回传 updatedPermissions 让匹配调用以后跳过提示。
  • AskUserQuestion 的输入含 questions 数组(question/header/options/multiSelect),每次调用限 1-4 个问题、每题 2-4 个选项,需把 answers 中每个 question 映射为选中的 label 返回。
  • Python 下 can_use_tool 依赖流式模式,用有限消息流时需要一个保持流打开的 PreToolUse hook(如返回 {"continue_": True}),否则回调不会被调用。
  • AskUserQuestion 目前在通过 Agent 工具生成的子代理(subagents)中不可用。
  • TypeScript 可用 toolConfig.askUserQuestion.previewFormat("markdown" 或 "html")让选项携带可视化 preview 字段,默认不生成。

本文是对官方 Claude Agent SDK 文档中「Handle approvals and user input」页面的中文整理,完整与最新内容以原文为准:https://code.claude.com/docs/en/agent-sdk/user-input

概述

Claude 在执行任务时,有时需要与用户确认——比如删除文件前需要许可,或需要知道新项目该用哪个数据库。应用需要把这些请求呈现给用户,并把用户的决定返回给 SDK。

Claude 在两种情况下请求用户输入:

  • 需要工具使用许可(例如删除文件、运行命令)
  • 澄清性问题(通过 AskUserQuestion 工具)

这两种情况都会触发你的 canUseTool 回调,执行会暂停,直到你返回响应。这不同于普通对话轮次——普通对话是 Claude 结束发言后等待你的下一条消息。

对于澄清性问题,问题和选项都是由 Claude 生成的。你的角色只是把它们呈现给用户,并返回用户的选择;你不能在这个流程里插入自己的问题——如果需要自己向用户提问,应在应用逻辑中单独处理。

回调可以无限期挂起。执行会一直暂停直到回调返回,只有当 query 本身被取消时,SDK 才会取消等待。如果用户可能需要比进程合理运行时长更久才能响应,可以返回 defer hook 决定(见 hooks 文档「Defer a tool call for later」一节),这样进程可以退出,之后再从持久化的会话中恢复。

检测 Claude 何时需要输入

在 query 选项中传入 canUseTool 回调。只要 Claude 需要用户输入,该回调就会被触发,接收工具名和输入作为参数:

from claude_agent_sdk import ClaudeAgentOptions


async def handle_tool_request(tool_name, input_data, context):
    # Prompt user and return allow or deny
    ...


options = ClaudeAgentOptions(can_use_tool=handle_tool_request)
async function handleToolRequest(toolName, input, options) {
  // options includes { signal: AbortSignal, suggestions?: PermissionUpdate[] }
  // Prompt user and return allow or deny
}

const options = { canUseTool: handleToolRequest };

回调在两种情况下触发:

  1. 工具需要批准:Claude 想使用一个未被权限规则或权限模式自动批准的工具。检查 tool_name(如 "Bash""Write")。
  2. Claude 提出问题:Claude 调用了 AskUserQuestion 工具。检查 tool_name == "AskUserQuestion" 以做不同处理。如果指定了 tools 数组,需把 AskUserQuestion 包含进去,否则这一机制无法工作。详见下文「处理澄清性问题」。

⚠️ 回调不会对已自动批准的工具触发。 权限评估流程中更早阶段做出的任何批准——一条 allow 规则,或 acceptEditsbypassPermissions 这类模式——都会在 canUseTool 被查询之前解析掉这次调用。如果你在 allowed_tools 中裸列出某个工具,只有当评估流程把该调用重新路由回一次提示(例如一条 ask 规则,或 plan 模式)时,针对该工具的 canUseTool 检查才会运行。如果某段逻辑必须应用于每一次工具调用,请使用 PreToolUse hook——它在流程其余部分之前执行,可以允许、拒绝或修改请求。

allow 规则不会预先批准「任何模式都不会自动批准的操作」;哪些操作会到达回调、在 dontAskauto 模式下会发生什么,详见权限评估流程文档。

你也可以使用 PermissionRequest hook,在 Claude 等待批准时发送外部通知(Slack、邮件、推送)。

处理工具批准请求

一旦在 query 选项中传入了 canUseTool 回调,当 Claude 想使用一个未被权限流程中更早阶段批准的工具时就会触发它。回调接收三个参数:

参数说明
toolNameClaude 想要使用的工具名称(如 "Bash""Write""Edit"
inputClaude 传给该工具的参数,内容因工具而异
options(TS)/ context(Python)附加上下文,包括可选的 suggestions(建议的 PermissionUpdate 条目,用于避免重复提示)以及一个取消信号。TypeScript 中 signalAbortSignal;Python 中该 signal 字段保留供未来使用。Python 版参见 ToolPermissionContext

input 对象包含工具特定的参数。常见示例:

工具Input 字段
Bashcommanddescriptiontimeout
Writefile_pathcontent
Editfile_pathold_stringnew_string
Readfile_pathoffsetlimit

完整的输入 schema 见 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:
    # Display the tool request
    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}")

    # Get user approval
    response = input("Allow this action? (y/n): ")

    # Return allow or deny based on user's response
    if response.lower() == "y":
        # Allow: tool executes with the original (or modified) input
        return PermissionResultAllow(updated_input=input_data)
    else:
        # Deny: tool doesn't execute, Claude sees the message
        return PermissionResultDeny(message="User denied this action")


# Required workaround: dummy hook keeps the stream open for 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())
import { query } from "@anthropic-ai/claude-agent-sdk";
import * as readline from "readline";

// Helper to prompt user for input in the terminal
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) => {
      // Display the tool request
      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)}`);
      }

      // Get user approval
      const response = await prompt("Allow this action? (y/n): ");

      // Return allow or deny based on user's response
      if (response.toLowerCase() === "y") {
        // Allow: tool executes with the original (or modified) input
        return { behavior: "allow", updatedInput: input };
      } else {
        // Deny: tool doesn't execute, Claude sees the message
        return { behavior: "deny", message: "User denied this action" };
      }
    }
  }
})) {
  if ("result" in message) console.log(message.result);
}

注意:在 Python 中,can_use_tool 需要流式模式(streaming mode)。当你通过 query(prompt=generator)ClaudeSDKClient.connect(prompt=async_iterable) 传入一个有限的消息流时,除非有注册的 hook 或进程内 MCP server 保持流打开,否则 SDK 会在最后一条消息之后、权限回调被调用之前就关闭输入流。上面的例子用一个返回 {"continue_": True}PreToolUse hook 来保持流打开。以不带 prompt 的方式连接、再通过 ClaudeSDKClient.query() 发送消息,则流会自行保持打开,不需要 hook。

这个例子用的是 y/n 流程,任何非 y 的输入都会被当作拒绝。实际项目中,你可能会构建更丰富的 UI,让用户修改请求、提供反馈,或者完全改变 Claude 的方向。完整的响应方式见下文「响应工具请求」。

响应工具请求

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

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

当允许时,工具会使用 Claude 请求的输入执行,除非你返回一个修改过的输入——TypeScript 中是 updatedInput,Python 中是 updated_input。在 v2.1.207 之前,如果 allow 结果省略了 updatedInput,Claude Code 会拒绝该结果,并以校验错误拒绝这次工具调用。

当拒绝时,请提供一条说明原因的消息。Claude 会看到这条消息,并可能据此调整方案。

from claude_agent_sdk.types import PermissionResultAllow, PermissionResultDeny

# Allow the tool to execute
return PermissionResultAllow(updated_input=input_data)

# Block the tool
return PermissionResultDeny(message="User rejected this action")
// Allow the tool to execute
return { behavior: "allow", updatedInput: input };

// Block the tool
return { behavior: "deny", message: "User rejected this action" };

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

  • 批准(Approve):让工具按 Claude 请求的方式执行
  • 批准并修改(Approve with changes):在执行前修改输入(如清理路径、增加约束)
  • 批准并记住(Approve and remember):回传一条建议的权限规则,让匹配的调用下次跳过提示
  • 拒绝(Reject):阻止工具执行,并告诉 Claude 原因
  • 建议替代方案(Suggest alternative):阻止执行,但引导 Claude 转向用户真正想要的方向
  • 完全重定向(Redirect entirely):用流式输入给 Claude 发送一条全新的指令

以下代码片段中的 ask_useraskUser 辅助函数代表你应用自己的提示 UI。

批准(Approve)

用户按原样批准该操作。把回调中的 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" };
};

批准并修改(Approve with changes)

用户批准,但想先修改请求。你可以在工具执行前修改输入。Claude 会看到执行结果,但不会被告知输入被改过。适合用来清理参数、添加约束或限制访问范围。

async def can_use_tool(tool_name, input_data, context):
    if tool_name == "Bash":
        # User approved, but scope all commands to sandbox
        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") {
    // User approved, but scope all commands to sandbox
    const sandboxedInput = {
      ...input,
      command: input.command.replace("/tmp", "/tmp/sandbox")
    };
    return { behavior: "allow", updatedInput: sandboxedInput };
  }
  return { behavior: "allow", updatedInput: input };
};

批准并记住(Approve and remember)

用户批准,并且不想以后再被问到同类调用。回调的第三个参数带有 suggestions——一个现成的 PermissionUpdate 条目数组。把其中一条回传到 updatedPermissions 即可应用它。目标为 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" };
};

拒绝(Reject)

用户不希望这个操作发生。阻止工具执行,并提供说明原因的消息。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 };
};

建议替代方案(Suggest alternative)

用户不想要这个具体操作,但有别的想法。阻止工具执行,并在消息中给出引导。Claude 会读取这条消息,并据此决定如何继续。

async def can_use_tool(tool_name, input_data, context):
    if tool_name == "Bash" and "rm" in input_data.get("command", ""):
        # User doesn't want to delete, suggest archiving instead
        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")) {
    // User doesn't want to delete, suggest archiving instead
    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 };
};

完全重定向(Redirect entirely)

如果是彻底改变方向(而不只是小小提醒),可以用流式输入直接给 Claude 发送一条新指令。这会绕过当前的工具请求,让 Claude 遵循全新的指示。

处理澄清性问题

当 Claude 在一个存在多种可行做法的任务上需要更多方向指引时,会调用 AskUserQuestion 工具。这会触发你的 canUseTool 回调,toolNameAskUserQuestion。输入中包含 Claude 以多选题形式提出的问题,你需要将其展示给用户,并返回用户的选择。

提示:澄清性问题在 plan 模式下尤为常见——Claude 会先探索代码库,再在提出计划前提问。这使 plan 模式非常适合那些希望 Claude 在改动代码前先收集需求的交互式工作流。

处理澄清性问题的步骤如下:

1. 传入 canUseTool 回调

在 query 选项中传入 canUseTool 回调。默认情况下 AskUserQuestion 是可用的。如果你指定了 tools 数组来限制 Claude 的能力(例如一个只有 ReadGlobGrep 的只读 agent),需要把 AskUserQuestion 加入该数组,否则 Claude 将无法提出澄清性问题:

async for message in query(
    prompt="Analyze this codebase",
    options=ClaudeAgentOptions(
        # Include AskUserQuestion in your tools list
        tools=["Read", "Glob", "Grep", "AskUserQuestion"],
        can_use_tool=can_use_tool,
    ),
):
    print(message)
for await (const message of query({
  prompt: "Analyze this codebase",
  options: {
    // Include AskUserQuestion in your tools list
    tools: ["Read", "Glob", "Grep", "AskUserQuestion"],
    canUseTool: async (toolName, input) => {
      // Handle clarifying questions here
    }
  }
})) {
  console.log(message);
}

2. 识别 AskUserQuestion

在回调中检查 toolName 是否等于 AskUserQuestion,以区别于其他工具进行处理:

async def can_use_tool(tool_name: str, input_data: dict, context):
    if tool_name == "AskUserQuestion":
        # Your implementation to collect answers from the user
        return await handle_clarifying_questions(input_data)
    # Handle other tools normally
    return await prompt_for_approval(tool_name, input_data)
canUseTool: async (toolName, input) => {
  if (toolName === "AskUserQuestion") {
    // Your implementation to collect answers from the user
    return handleClarifyingQuestions(input);
  }
  // Handle other tools normally
  return promptForApproval(toolName, input);
};

3. 解析问题输入

输入的 questions 数组中包含 Claude 的问题。每个问题都有 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. 收集用户答案

把问题展示给用户并收集他们的选择。具体方式取决于你的应用:终端提示、网页表单、移动端弹窗等。

5. 把答案返回给 Claude

构造 answers 对象:一个以 question 文本为 key、被选中选项的 label 为 value 的记录。

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

对于多选题,可以传一个 label 数组,或用 ", " 拼接。如果支持自由文本输入(见下文),把用户的自定义文本作为 value。

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

问题格式

输入中的 questions 数组包含 Claude 生成的问题,每个问题包含以下字段:

字段说明
question完整的问题文本,用于展示
header问题的简短标签(最多 12 个字符)
options2-4 个选项组成的数组,每个选项含 labeldescription。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 不会生成预览,该字段也不会出现。

previewFormatpreview 内容
未设置(默认)字段不存在,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 is an HTML string or 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>"
}

响应格式

返回一个 answers 对象,把每个问题的 question 字段映射到被选中选项的 label:

字段说明
questions原样传回原始的 questions 数组(工具处理需要它)
answers对象,key 为问题文本,value 为被选中的 label
response可选,用户输入的自由文本回复,用来代替对结构化问题的作答

对于多选题,可以传一个 label 数组,或用 ", " 拼接。对于类似「其他」选项这种逐题的自由文本,把用户的文本放进 answers[question](见下文「支持自由文本输入」)。只有当你的 UI 允许用户跳过问题卡片、直接输入一段不针对任何具体问题的通用回复时,才设置 response。设置了 response 后,Claude 收到的是「The user responded: …」,而不是逐题的答案列表。

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

支持自由文本输入

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

  • 在 Claude 给出的选项之后,额外展示一个接受文本输入的「其他」选项
  • 用用户输入的自定义文本作为答案的 value(而不是「Other」这个词本身)

完整实现见下文「完整示例」。

完整示例

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

下面的例子在一个终端应用中处理这些问题。每一步做的事情是:

  1. 路由请求canUseTool 回调检查工具名是否为 "AskUserQuestion",并路由到专门的处理函数
  2. 展示问题:处理函数遍历 questions 数组,把每个问题连同编号选项打印出来
  3. 收集输入:用户可以输入数字来选择一个选项,也可以直接输入自由文本(如 "jquery""i don't know"
  4. 映射答案:代码判断输入是数字(使用该选项的 label)还是自由文本(直接使用该文本)
  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:
    # Route AskUserQuestion to our question handler
    if tool_name == "AskUserQuestion":
        return await handle_ask_user_question(input_data)
    # Auto-approve other tools for this example
    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",
        },
    }


# Required workaround: dummy hook keeps the stream open for 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())
import { query } from "@anthropic-ai/claude-agent-sdk";
import * as readline from "readline/promises";

// Helper to prompt user for input in the terminal
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;
}

// Parse user input as option number(s) or free text
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;
}

// Display Claude's questions and collect user answers
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);
  }

  // Return the answers to Claude (must include original 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) => {
        // Route AskUserQuestion to our question handler
        if (toolName === "AskUserQuestion") {
          return handleAskUserQuestion(input);
        }
        // Auto-approve other tools for this example
        return { behavior: "allow", updatedInput: input };
      }
    }
  })) {
    if ("result" in message) console.log(message.result);
  }
}

main();

限制

  • 子代理(Subagents):通过 Agent 工具生成的子代理中,目前不支持 AskUserQuestion
  • 问题数量限制:每次 AskUserQuestion 调用最多支持 1-4 个问题,每个问题最多 2-4 个选项

获取用户输入的其他方式

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

流式输入(Streaming input)

在以下场景使用流式输入:

  • 在任务中途打断 agent:发送取消信号,或在 Claude 工作过程中改变方向
  • 提供额外上下文:在 Claude 询问之前主动补充它需要的信息
  • 构建聊天界面:在长时间运行的操作期间,让用户发送后续消息

流式输入适合那些用户在整个执行过程中(而不仅是在批准检查点)都与 agent 交互的对话式 UI。

自定义工具(Custom tools)

在以下场景使用自定义工具:

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

自定义工具能让你完全掌控交互过程,但比使用内置的 canUseTool 回调需要更多实现工作。

相关资源

  • 配置权限(Configure permissions):设置权限模式和规则
  • 用 hooks 控制执行(Control execution with hooks):在 agent 生命周期的关键节点运行自定义代码
  • TypeScript SDK 参考:canUseTool 完整 API 文档