All skills
Skillintermediate

Handle approvals and user input

> ## Documentation Index > Fetch the complete documentation index at: https://code.claude.com/docs/llms.txt > Use this file to discover all available pages before exploring further.

Claude Code Knowledge Pack7/10/2026

Overview

Documentation Index

Fetch the complete documentation index at: https://code.claude.com/docs/llms.txt Use this file to discover all available pages before exploring further.

Handle approvals and user input

Surface Claude's approval requests and clarifying questions to users, then return their decisions to the SDK.

While working on a task, Claude sometimes needs to check in with users. It might need permission before deleting files, or need to ask which database to use for a new project. Your application needs to surface these requests to users so Claude can continue with their input.

Claude requests user input in two situations: when it needs permission to use a tool (like deleting files or running commands), and when it has clarifying questions (via the AskUserQuestion tool). Both trigger your canUseTool callback, which pauses execution until you return a response. This is different from normal conversation turns where Claude finishes and waits for your next message.

For clarifying questions, Claude generates the questions and options. Your role is to present them to users and return their selections. You can't add your own questions to this flow; if you need to ask users something yourself, do that separately in your application logic.

The callback can stay pending indefinitely. Execution remains paused until your callback returns, and the SDK only cancels the wait when the query itself is cancelled. If a user might take longer to respond than your process can reasonably stay running, the TypeScript SDK supports the defer hook decision, which lets the process exit and resume later from the persisted session; this option is not available in the Python SDK.

This guide shows you how to detect each type of request and respond appropriately.

Detect when Claude needs input

Pass a canUseTool callback in your query options. The callback fires whenever Claude needs user input, receiving the tool name and input as arguments:

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

The callback fires in two cases:

  1. Tool needs approval: Claude wants to use a tool that isn't auto-approved by permission rules or modes. Check tool_name for the tool (e.g., "Bash", "Write").
  2. Claude asks a question: Claude calls the AskUserQuestion tool. Check if tool_name == "AskUserQuestion" to handle it differently. If you specify a tools array, include AskUserQuestion for this to work. See Handle clarifying questions for details.

To automatically allow or deny tools without prompting users, use hooks instead. Hooks execute before canUseTool and can allow, deny, or modify requests based on your own logic. You can also use the PermissionRequest hook to send external notifications (Slack, email, push) when Claude is waiting for approval.

Handle tool approval requests

Once you've passed a canUseTool callback in your query options, it fires when Claude wants to use a tool that isn't auto-approved. Your callback receives three arguments:

ArgumentDescription
toolNameThe name of the tool Claude wants to use (e.g., "Bash", "Write", "Edit")
inputThe parameters Claude is passing to the tool. Contents vary by tool.
options (TS) / context (Python)Additional context including optional suggestions (proposed PermissionUpdate entries to avoid re-prompting) and a cancellation signal. In TypeScript, signal is an AbortSignal; in Python, the signal field is reserved for future use. See ToolPermissionContext for Python.

The input object contains tool-specific parameters. Common examples:

ToolInput fields
Bashcommand, description, timeout
Writefile_path, content
Editfile_path, old_string, new_string
Readfile_path, offset, limit

See the SDK reference for complete input schemas: Python | TypeScript.

You can display this information to the user so they can decide whether to allow or reject the action, then return the appropriate response.

The following example asks Claude to create and delete a test file. When Claude attempts each operation, the callback prints the tool request to the terminal and prompts for y/n approval.


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"\
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}")

    # 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())

// 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(`\
Tool: ${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);
}

In Python, can_use_tool requires streaming mode and a PreToolUse hook that returns {"continue_": True} to keep the stream open. Without this hook, the stream closes before the permission callback can be invoked.

This example uses a y/n flow where any input other than y is treated as a denial. In practice, you might build a richer UI that lets users modify the request, provide feedback, or redirect Claude entirely. See Respond to tool requests for all the ways you can respond.

Respond to tool requests

Your callback returns one of two response types:

ResponsePythonTypeScript
AllowPermissionResultAllow(updated_input=...){ behavior: "allow", updatedInput }
DenyPermissionResultDeny(message=...){ behavior: "deny", message }

When allowing, pass the tool input (original or modified). When denying, provide a message explaining why. Claude sees this message and may adjust its approach.

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

Beyond allowing or denying, you can modify the tool's input or provide context that helps Claude adjust its approach:

  • Approve: let the tool execute as Claude requested

  • Approve with changes: modify the input before execution (e.g., sanitize paths, add constraints)

  • Reject: block the tool and tell Claude why

  • Suggest alternative: block but guide Claude toward what the user wants instead

  • Redirect entirely: use streaming input to send Claude a completely new instruction

    The user approves the action as-is. Pass through the input from your callback unchanged and the tool executes exactly as Claude requested.

    ```python Python theme={null}
    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 TypeScript theme={null}
    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" };
    };
    ```
    

    The user approves but wants to modify the request first. You can change the input before the tool executes. Claude sees the result but isn't told you changed anything. Useful for sanitizing parameters, adding constraints, or scoping access.

    ```python Python theme={null}
    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)
    ```
    
    ```typescript TypeScript theme={null}
    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 };
    };
    ```
    

    The user doesn't want this action to happen. Block the tool and provide a message explaining why. Claude sees this message and may try a different approach.

    ```python Python theme={null}
    async def can_use_tool(tool_name, input_data, context)