处理批准和用户输入
向用户显示 Claude 的批准请求和澄清问题,然后将他们的决定返回给 SDK。
在处理任务时,Claude 有时需要与用户进行沟通。它可能需要在删除文件前获得许可,或需要询问为新项目使用哪个数据库。您的应用程序需要向用户显示这些请求,以便 Claude 可以继续使用他们的输入。
Claude 在两种情况下请求用户输入:当它需要使用工具的权限 (如删除文件或运行命令)时,以及当它有澄清问题 (通过 AskUserQuestion 工具)时。两者都会触发您的 canUseTool 回调,该回调会暂停执行,直到您返回响应。这与普通对话轮次不同,在普通对话轮次中 Claude 完成后等待您的下一条消息。
对于澄清问题,Claude 生成问题和选项。您的角色是向用户呈现这些问题,并返回他们的选择。您不能向此流程添加自己的问题;如果您需要自己询问用户某些内容,请在应用程序逻辑中单独进行。
回调可以无限期地保持待处理状态。执行保持暂停状态,直到您的回调返回,SDK 仅在查询本身被取消时才取消等待。如果用户可能需要比您的进程能够合理保持运行的时间更长的时间来响应,请返回 defer hook 决定 ,它允许进程退出并稍后从持久化会话恢复。
本指南向您展示如何检测每种类型的请求并做出适当的响应。
检测 Claude 何时需要输入
在您的查询选项中传递 canUseTool 回调。每当 Claude 需要用户输入时,回调就会触发,接收工具名称和输入作为参数:
async def handle_tool_request (tool_name, input_data, context):
# 提示用户并返回允许或拒绝
...
options = ClaudeAgentOptions( can_use_tool = handle_tool_request)
回调在两种情况下触发:
工具需要批准 :Claude 想要使用不被权限规则 或权限模式自动批准的工具。检查 tool_name 以获取工具(例如 "Bash"、"Write")。
Claude 提出问题 :Claude 调用 AskUserQuestion 工具。检查 tool_name == "AskUserQuestion" 以不同方式处理它。如果您指定 tools 数组,请包含 AskUserQuestion 以使其工作。有关详细信息,请参阅处理澄清问题 。
回调永远不会对自动批准的工具触发。 权限评估流程 中任何较早的批准、允许规则或 acceptEdits 或 bypassPermissions 等模式会在咨询 canUseTool 之前解决调用。如果您在 allowed_tools 中列出一个工具,除非询问规则或 plan 模式将调用路由回提示,否则该工具的 canUseTool 检查永远不会运行。对于必须应用于每个工具调用的逻辑,请使用 PreToolUse hook ,它在流程的其余部分之前执行,可以允许、拒绝或修改请求。
AskUserQuestion、标记为 requiresUserInteraction 的 MCP 工具以及连接器工具您的组织设置为 ask 即使在允许规则匹配时也会到达回调。在 dontAsk 模式下,这些调用会被拒绝,而不会调用回调。
您还可以使用 PermissionRequest hook 在 Claude 等待批准时发送外部通知(Slack、电子邮件、推送)。
处理工具批准请求
一旦您在查询选项中传递了 canUseTool 回调,当 Claude 想要使用不被自动批准的工具时,它就会触发。您的回调接收三个参数:
input 对象包含工具特定的参数。常见示例:
有关完整的输入架构,请参阅 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 " \n Tool: { 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" )
# 必需的解决方法:虚拟 hook 保持流打开以供 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())
在 Python 中,can_use_tool 需要流模式 。当您通过 query(prompt=generator) 或 ClaudeSDKClient.connect(prompt=async_iterable) 传递有限的消息流时,SDK 会在最后一条消息后关闭输入流,在权限回调被调用之前,除非已注册的 hook 或进程内 MCP 服务器保持其打开。上面的示例使用返回 {"continue_": True} 的 PreToolUse hook 保持其打开。不带提示连接并通过 ClaudeSDKClient.query() 发送消息会自动保持流打开,不需要 hook。
此示例使用 y/n 流,其中除 y 之外的任何输入都被视为拒绝。在实践中,您可能会构建一个更丰富的 UI,让用户修改请求、提供反馈或完全重定向 Claude。有关所有响应方式,请参阅响应工具请求 。
响应工具请求
您的回调返回两种响应类型之一:
允许时,工具使用 Claude 请求的输入运行,除非您返回修改的输入,TypeScript 中的 updatedInput 或 Python 中的 updated_input。在 v2.1.207 之前,Claude Code 拒绝了省略 updatedInput 的允许结果,并以验证错误拒绝了工具调用。
拒绝时,提供说明原因的消息。Claude 会看到此消息并可能调整其方法。
from claude_agent_sdk.types import PermissionResultAllow, PermissionResultDeny
# 允许工具执行
return PermissionResultAllow( updated_input = input_data)
# 阻止工具
return PermissionResultDeny( 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" )
用户批准但想先修改请求。您可以在工具执行前更改输入。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)
用户批准并且不想再被询问此类调用。第三个回调参数携带 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" )
用户不希望发生此操作。阻止工具并提供说明原因的消息。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)
用户不想要此特定操作,但有不同的想法。阻止工具并在您的消息中包含指导。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)
对于完全改变方向(不仅仅是轻推),使用流输入 向 Claude 发送新指令。这绕过当前工具请求并为 Claude 提供全新指令来遵循。
处理澄清问题
当 Claude 需要在具有多个有效方法的任务上获得更多指导时,它会调用 AskUserQuestion 工具。这会触发您的 canUseTool 回调,其中 toolName 设置为 AskUserQuestion。输入包含 Claude 的问题作为多选选项,您向用户显示这些问题并返回他们的选择。
澄清问题在 plan 模式 中特别常见,其中 Claude 探索代码库并在提出计划前提出问题。这使 plan 模式非常适合交互式工作流,您希望 Claude 在进行更改前收集需求。
以下步骤显示如何处理澄清问题:
传递 canUseTool 回调
在您的查询选项中传递 canUseTool 回调。默认情况下,AskUserQuestion 可用。如果您指定 tools 数组来限制 Claude 的功能(例如,仅具有 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)
检测 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)
解析问题输入
输入包含 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:
对于多选问题,传递标签数组或用 ", " 连接它们。如果您支持自由文本输入 ,使用用户的自定义文本作为值。
return PermissionResultAllow(
updated_input = {
"questions" : input_data.get( "questions" , []),
"answers" : {
"How should I format the output?" : "Summary" ,
"Which sections should I include?" : [ "Introduction" , "Conclusion" ],
},
}
)
问题格式
输入包含 Claude 在 questions 数组中生成的问题。每个问题都有这些字段:
您的回调接收的结构:
{
"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 不会生成预览,该字段不存在。
该格式适用于会话中的所有问题。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>"
}
响应格式
返回 answers 对象,将每个问题的 question 字段映射到所选选项的 label:
对于多选问题,传递标签数组或用 ", " 连接它们。对于按问题的自由文本,例如"其他"选项,将用户的文本放在 answers[question] 中,如支持自由文本输入 中所示。仅当您的 UI 让用户关闭问题卡并输入不是任何特定问题答案的一般回复时,才设置 response。当设置 response 时,Claude 会收到"用户回复:…"而不是按问题答案列表。
{
"questions" : [
// ...
],
"answers" : {
"How should I format the output?" : "Summary" ,
"Which sections should I include?" : [ "Introduction" , "Conclusion" ]
}
}
支持自由文本输入
Claude 的预定义选项并不总是涵盖用户想要的内容。要让用户输入自己的答案:
在 Claude 的选项后显示额外的"其他"选择,接受文本输入
使用用户的自定义文本作为答案值(不是单词"其他")
有关完整实现,请参阅下面的完整示例 。
完整示例
当 Claude 需要用户输入来继续时,它会提出澄清问题。例如,当被要求帮助为移动应用程序决定技术栈时,Claude 可能会询问跨平台与原生、后端偏好或目标平台。这些问题帮助 Claude 做出与用户偏好相匹配的决定,而不是猜测。
此示例在终端应用程序中处理这些问题。以下是每个步骤发生的情况:
路由请求 :canUseTool 回调检查工具名称是否为 "AskUserQuestion" 并路由到专用处理程序
显示问题 :处理程序循环遍历 questions 数组并打印每个问题及编号选项
收集输入 :用户可以输入数字来选择选项,或直接输入自由文本(例如"jquery"、"i don't know")
映射答案 :代码检查输入是数字(使用选项的标签)还是自由文本(使用文本直接)
返回给 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 :
"""将用户输入解析为选项编号或自由文本。"""
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" ,
},
}
# 必需的解决方法:虚拟 hook 保持流打开以供 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())
限制
子智能体 :AskUserQuestion 目前在通过 Agent 工具生成的子智能体中不可用
问题限制 :每个 AskUserQuestion 调用支持 1-4 个问题,每个 2-4 个选项
获取用户输入的其他方式
canUseTool 回调和 AskUserQuestion 工具涵盖了大多数批准和澄清场景,但 SDK 提供了其他从用户获取输入的方式:
流输入
当您需要以下情况时,使用流输入 :
在任务中断智能体 :在 Claude 工作时发送取消信号或改变方向
提供额外上下文 :添加 Claude 需要的信息而无需等待它提出问题
构建聊天界面 :让用户在长时间运行的操作期间发送后续消息
流输入非常适合对话式 UI,用户在整个执行过程中与智能体交互,而不仅仅在批准检查点。
自定义工具
当您需要以下情况时,使用自定义工具 :
收集结构化输入 :构建超越 AskUserQuestion 多选格式的表单、向导或多步工作流
集成外部批准系统 :连接到现有的票务、工作流或批准平台
实现特定领域的交互 :创建针对您的应用程序需求定制的工具,如代码审查界面或部署清单
自定义工具让您完全控制交互,但需要比使用内置 canUseTool 回调更多的实现工作。
相关资源