رفتن به محتوا

مدیریت تأییدها و ورودی کاربر

هنگامِ کار روی یک وظیفه، Claude گاهی نیاز دارد با کاربر هماهنگ شود. ممکن است پیش از حذفِ فایل‌ها به مجوز نیاز داشته باشد، یا برای یک پروژه‌ی جدید بپرسد کدام پایگاه‌داده استفاده شود. برنامه‌ات باید این درخواست‌ها را به کاربر نشان بدهد تا Claude بتواند با ورودیِ او ادامه دهد.

Claude در دو موقعیت درخواستِ ورودیِ کاربر می‌کند: وقتی به مجوزِ استفاده از یک ابزار نیاز دارد (مثل حذفِ فایل‌ها یا اجرای دستورها)، و وقتی پرسش‌های روشن‌گرانه دارد (از طریقِ ابزارِ AskUserQuestion). هر دو، callbackِ canUseTool تو را فعال می‌کنند که اجرا را تا برگرداندنِ پاسخت متوقف نگه می‌دارد. این با نوبت‌های عادیِ گفتگو فرق دارد، جایی که Claude کار را تمام می‌کند و منتظرِ پیامِ بعدیِ تو می‌ماند.

برای پرسش‌های روشن‌گرانه، Claude خودِ پرسش‌ها و گزینه‌ها را تولید می‌کند. نقشِ تو این است که آن‌ها را به کاربر ارائه بدهی و انتخاب‌هایش را برگردانی. تو نمی‌توانی پرسش‌های خودت را به این جریان اضافه کنی؛ اگر می‌خواهی خودت چیزی از کاربر بپرسی، این کار را جداگانه در منطقِ برنامه‌ات انجام بده.

callback می‌تواند به‌صورتِ نامحدود معلق بماند. اجرا تا برگشتِ callbackت متوقف می‌ماند، و SDK فقط وقتی انتظار را لغو می‌کند که خودِ پرس‌وجو لغو شود. اگر ممکن است کاربر دیرتر از آن‌چه فرآیندت می‌تواند به‌طور معقول روشن بماند پاسخ بدهد، تصمیمِ hook به‌نامِ defer را برگردان، که به فرآیند اجازه می‌دهد خارج شود و بعداً از نشستِ پایدارشده ادامه بدهد.

این راهنما به تو نشان می‌دهد چطور هر نوع درخواست را تشخیص بدهی و مناسب پاسخ بدهی.

تشخیصِ این‌که Claude کِی به ورودی نیاز دارد

Section titled “تشخیصِ این‌که Claude کِی به ورودی نیاز دارد”

یک callbackِ canUseTool در گزینه‌های پرس‌وجوی خود پاس بده. این callback هر وقت Claude به ورودیِ کاربر نیاز داشته باشد فعال می‌شود و نامِ ابزار و ورودی را به‌عنوانِ آرگومان دریافت می‌کند:

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

این callback در دو حالت فعال می‌شود:

  1. ابزار به تأیید نیاز دارد: Claude می‌خواهد از ابزاری استفاده کند که با قواعدِ دسترسی یا حالت‌ها به‌صورت خودکار تأیید نشده است. tool_name را برای ابزار بررسی کن (مثلاً "Bash", "Write").
  2. Claude پرسش می‌کند: Claude ابزارِ AskUserQuestion را صدا می‌زند. بررسی کن آیا tool_name == "AskUserQuestion" است تا متفاوت با آن برخورد کنی. اگر یک آرایه‌ی tools مشخص می‌کنی، AskUserQuestion را در آن بگنجان تا این کار کند. برای جزئیات مدیریتِ پرسش‌های روشن‌گرانه را ببین.

مدیریتِ درخواست‌های تأییدِ ابزار

Section titled “مدیریتِ درخواست‌های تأییدِ ابزار”

وقتی یک callbackِ canUseTool در گزینه‌های پرس‌وجویت پاس دادی، هر بار که Claude بخواهد از ابزاری استفاده کند که به‌صورت خودکار تأیید نشده، فعال می‌شود. callbackت سه آرگومان دریافت می‌کند:

آرگومانتوضیح
toolNameنامِ ابزاری که Claude می‌خواهد استفاده کند (مثلاً "Bash", "Write", "Edit")
inputپارامترهایی که Claude به ابزار پاس می‌دهد. محتوا بسته به ابزار فرق می‌کند.
options (TS) / context (Python)کانتکستِ اضافی شاملِ suggestionsِ اختیاری (ورودی‌های پیشنهادیِ PermissionUpdate برای جلوگیری از پرسشِ مجدد) و یک سیگنالِ لغو. در TypeScript، signal یک AbortSignal است؛ در Python، فیلدِ signal برای استفاده‌ی آینده رزرو شده است. برای Python ToolPermissionContext را ببین.

