1> ## Documentation Index
2> Fetch the complete documentation index at: https://code.claude.com/docs/llms.txt
3> Use this file to discover all available pages before exploring further.
4
5# 승인 및 사용자 입력 처리
6
7> Claude의 승인 요청 및 명확화 질문을 사용자에게 표시한 후 SDK에 사용자의 결정을 반환합니다.
8
9작업을 진행하는 동안 Claude는 때때로 사용자와 확인해야 합니다. 파일을 삭제하기 전에 권한이 필요할 수도 있고, 새 프로젝트를 위해 어떤 데이터베이스를 사용할지 물어봐야 할 수도 있습니다. 애플리케이션은 이러한 요청을 사용자에게 표시하여 Claude가 사용자의 입력으로 계속 진행할 수 있도록 해야 합니다.
10
11Claude는 두 가지 상황에서 사용자 입력을 요청합니다. **도구 사용 권한**이 필요할 때(파일 삭제 또는 명령 실행 등)와 **명확화 질문**이 있을 때(`AskUserQuestion` 도구를 통해)입니다. 둘 다 `canUseTool` 콜백을 트리거하며, 이는 응답을 반환할 때까지 실행을 일시 중지합니다. 이는 Claude가 완료되고 다음 메시지를 기다리는 일반적인 대화 턴과는 다릅니다.
12
13명확화 질문의 경우 Claude가 질문과 옵션을 생성합니다. 사용자의 역할은 이를 사용자에게 제시하고 선택 사항을 반환하는 것입니다. 이 흐름에 자신의 질문을 추가할 수 없습니다. 사용자에게 직접 물어봐야 할 사항이 있으면 애플리케이션 로직에서 별도로 수행하십시오.
14
15콜백은 무기한 대기 상태로 유지될 수 있습니다. 콜백이 반환될 때까지 실행이 일시 중지되며, SDK는 쿼리 자체가 취소될 때만 대기를 취소합니다. 사용자가 프로세스가 합리적으로 실행 상태를 유지할 수 있는 것보다 더 오래 응답하는 데 시간이 걸릴 수 있다면, TypeScript SDK는 [`defer` 훅 결정](/ko/hooks#defer-a-tool-call-for-later)을 지원하므로 프로세스를 종료하고 나중에 지속된 세션에서 재개할 수 있습니다. 이 옵션은 Python SDK에서는 사용할 수 없습니다.
16
17이 가이드는 각 유형의 요청을 감지하고 적절하게 응답하는 방법을 보여줍니다.
18
19## Claude가 입력이 필요한 시점 감지
20
21쿼리 옵션에 `canUseTool` 콜백을 전달합니다. 콜백은 Claude가 사용자 입력이 필요할 때마다 실행되며, 도구 이름과 입력을 인수로 받습니다.
22
23<CodeGroup>
24 ```python Python theme={null}
25 async def handle_tool_request(tool_name, input_data, context):
26 # 사용자에게 프롬프트하고 허용 또는 거부 반환
27 ...
28
29
30 options = ClaudeAgentOptions(can_use_tool=handle_tool_request)
31 ```
32
33 ```typescript TypeScript theme={null}
34 async function handleToolRequest(toolName, input, options) {
35 // options includes { signal: AbortSignal, suggestions?: PermissionUpdate[] }
36 // 사용자에게 프롬프트하고 허용 또는 거부 반환
37 }
38
39 const options = { canUseTool: handleToolRequest };
40 ```
41</CodeGroup>
42
43콜백은 두 가지 경우에 실행됩니다.
44
451. **도구가 승인 필요**: Claude가 [권한 규칙](/ko/agent-sdk/permissions) 또는 모드에 의해 자동 승인되지 않은 도구를 사용하려고 합니다. 도구에 대해 `tool_name`을 확인합니다(예: `"Bash"`, `"Write"`).
462. **Claude가 질문함**: Claude가 `AskUserQuestion` 도구를 호출합니다. `tool_name == "AskUserQuestion"`을 확인하여 다르게 처리합니다. `tools` 배열을 지정하는 경우 이것이 작동하려면 `AskUserQuestion`을 포함하십시오. 자세한 내용은 [명확화 질문 처리](#명확화-질문-처리)를 참조하십시오.
47
48<Note>
49 사용자에게 프롬프트하지 않고 도구를 자동으로 허용하거나 거부하려면 [훅](/ko/agent-sdk/hooks)을 대신 사용하십시오. 훅은 `canUseTool` 전에 실행되며 자신의 로직에 따라 요청을 허용, 거부 또는 수정할 수 있습니다. [`PermissionRequest` 훅](/ko/agent-sdk/hooks#available-hooks)을 사용하여 Claude가 승인을 기다리고 있을 때 외부 알림(Slack, 이메일, 푸시)을 보낼 수도 있습니다.
50</Note>
51
52## 도구 승인 요청 처리
53
54쿼리 옵션에 `canUseTool` 콜백을 전달하면, Claude가 자동 승인되지 않은 도구를 사용하려고 할 때 실행됩니다. 콜백은 세 가지 인수를 받습니다.
55
56| 인수 | 설명 |
57| ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
58| `toolName` | Claude가 사용하려는 도구의 이름(예: `"Bash"`, `"Write"`, `"Edit"`) |
59| `input` | Claude가 도구에 전달하는 매개변수입니다. 내용은 도구에 따라 다릅니다. |
60| `options` (TS) / `context` (Python) | 선택적 `suggestions`(재프롬프트를 피하기 위한 제안된 `PermissionUpdate` 항목)과 취소 신호를 포함한 추가 컨텍스트입니다. TypeScript에서 `signal`은 `AbortSignal`입니다. Python에서 신호 필드는 향후 사용을 위해 예약되어 있습니다. Python의 경우 [`ToolPermissionContext`](/ko/agent-sdk/python#toolpermissioncontext)를 참조하십시오. |
61
62`input` 객체에는 도구별 매개변수가 포함됩니다. 일반적인 예:
63
64| 도구 | 입력 필드 |
65| ------- | --------------------------------------- |
66| `Bash` | `command`, `description`, `timeout` |
67| `Write` | `file_path`, `content` |
68| `Edit` | `file_path`, `old_string`, `new_string` |
69| `Read` | `file_path`, `offset`, `limit` |
70
71완전한 입력 스키마는 SDK 참조를 참조하십시오. [Python](/ko/agent-sdk/python#tool-input%2Foutput-types) | [TypeScript](/ko/agent-sdk/typescript#tool-input-types).
72
73이 정보를 사용자에게 표시하여 작업을 허용할지 거부할지 결정한 후 적절한 응답을 반환할 수 있습니다.
74
75다음 예제는 Claude에게 테스트 파일을 생성하고 삭제하도록 요청합니다. Claude가 각 작업을 시도할 때 콜백은 도구 요청을 터미널에 인쇄하고 y/n 승인을 요청합니다.
76
77<CodeGroup>
78 ```python Python theme={null}
79 import asyncio
80
81 from claude_agent_sdk import ClaudeAgentOptions, ResultMessage, query
82 from claude_agent_sdk.types import (
83 HookMatcher,
84 PermissionResultAllow,
85 PermissionResultDeny,
86 ToolPermissionContext,
87 )
88
89
90 async def can_use_tool(
91 tool_name: str, input_data: dict, context: ToolPermissionContext
92 ) -> PermissionResultAllow | PermissionResultDeny:
93 # 도구 요청 표시
94 print(f"\nTool: {tool_name}")
95 if tool_name == "Bash":
96 print(f"Command: {input_data.get('command')}")
97 if input_data.get("description"):
98 print(f"Description: {input_data.get('description')}")
99 else:
100 print(f"Input: {input_data}")
101
102 # 사용자 승인 받기
103 response = input("Allow this action? (y/n): ")
104
105 # 사용자의 응답에 따라 허용 또는 거부 반환
106 if response.lower() == "y":
107 # 허용: 도구가 원본(또는 수정된) 입력으로 실행됨
108 return PermissionResultAllow(updated_input=input_data)
109 else:
110 # 거부: 도구가 실행되지 않음, Claude가 메시지를 봄
111 return PermissionResultDeny(message="User denied this action")
112
113
114 # 필수 해결 방법: 더미 훅이 can_use_tool을 위해 스트림을 열어 둠
115 async def dummy_hook(input_data, tool_use_id, context):
116 return {"continue_": True}
117
118
119 async def prompt_stream():
120 yield {
121 "type": "user",
122 "message": {
123 "role": "user",
124 "content": "Create a test file in /tmp and then delete it",
125 },
126 }
127
128
129 async def main():
130 async for message in query(
131 prompt=prompt_stream(),
132 options=ClaudeAgentOptions(
133 can_use_tool=can_use_tool,
134 hooks={"PreToolUse": [HookMatcher(matcher=None, hooks=[dummy_hook])]},
135 ),
136 ):
137 if isinstance(message, ResultMessage) and message.subtype == "success":
138 print(message.result)
139
140
141 asyncio.run(main())
142 ```
143
144 ```typescript TypeScript theme={null}
145 import { query } from "@anthropic-ai/claude-agent-sdk";
146 import * as readline from "readline";
147
148 // 터미널에서 사용자 입력을 프롬프트하는 헬퍼
149 function prompt(question: string): Promise<string> {
150 const rl = readline.createInterface({
151 input: process.stdin,
152 output: process.stdout
153 });
154 return new Promise((resolve) =>
155 rl.question(question, (answer) => {
156 rl.close();
157 resolve(answer);
158 })
159 );
160 }
161
162 for await (const message of query({
163 prompt: "Create a test file in /tmp and then delete it",
164 options: {
165 canUseTool: async (toolName, input) => {
166 // 도구 요청 표시
167 console.log(`\nTool: ${toolName}`);
168 if (toolName === "Bash") {
169 console.log(`Command: ${input.command}`);
170 if (input.description) console.log(`Description: ${input.description}`);
171 } else {
172 console.log(`Input: ${JSON.stringify(input, null, 2)}`);
173 }
174
175 // 사용자 승인 받기
176 const response = await prompt("Allow this action? (y/n): ");
177
178 // 사용자의 응답에 따라 허용 또는 거부 반환
179 if (response.toLowerCase() === "y") {
180 // 허용: 도구가 원본(또는 수정된) 입력으로 실행됨
181 return { behavior: "allow", updatedInput: input };
182 } else {
183 // 거부: 도구가 실행되지 않음, Claude가 메시지를 봄
184 return { behavior: "deny", message: "User denied this action" };
185 }
186 }
187 }
188 })) {
189 if ("result" in message) console.log(message.result);
190 }
191 ```
192</CodeGroup>
193
194<Note>
195 Python에서 `can_use_tool`은 [스트리밍 모드](/ko/agent-sdk/streaming-vs-single-mode)와 스트림을 열어 두기 위해 `{"continue_": True}`를 반환하는 `PreToolUse` 훅이 필요합니다. 이 훅이 없으면 권한 콜백이 호출되기 전에 스트림이 닫힙니다.
196</Note>
197
198이 예제는 `y` 이외의 모든 입력이 거부로 처리되는 y/n 흐름을 사용합니다. 실제로는 사용자가 요청을 수정하거나, 피드백을 제공하거나, Claude를 완전히 리디렉션할 수 있는 더 풍부한 UI를 구축할 수 있습니다. 응답할 수 있는 모든 방법은 [도구 요청에 응답](#도구-요청에-응답)을 참조하십시오.
199
200### 도구 요청에 응답
201
202콜백은 두 가지 응답 유형 중 하나를 반환합니다.
203
204| 응답 | Python | TypeScript |
205| ------ | ------------------------------------------ | ------------------------------------- |
206| **허용** | `PermissionResultAllow(updated_input=...)` | `{ behavior: "allow", updatedInput }` |
207| **거부** | `PermissionResultDeny(message=...)` | `{ behavior: "deny", message }` |
208
209허용할 때 도구 입력(원본 또는 수정됨)을 전달합니다. 거부할 때 이유를 설명하는 메시지를 제공합니다. Claude는 이 메시지를 보고 접근 방식을 조정할 수 있습니다.
210
211<CodeGroup>
212 ```python Python theme={null}
213 from claude_agent_sdk.types import PermissionResultAllow, PermissionResultDeny
214
215 # 도구가 실행되도록 허용
216 return PermissionResultAllow(updated_input=input_data)
217
218 # 도구 차단
219 return PermissionResultDeny(message="User rejected this action")
220 ```
221
222 ```typescript TypeScript theme={null}
223 // 도구가 실행되도록 허용
224 return { behavior: "allow", updatedInput: input };
225
226 // 도구 차단
227 return { behavior: "deny", message: "User rejected this action" };
228 ```
229</CodeGroup>
230
231허용하거나 거부하는 것 외에도 도구의 입력을 수정하거나 Claude가 접근 방식을 조정하는 데 도움이 되는 컨텍스트를 제공할 수 있습니다.
232
233* **승인**: 도구가 Claude가 요청한 대로 실행되도록 허용
234* **변경 사항과 함께 승인**: 실행 전에 입력 수정(예: 경로 정제, 제약 조건 추가)
235* **거부**: 도구를 차단하고 이유를 Claude에 알림
236* **대안 제안**: 차단하지만 사용자가 원하는 것으로 Claude를 안내
237* **완전히 리디렉션**: [스트리밍 입력](/ko/agent-sdk/streaming-vs-single-mode)을 사용하여 Claude에 완전히 새로운 지시를 보냄
238
239<Tabs>
240 <Tab title="승인">
241 사용자가 작업을 그대로 승인합니다. 콜백에서 `input`을 변경하지 않고 전달하면 도구가 Claude가 요청한 대로 정확히 실행됩니다.
242
243 <CodeGroup>
244 ```python Python theme={null}
245 async def can_use_tool(tool_name, input_data, context):
246 print(f"Claude wants to use {tool_name}")
247 approved = await ask_user("Allow this action?")
248
249 if approved:
250 return PermissionResultAllow(updated_input=input_data)
251 return PermissionResultDeny(message="User declined")
252 ```
253
254 ```typescript TypeScript theme={null}
255 canUseTool: async (toolName, input) => {
256 console.log(`Claude wants to use ${toolName}`);
257 const approved = await askUser("Allow this action?");
258
259 if (approved) {
260 return { behavior: "allow", updatedInput: input };
261 }
262 return { behavior: "deny", message: "User declined" };
263 };
264 ```
265 </CodeGroup>
266 </Tab>
267
268 <Tab title="변경 사항과 함께 승인">
269 사용자가 승인하지만 먼저 요청을 수정하려고 합니다. 도구가 실행되기 전에 입력을 변경할 수 있습니다. Claude는 결과를 보지만 변경 사항을 알려주지 않습니다. 매개변수 정제, 제약 조건 추가 또는 액세스 범위 지정에 유용합니다.
270
271 <CodeGroup>
272 ```python Python theme={null}
273 async def can_use_tool(tool_name, input_data, context):
274 if tool_name == "Bash":
275 # 사용자가 승인했지만 모든 명령을 샌드박스로 범위 지정
276 sandboxed_input = {**input_data}
277 sandboxed_input["command"] = input_data["command"].replace(
278 "/tmp", "/tmp/sandbox"
279 )
280 return PermissionResultAllow(updated_input=sandboxed_input)
281 return PermissionResultAllow(updated_input=input_data)
282 ```
283
284 ```typescript TypeScript theme={null}
285 canUseTool: async (toolName, input) => {
286 if (toolName === "Bash") {
287 // 사용자가 승인했지만 모든 명령을 샌드박스로 범위 지정
288 const sandboxedInput = {
289 ...input,
290 command: input.command.replace("/tmp", "/tmp/sandbox")
291 };
292 return { behavior: "allow", updatedInput: sandboxedInput };
293 }
294 return { behavior: "allow", updatedInput: input };
295 };
296 ```
297 </CodeGroup>
298 </Tab>
299
300 <Tab title="거부">
301 사용자가 이 작업이 발생하기를 원하지 않습니다. 도구를 차단하고 이유를 설명하는 메시지를 제공합니다. Claude는 이 메시지를 보고 다른 접근 방식을 시도할 수 있습니다.
302
303 <CodeGroup>
304 ```python Python theme={null}
305 async def can_use_tool(tool_name, input_data, context):
306 approved = await ask_user(f"Allow {tool_name}?")
307
308 if not approved:
309 return PermissionResultDeny(message="User rejected this action")
310 return PermissionResultAllow(updated_input=input_data)
311 ```
312
313 ```typescript TypeScript theme={null}
314 canUseTool: async (toolName, input) => {
315 const approved = await askUser(`Allow ${toolName}?`);
316
317 if (!approved) {
318 return {
319 behavior: "deny",
320 message: "User rejected this action"
321 };
322 }
323 return { behavior: "allow", updatedInput: input };
324 };
325 ```
326 </CodeGroup>
327 </Tab>
328
329 <Tab title="대안 제안">
330 사용자가 이 특정 작업을 원하지 않지만 다른 아이디어가 있습니다. 도구를 차단하고 메시지에 지침을 포함합니다. Claude는 이를 읽고 피드백에 따라 진행 방법을 결정합니다.
331
332 <CodeGroup>
333 ```python Python theme={null}
334 async def can_use_tool(tool_name, input_data, context):
335 if tool_name == "Bash" and "rm" in input_data.get("command", ""):
336 # 사용자가 삭제를 원하지 않음, 대신 보관을 제안
337 return PermissionResultDeny(
338 message="User doesn't want to delete files. They asked if you could compress them into an archive instead."
339 )
340 return PermissionResultAllow(updated_input=input_data)
341 ```
342
343 ```typescript TypeScript theme={null}
344 canUseTool: async (toolName, input) => {
345 if (toolName === "Bash" && input.command.includes("rm")) {
346 // 사용자가 삭제를 원하지 않음, 대신 보관을 제안
347 return {
348 behavior: "deny",
349 message:
350 "User doesn't want to delete files. They asked if you could compress them into an archive instead."
351 };
352 }
353 return { behavior: "allow", updatedInput: input };
354 };
355 ```
356 </CodeGroup>
357 </Tab>
358
359 <Tab title="완전히 리디렉션">
360 방향의 완전한 변경(단순한 밀어붙이기가 아닌)의 경우 [스트리밍 입력](/ko/agent-sdk/streaming-vs-single-mode)을 사용하여 Claude에 새로운 지시를 직접 보냅니다. 이는 현재 도구 요청을 우회하고 Claude에 완전히 새로운 지시를 따르도록 합니다.
361 </Tab>
362</Tabs>
363
364## 명확화 질문 처리
365
366Claude가 여러 유효한 접근 방식이 있는 작업에 대해 더 많은 방향이 필요할 때 `AskUserQuestion` 도구를 호출합니다. 이는 `toolName`이 `AskUserQuestion`으로 설정된 `canUseTool` 콜백을 트리거합니다. 입력에는 Claude의 질문이 객관식 옵션으로 포함되어 있으며, 이를 사용자에게 표시하고 선택 사항을 반환합니다.
367
368<Tip>
369 명확화 질문은 특히 [`plan` 모드](/ko/agent-sdk/permissions#plan-mode-plan)에서 흔하며, Claude가 코드베이스를 탐색하고 계획을 제안하기 전에 질문합니다. 이는 계획 모드를 Claude가 변경하기 전에 요구 사항을 수집하기를 원하는 대화형 워크플로우에 이상적으로 만듭니다.
370</Tip>
371
372다음 단계는 명확화 질문을 처리하는 방법을 보여줍니다.
373
374<Steps>
375 <Step title="canUseTool 콜백 전달">
376 쿼리 옵션에 `canUseTool` 콜백을 전달합니다. 기본적으로 `AskUserQuestion`을 사용할 수 있습니다. Claude의 기능을 제한하기 위해 `tools` 배열을 지정하는 경우(예: `Read`, `Glob` 및 `Grep`만 있는 읽기 전용 에이전트), 그 배열에 `AskUserQuestion`을 포함하십시오. 그렇지 않으면 Claude가 명확화 질문을 할 수 없습니다.
377
378 <CodeGroup>
379 ```python Python theme={null}
380 async for message in query(
381 prompt="Analyze this codebase",
382 options=ClaudeAgentOptions(
383 # 도구 목록에 AskUserQuestion 포함
384 tools=["Read", "Glob", "Grep", "AskUserQuestion"],
385 can_use_tool=can_use_tool,
386 ),
387 ):
388 print(message)
389 ```
390
391 ```typescript TypeScript theme={null}
392 for await (const message of query({
393 prompt: "Analyze this codebase",
394 options: {
395 // 도구 목록에 AskUserQuestion 포함
396 tools: ["Read", "Glob", "Grep", "AskUserQuestion"],
397 canUseTool: async (toolName, input) => {
398 // 명확화 질문을 여기서 처리
399 }
400 }
401 })) {
402 console.log(message);
403 }
404 ```
405 </CodeGroup>
406 </Step>
407
408 <Step title="AskUserQuestion 감지">
409 콜백에서 `toolName`이 `AskUserQuestion`과 같은지 확인하여 다른 도구와 다르게 처리합니다.
410
411 <CodeGroup>
412 ```python Python theme={null}
413 async def can_use_tool(tool_name: str, input_data: dict, context):
414 if tool_name == "AskUserQuestion":
415 # 사용자로부터 답변을 수집하는 구현
416 return await handle_clarifying_questions(input_data)
417 # 다른 도구를 정상적으로 처리
418 return await prompt_for_approval(tool_name, input_data)
419 ```
420
421 ```typescript TypeScript theme={null}
422 canUseTool: async (toolName, input) => {
423 if (toolName === "AskUserQuestion") {
424 // 사용자로부터 답변을 수집하는 구현
425 return handleClarifyingQuestions(input);
426 }
427 // 다른 도구를 정상적으로 처리
428 return promptForApproval(toolName, input);
429 };
430 ```
431 </CodeGroup>
432 </Step>
433
434 <Step title="질문 입력 구문 분석">
435 입력에는 `questions` 배열의 Claude 질문이 포함됩니다. 각 질문에는 `question`(표시할 텍스트), `options`(선택 사항) 및 `multiSelect`(여러 선택이 허용되는지 여부)가 있습니다.
436
437 ```json theme={null}
438 {
439 "questions": [
440 {
441 "question": "How should I format the output?",
442 "header": "Format",
443 "options": [
444 { "label": "Summary", "description": "Brief overview" },
445 { "label": "Detailed", "description": "Full explanation" }
446 ],
447 "multiSelect": false
448 },
449 {
450 "question": "Which sections should I include?",
451 "header": "Sections",
452 "options": [
453 { "label": "Introduction", "description": "Opening context" },
454 { "label": "Conclusion", "description": "Final summary" }
455 ],
456 "multiSelect": true
457 }
458 ]
459 }
460 ```
461
462 전체 필드 설명은 [질문 형식](#질문-형식)을 참조하십시오.
463 </Step>
464
465 <Step title="사용자로부터 답변 수집">
466 사용자에게 질문을 제시하고 선택 사항을 수집합니다. 이를 수행하는 방법은 애플리케이션에 따라 다릅니다. 터미널 프롬프트, 웹 양식, 모바일 대화 상자 등입니다.
467 </Step>
468
469 <Step title="Claude에 답변 반환">
470 `answers` 객체를 레코드로 구성합니다. 여기서 각 키는 `question` 텍스트이고 각 값은 선택된 옵션의 `label`입니다.
471
472 | 질문 객체에서 | 다음으로 사용 |
473 | ----------------------------------------------------- | ------- |
474 | `question` 필드(예: `"How should I format the output?"`) | 키 |
475 | 선택된 옵션의 `label` 필드(예: `"Summary"`) | 값 |
476
477 다중 선택 질문의 경우 레이블 배열을 전달하거나 `", "`로 조인합니다. [자유 텍스트 입력을 지원](#자유-텍스트-입력-지원)하는 경우 사용자의 사용자 정의 텍스트를 값으로 사용합니다.
478
479 <CodeGroup>
480 ```python Python theme={null}
481 return PermissionResultAllow(
482 updated_input={
483 "questions": input_data.get("questions", []),
484 "answers": {
485 "How should I format the output?": "Summary",
486 "Which sections should I include?": ["Introduction", "Conclusion"],
487 },
488 }
489 )
490 ```
491
492 ```typescript TypeScript theme={null}
493 return {
494 behavior: "allow",
495 updatedInput: {
496 questions: input.questions,
497 answers: {
498 "How should I format the output?": "Summary",
499 "Which sections should I include?": "Introduction, Conclusion"
500 }
501 }
502 };
503 ```
504 </CodeGroup>
505 </Step>
506</Steps>
507
508### 질문 형식
509
510입력에는 `questions` 배열의 Claude 생성 질문이 포함됩니다. 각 질문에는 다음 필드가 있습니다.
511
512| 필드 | 설명 |
513| ------------- | -------------------------------------------------------------------------------------------------------------------- |
514| `question` | 표시할 전체 질문 텍스트 |
515| `header` | 질문의 짧은 레이블(최대 12자) |
516| `options` | 각각 `label` 및 `description`이 있는 2-4개 선택 사항의 배열입니다. TypeScript: 선택적으로 `preview`([아래](#option-previews-type-script) 참조) |
517| `multiSelect` | `true`인 경우 사용자가 여러 옵션을 선택할 수 있습니다. |
518
519콜백이 받는 구조:
520
521```json theme={null}
522{
523 "questions": [
524 {
525 "question": "How should I format the output?",
526 "header": "Format",
527 "options": [
528 { "label": "Summary", "description": "Brief overview of key points" },
529 { "label": "Detailed", "description": "Full explanation with examples" }
530 ],
531 "multiSelect": false
532 }
533 ]
534}
535```
536
537#### 옵션 미리보기(TypeScript)
538
539`toolConfig.askUserQuestion.previewFormat`은 각 옵션에 `preview` 필드를 추가하므로 앱이 레이블 옆에 시각적 목업을 표시할 수 있습니다. 이 설정이 없으면 Claude는 미리보기를 생성하지 않으며 필드가 없습니다.
540
541| `previewFormat` | `preview` 포함 |
542| :-------------- | :--------------------------------------------------------------------------------- |
543| 설정되지 않음(기본값) | 필드가 없습니다. Claude는 미리보기를 생성하지 않습니다. |
544| `"markdown"` | ASCII 아트 및 펜스 코드 블록 |
545| `"html"` | 스타일이 지정된 `<div>` 조각(SDK는 콜백이 실행되기 전에 `<script>`, `<style>` 및 `<!DOCTYPE>`을 거부합니다.) |
546
547형식은 세션의 모든 질문에 적용됩니다. Claude는 시각적 비교가 도움이 되는 옵션(레이아웃 선택, 색 구성표)에 `preview`를 포함하고 도움이 되지 않는 옵션(예/아니오 확인, 텍스트 전용 선택)에서 생략합니다. 렌더링하기 전에 `undefined`를 확인하십시오.
548
549```typescript theme={null}
550import { query } from "@anthropic-ai/claude-agent-sdk";
551
552for await (const message of query({
553 prompt: "Help me choose a card layout",
554 options: {
555 toolConfig: {
556 askUserQuestion: { previewFormat: "html" }
557 },
558 canUseTool: async (toolName, input) => {
559 // input.questions[].options[].preview는 HTML 문자열 또는 undefined입니다.
560 return { behavior: "allow", updatedInput: input };
561 }
562 }
563})) {
564 // ...
565}
566```
567
568HTML 미리보기가 있는 옵션:
569
570```json theme={null}
571{
572 "label": "Compact",
573 "description": "Title and metric value only",
574 "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>"
575}
576```
577
578### 응답 형식
579
580각 질문의 `question` 필드를 선택된 옵션의 `label`에 매핑하는 `answers` 객체를 반환합니다.
581
582| 필드 | 설명 |
583| ----------- | ------------------------------ |
584| `questions` | 원본 질문 배열을 전달합니다(도구 처리에 필수). |
585| `answers` | 키가 질문 텍스트이고 값이 선택된 레이블인 객체입니다. |
586
587다중 선택 질문의 경우 레이블 배열을 전달하거나 `", "`로 조인합니다. 자유 텍스트 입력의 경우 사용자의 사용자 정의 텍스트를 직접 사용합니다.
588
589```json theme={null}
590{
591 "questions": [
592 // ...
593 ],
594 "answers": {
595 "How should I format the output?": "Summary",
596 "Which sections should I include?": ["Introduction", "Conclusion"]
597 }
598}
599```
600
601#### 자유 텍스트 입력 지원
602
603Claude의 사전 정의된 옵션이 항상 사용자가 원하는 것을 다루지는 않습니다. 사용자가 자신의 답변을 입력하도록 허용하려면:
604
605* Claude의 옵션 후에 추가 "Other" 선택을 표시하여 텍스트 입력을 허용합니다.
606* 사용자의 사용자 정의 텍스트를 답변 값으로 사용합니다("Other"라는 단어가 아님).
607
608전체 구현은 아래의 [완전한 예제](#완전한-예제)를 참조하십시오.
609
610### 완전한 예제
611
612Claude는 진행하기 위해 사용자 입력이 필요할 때 명확화 질문을 합니다. 예를 들어 모바일 앱의 기술 스택을 결정하는 데 도움을 달라는 요청을 받으면 Claude는 크로스 플랫폼 대 네이티브, 백엔드 선호도 또는 대상 플랫폼에 대해 물어볼 수 있습니다. 이러한 질문은 Claude가 추측하기보다는 사용자의 선호도와 일치하는 결정을 내리는 데 도움이 됩니다.
613
614이 예제는 터미널 애플리케이션에서 이러한 질문을 처리합니다. 각 단계에서 발생하는 일은 다음과 같습니다.
615
6161. **요청 라우팅**: `canUseTool` 콜백은 도구 이름이 `"AskUserQuestion"`인지 확인하고 전용 핸들러로 라우팅합니다.
6172. **질문 표시**: 핸들러는 `questions` 배열을 반복하고 각 질문을 번호가 매겨진 옵션과 함께 인쇄합니다.
6183. **입력 수집**: 사용자는 숫자를 입력하여 옵션을 선택하거나 자유 텍스트를 직접 입력할 수 있습니다(예: "jquery", "i don't know").
6194. **답변 매핑**: 코드는 입력이 숫자(옵션의 레이블 사용)인지 자유 텍스트(텍스트 직접 사용)인지 확인합니다.
6205. **Claude에 반환**: 응답에는 원본 `questions` 배열과 `answers` 매핑이 모두 포함됩니다.
621
622<CodeGroup>
623 ```python Python theme={null}
624 import asyncio
625
626 from claude_agent_sdk import ClaudeAgentOptions, ResultMessage, query
627 from claude_agent_sdk.types import HookMatcher, PermissionResultAllow
628
629
630 def parse_response(response: str, options: list) -> str:
631 """사용자 입력을 옵션 번호 또는 자유 텍스트로 구문 분석합니다."""
632 try:
633 indices = [int(s.strip()) - 1 for s in response.split(",")]
634 labels = [options[i]["label"] for i in indices if 0 <= i < len(options)]
635 return ", ".join(labels) if labels else response
636 except ValueError:
637 return response
638
639
640 async def handle_ask_user_question(input_data: dict) -> PermissionResultAllow:
641 """Claude의 질문을 표시하고 사용자 답변을 수집합니다."""
642 answers = {}
643
644 for q in input_data.get("questions", []):
645 print(f"\n{q['header']}: {q['question']}")
646
647 options = q["options"]
648 for i, opt in enumerate(options):
649 print(f" {i + 1}. {opt['label']} - {opt['description']}")
650 if q.get("multiSelect"):
651 print(" (Enter numbers separated by commas, or type your own answer)")
652 else:
653 print(" (Enter a number, or type your own answer)")
654
655 response = input("Your choice: ").strip()
656 answers[q["question"]] = parse_response(response, options)
657
658 return PermissionResultAllow(
659 updated_input={
660 "questions": input_data.get("questions", []),
661 "answers": answers,
662 }
663 )
664
665
666 async def can_use_tool(
667 tool_name: str, input_data: dict, context
668 ) -> PermissionResultAllow:
669 # AskUserQuestion을 질문 핸들러로 라우팅
670 if tool_name == "AskUserQuestion":
671 return await handle_ask_user_question(input_data)
672 # 이 예제에서는 다른 도구를 자동 승인
673 return PermissionResultAllow(updated_input=input_data)
674
675
676 async def prompt_stream():
677 yield {
678 "type": "user",
679 "message": {
680 "role": "user",
681 "content": "Help me decide on the tech stack for a new mobile app",
682 },
683 }
684
685
686 # 필수 해결 방법: 더미 훅이 can_use_tool을 위해 스트림을 열어 둠
687 async def dummy_hook(input_data, tool_use_id, context):
688 return {"continue_": True}
689
690
691 async def main():
692 async for message in query(
693 prompt=prompt_stream(),
694 options=ClaudeAgentOptions(
695 can_use_tool=can_use_tool,
696 hooks={"PreToolUse": [HookMatcher(matcher=None, hooks=[dummy_hook])]},
697 ),
698 ):
699 if isinstance(message, ResultMessage) and message.subtype == "success":
700 print(message.result)
701
702
703 asyncio.run(main())
704 ```
705
706 ```typescript TypeScript theme={null}
707 import { query } from "@anthropic-ai/claude-agent-sdk";
708 import * as readline from "readline/promises";
709
710 // 터미널에서 사용자 입력을 프롬프트하는 헬퍼
711 async function prompt(question: string): Promise<string> {
712 const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
713 const answer = await rl.question(question);
714 rl.close();
715 return answer;
716 }
717
718 // 사용자 입력을 옵션 번호 또는 자유 텍스트로 구문 분석
719 function parseResponse(response: string, options: any[]): string {
720 const indices = response.split(",").map((s) => parseInt(s.trim()) - 1);
721 const labels = indices
722 .filter((i) => !isNaN(i) && i >= 0 && i < options.length)
723 .map((i) => options[i].label);
724 return labels.length > 0 ? labels.join(", ") : response;
725 }
726
727 // Claude의 질문을 표시하고 사용자 답변을 수집
728 async function handleAskUserQuestion(input: any) {
729 const answers: Record<string, string> = {};
730
731 for (const q of input.questions) {
732 console.log(`\n${q.header}: ${q.question}`);
733
734 const options = q.options;
735 options.forEach((opt: any, i: number) => {
736 console.log(` ${i + 1}. ${opt.label} - ${opt.description}`);
737 });
738 if (q.multiSelect) {
739 console.log(" (Enter numbers separated by commas, or type your own answer)");
740 } else {
741 console.log(" (Enter a number, or type your own answer)");
742 }
743
744 const response = (await prompt("Your choice: ")).trim();
745 answers[q.question] = parseResponse(response, options);
746 }
747
748 // Claude에 답변 반환(원본 질문 배열 포함 필수)
749 return {
750 behavior: "allow",
751 updatedInput: { questions: input.questions, answers }
752 };
753 }
754
755 async function main() {
756 for await (const message of query({
757 prompt: "Help me decide on the tech stack for a new mobile app",
758 options: {
759 canUseTool: async (toolName, input) => {
760 // AskUserQuestion을 질문 핸들러로 라우팅
761 if (toolName === "AskUserQuestion") {
762 return handleAskUserQuestion(input);
763 }
764 // 이 예제에서는 다른 도구를 자동 승인
765 return { behavior: "allow", updatedInput: input };
766 }
767 }
768 })) {
769 if ("result" in message) console.log(message.result);
770 }
771 }
772
773 main();
774 ```
775</CodeGroup>
776
777## 제한 사항
778
779* **서브에이전트**: `AskUserQuestion`은 현재 Agent 도구를 통해 생성된 서브에이전트에서 사용할 수 없습니다.
780* **질문 제한**: 각 `AskUserQuestion` 호출은 각각 2-4개 옵션이 있는 1-4개 질문을 지원합니다.
781
782## 사용자 입력을 얻는 다른 방법
783
784`canUseTool` 콜백과 `AskUserQuestion` 도구는 대부분의 승인 및 명확화 시나리오를 다루지만, SDK는 사용자로부터 입력을 얻는 다른 방법을 제공합니다.
785
786### 스트리밍 입력
787
788다음이 필요할 때 [스트리밍 입력](/ko/agent-sdk/streaming-vs-single-mode)을 사용하십시오.
789
790* **에이전트 중간에 중단**: Claude가 작업 중일 때 취소 신호를 보내거나 방향을 변경합니다.
791* **추가 컨텍스트 제공**: Claude가 물어볼 때까지 기다리지 않고 필요한 정보를 추가합니다.
792* **채팅 인터페이스 구축**: 장시간 실행되는 작업 중에 사용자가 후속 메시지를 보낼 수 있습니다.
793
794스트리밍 입력은 사용자가 승인 체크포인트에서만이 아니라 실행 전체에서 에이전트와 상호 작용하는 대화형 UI에 이상적입니다.
795
796### 사용자 정의 도구
797
798다음이 필요할 때 [사용자 정의 도구](/ko/agent-sdk/custom-tools)를 사용하십시오.
799
800* **구조화된 입력 수집**: `AskUserQuestion`의 객관식 형식을 넘어서는 양식, 마법사 또는 다단계 워크플로우를 구축합니다.
801* **외부 승인 시스템 통합**: 기존 티켓팅, 워크플로우 또는 승인 플랫폼에 연결합니다.
802* **도메인별 상호 작용 구현**: 코드 검토 인터페이스 또는 배포 체크리스트와 같이 애플리케이션의 필요에 맞는 도구를 만듭니다.
803
804사용자 정의 도구는 상호 작용을 완전히 제어할 수 있지만 기본 제공 `canUseTool` 콜백을 사용하는 것보다 더 많은 구현 작업이 필요합니다.
805
806## 관련 리소스
807
808* [권한 구성](/ko/agent-sdk/permissions): 권한 모드 및 규칙 설정
809* [훅으로 실행 제어](/ko/agent-sdk/hooks): 에이전트 수명 주기의 주요 지점에서 사용자 정의 코드 실행
810* [TypeScript SDK 참조](/ko/agent-sdk/typescript#canusetool): 전체 canUseTool API 문서