شیءِ input شاملِ پارامترهای مختصِ ابزار است. نمونه‌های رایج:

ابزارفیلدهای ورودی
Bashcommand, description, timeout
Writefile_path, content
Editfile_path, old_string, new_string
Readfile_path, offset, limit

برای اسکیماهای کاملِ ورودی، مرجعِ SDK را ببین: Python | TypeScript.

می‌توانی این اطلاعات را به کاربر نشان بدهی تا تصمیم بگیرد عمل را اجازه بدهد یا رد کند، سپس پاسخِ مناسب را برگردانی.

مثالِ زیر از Claude می‌خواهد یک فایلِ آزمایشی بسازد و حذف کند. وقتی Claude هر عملیات را تلاش می‌کند، callback درخواستِ ابزار را در ترمینال چاپ می‌کند و برای تأییدِ 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);
}

این مثال از یک جریانِ y/n استفاده می‌کند که هر ورودیِ غیر از y به‌عنوانِ رد تلقی می‌شود. در عمل، ممکن است یک UI غنی‌تر بسازی که به کاربران اجازه دهد درخواست را تغییر دهند، بازخورد بدهند، یا Claude را به‌کلی به مسیرِ دیگری هدایت کنند. برای همه‌ی راه‌هایی که می‌توانی پاسخ بدهی پاسخ به درخواست‌های ابزار را ببین.

پاسخ به درخواست‌های ابزار

Section titled “پاسخ به درخواست‌های ابزار”

callbackت یکی از دو نوعِ پاسخ را برمی‌گرداند:

پاسخPythonTypeScript
AllowPermissionResultAllow(updated_input=...){ behavior: "allow", updatedInput }
DenyPermissionResultDeny(message=...){ behavior: "deny", message }

هنگامِ اجازه‌دادن، ورودیِ ابزار (اصلی یا تغییریافته) را پاس بده. هنگامِ رد، پیامی بده که دلیل را توضیح می‌دهد. 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 کمک کند رویکردش را تنظیم کند:

  • تأیید: بگذار ابزار همان‌طور که Claude درخواست کرد اجرا شود
  • تأیید با تغییرات: ورودی را پیش از اجرا تغییر بده (مثلاً پاک‌سازیِ مسیرها، افزودنِ محدودیت)
  • تأیید و به‌خاطر سپردن: یک قاعده‌ی دسترسیِ پیشنهادی را بازتاب بده تا فراخوانی‌های منطبق دفعه‌ی بعد پرسش را رد کنند
  • رد: ابزار را مسدود کن و به Claude بگو چرا
  • پیشنهادِ جایگزین: مسدود کن اما Claude را به‌سمتِ چیزی که کاربر می‌خواهد هدایت کن
  • هدایتِ کامل: از ورودیِ استریمینگ استفاده کن تا یک دستورالعملِ کاملاً جدید به Claude بفرستی

کاربر عمل را همان‌طور که هست تأیید می‌کند. input را از callbackت بدونِ تغییر پاس بده و ابزار دقیقاً همان‌طور که 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" };
};

مدیریتِ پرسش‌های روشن‌گرانه

Section titled “مدیریتِ پرسش‌های روشن‌گرانه”

وقتی Claude برای وظیفه‌ای با چند رویکردِ معتبر به جهت‌گیریِ بیشتری نیاز دارد، ابزارِ AskUserQuestion را صدا می‌زند. این callbackِ canUseTool تو را با toolName برابرِ AskUserQuestion فعال می‌کند. ورودی شاملِ پرسش‌های Claude به‌صورتِ گزینه‌های چندگزینه‌ای است که آن‌ها را به کاربر نشان می‌دهی و انتخاب‌هایش را برمی‌گردانی.

گام‌های زیر نشان می‌دهند چطور پرسش‌های روشن‌گرانه را مدیریت کنی:

یک callbackِ canUseTool پاس بده

یک callbackِ canUseTool در گزینه‌های پرس‌وجویت پاس بده. به‌صورتِ پیش‌فرض، AskUserQuestion در دسترس است. اگر یک آرایه‌ی tools مشخص می‌کنی تا توانایی‌های Claude را محدود کنی (مثلاً یک ایجنتِ فقط‌خواندنی با فقط Read، Glob و GrepAskUserQuestion را در آن آرایه بگنجان. وگرنه 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);
}

AskUserQuestion را تشخیص بده

در callbackت، بررسی کن آیا 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);
};

ورودیِ پرسش را تجزیه کن

ورودی شاملِ پرسش‌های 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
}
]
}

برای توضیحِ کاملِ فیلدها قالبِ پرسش را ببین.

پاسخ‌ها را از کاربر جمع‌آوری کن

پرسش‌ها را به کاربر ارائه بده و انتخاب‌هایش را جمع‌آوری کن. نحوه‌ی این کار به برنامه‌ات بستگی دارد: یک پرامپتِ ترمینال، یک فرمِ وب، یک دیالوگِ موبایل، و جز این‌ها.

پاسخ‌ها را به Claude برگردان

شیءِ answers را به‌صورتِ یک رکورد بساز که هر کلید، متنِ question و هر مقدار، labelِ گزینه‌ی انتخاب‌شده است:

از شیءِ پرسشبه‌عنوانِ
فیلدِ question (مثلاً "How should I format the output?")کلید
فیلدِ labelِ گزینه‌ی انتخاب‌شده (مثلاً "Summary")مقدار

برای پرسش‌های چندانتخابی، یک آرایه از 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"],
},
}
)
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برچسبِ کوتاه برای پرسش (حداکثر ۱۲ کاراکتر)
optionsآرایه‌ای از ۲ تا ۴ گزینه، هرکدام با label و description. در TypeScript: به‌صورتِ اختیاری preview (به پایین نگاه کن)
multiSelectاگر true باشد، کاربران می‌توانند چند گزینه انتخاب کنند

ساختاری که callbackت دریافت می‌کند:

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

Section titled “پیش‌نمایشِ گزینه‌ها (TypeScript)”

toolConfig.askUserQuestion.previewFormat یک فیلدِ preview به هر گزینه اضافه می‌کند تا برنامه‌ات بتواند کنارِ label یک ماک‌آپِ بصری نشان دهد. بدونِ این تنظیم، Claude پیش‌نمایش تولید نمی‌کند و این فیلد غایب است.

previewFormatpreview شاملِ
تنظیم‌نشده (پیش‌فرض)فیلد غایب است. Claude پیش‌نمایش تولید نمی‌کند.
"markdown"ASCII art و بلاک‌های کدِ محصورشده
"html"یک قطعه‌ی <div>ِ استایل‌خورده (SDK تگ‌های <script>, <style> و <!DOCTYPE> را پیش از اجرای callbackت رد می‌کند)

این قالب برای همه‌ی پرسش‌های نشست اعمال می‌شود. 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آرایه‌ی پرسش‌های اصلی را عیناً پاس بده (برای پردازشِ ابزار لازم است)
answersشیئی که کلیدهایش متنِ پرسش و مقادیرش label‌های انتخاب‌شده‌اند
responseپاسخِ آزادِ اختیاری که کاربر به‌جای پاسخ به پرسش‌های ساختاریافته تایپ کرده است

برای پرسش‌های چندانتخابی، یک آرایه از label‌ها پاس بده یا آن‌ها را با ", " به هم بچسبان. برای متنِ آزادِ هر-پرسش مثل گزینه‌ی «Other»، متنِ کاربر را در answers[question] بگذار، همان‌طور که در پشتیبانی از ورودیِ متنِ آزاد نشان داده شده. response را فقط وقتی تنظیم کن که UI تو به کاربر اجازه می‌دهد کارتِ پرسش را کنار بزند و یک پاسخِ کلی تایپ کند که جوابِ هیچ پرسشِ خاصی نیست. وقتی response تنظیم می‌شود، Claude به‌جای فهرستِ پاسخِ هر-پرسش، «The user responded: …» را دریافت می‌کند.

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

پشتیبانی از ورودیِ متنِ آزاد

Section titled “پشتیبانی از ورودیِ متنِ آزاد”

گزینه‌های از پیش‌تعریف‌شده‌ی Claude همیشه چیزی را که کاربران می‌خواهند پوشش نمی‌دهند. برای این‌که به کاربران اجازه دهی پاسخِ خودشان را تایپ کنند:

  • یک گزینه‌ی اضافیِ «Other» بعد از گزینه‌های Claude نمایش بده که ورودیِ متنی می‌پذیرد
  • از متنِ سفارشیِ کاربر به‌عنوانِ مقدارِ پاسخ استفاده کن (نه واژه‌ی «Other»)

برای پیاده‌سازیِ کامل مثالِ کاملِ پایین را ببین.

Claude وقتی برای پیش‌رفتن به ورودیِ کاربر نیاز دارد پرسش‌های روشن‌گرانه می‌پرسد. مثلاً، وقتی از او خواسته می‌شود برای انتخابِ یک tech stack برای یک اپِ موبایل کمک کند، Claude ممکن است درباره‌ی چندسکویی در برابرِ بومی، ترجیحاتِ backend، یا پلتفرم‌های هدف بپرسد. این پرسش‌ها به Claude کمک می‌کنند تصمیماتی بگیرد که با ترجیحاتِ کاربر هم‌خوان باشد به‌جای حدس‌زدن.

این مثال آن پرسش‌ها را در یک برنامه‌ی ترمینالی مدیریت می‌کند. این چیزی است که در هر گام اتفاق می‌افتد:

  1. مسیریابیِ درخواست: callbackِ canUseTool بررسی می‌کند آیا نامِ ابزار "AskUserQuestion" است و به یک هندلرِ اختصاصی مسیر می‌دهد
  2. نمایشِ پرسش‌ها: هندلر روی آرایه‌ی questions حلقه می‌زند و هر پرسش را با گزینه‌های شماره‌گذاری‌شده چاپ می‌کند
  3. جمع‌آوریِ ورودی: کاربر می‌تواند یک شماره برای انتخابِ یک گزینه وارد کند، یا متنِ آزاد را مستقیماً تایپ کند (مثلاً «jquery»، «i don’t know»)
  4. نگاشتِ پاسخ‌ها: کد بررسی می‌کند آیا ورودی عددی است (از label‌ِ گزینه استفاده می‌کند) یا متنِ آزاد (مستقیماً از متن استفاده می‌کند)
  5. برگشت به 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:
"""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();
  • ساب‌ایجنت‌ها: AskUserQuestion در حالِ حاضر در ساب‌ایجنت‌هایی که از طریقِ ابزارِ Agent ساخته می‌شوند در دسترس نیست
  • محدودیتِ پرسش: هر فراخوانیِ AskUserQuestion از ۱ تا ۴ پرسش با ۲ تا ۴ گزینه برای هرکدام پشتیبانی می‌کند

راه‌های دیگرِ گرفتنِ ورودی از کاربر

Section titled “راه‌های دیگرِ گرفتنِ ورودی از کاربر”

callbackِ canUseTool و ابزارِ AskUserQuestion بیشترِ سناریوهای تأیید و روشن‌سازی را پوشش می‌دهند، اما SDK راه‌های دیگری هم برای گرفتنِ ورودی از کاربر ارائه می‌دهد:

از ورودیِ استریمینگ وقتی استفاده کن که نیاز داری:

  • ایجنت را وسطِ وظیفه قطع کنی: یک سیگنالِ لغو بفرستی یا حین کارِ Claude مسیر را عوض کنی
  • کانتکستِ اضافی بدهی: اطلاعاتی را که Claude نیاز دارد اضافه کنی بدونِ این‌که منتظرِ پرسیدنش بمانی
  • رابط‌های چت بسازی: به کاربران اجازه بدهی حینِ عملیاتِ طولانی پیام‌های پیگیری بفرستند

ورودیِ استریمینگ برای UIهای گفتگومحور ایده‌آل است، جایی که کاربران در طولِ اجرا با ایجنت تعامل می‌کنند، نه فقط در نقاطِ بازرسیِ تأیید.

از ابزارهای سفارشی وقتی استفاده کن که نیاز داری:

  • ورودیِ ساختاریافته جمع‌آوری کنی: فرم‌ها، ویزاردها، یا ورک‌فلوهای چندگامی بسازی که فراتر از قالبِ چندگزینه‌ایِ AskUserQuestion بروند
  • سیستم‌های تأییدِ بیرونی را یکپارچه کنی: به پلتفرم‌های موجودِ تیکتینگ، ورک‌فلو، یا تأیید وصل شوی
  • تعامل‌های مختصِ دامنه پیاده‌سازی کنی: ابزارهایی متناسب با نیازهای برنامه‌ات بسازی، مثل رابط‌های بازبینیِ کد یا چک‌لیست‌های استقرار

ابزارهای سفارشی کنترلِ کاملی بر تعامل به تو می‌دهند، اما نسبت به استفاده از callbackِ توکارِ canUseTool کارِ پیاده‌سازیِ بیشتری می‌طلبند.