ASK KNOX
beta
LESSON 309

Tool Confirmation and Custom Tools

Giving agents tools is giving them the ability to take actions in the world — permission policies and custom tool design are how you control exactly what actions they can take.

9 min read

Every tool you give an agent is a capability to take action in the world. Web search can retrieve information — safe. Bash can run arbitrary commands — powerful. A custom database tool can modify production records — consequential. The tool is the mechanism; the permission policy is the safety gate; the schema is the contract.

These are not independent concerns. An agent with broad tools and loose permission policies in a production environment is not an autonomous agent — it is a liability waiting to materialize.

Permission Policies

Permission policies control whether the agent can call a tool automatically or whether human confirmation is required:

agent = client.beta.agents.create(
    name="data-pipeline-agent",
    model="claude-sonnet-4-6",
    system="...",
    tools=[
        {
            "type": "bash",
            "permission": "auto_allow"  # Agent runs bash freely
        },
        {
            "type": "database_write",
            "permission": "always_ask"  # Requires confirmation before writes
        }
    ]
)

auto_allow — the agent can call this tool at any time without confirmation. Use for read-only tools, low-risk actions, and any tool where the cost of a wrong call is low and reversible.

auto_deny — the tool is blocked. The agent can see the tool exists but every call is rejected. Use when you want to include a tool in the agent's schema for reasoning purposes but prevent actual execution in the current context.

always_ask — each call requires explicit confirmation from a human or a confirmation handler. Use for destructive actions, external communications, financial operations, and anything where the cost of a wrong call is high or irreversible.

The Confirmation Handler

When a tool has always_ask, the session pauses and emits a confirmation request event. Your code handles this:

with client.beta.agents.sessions.stream(session.id) as stream:
    for event in stream:
        if event.type == "tool_confirmation_required":
            tool_name = event.tool_name
            tool_input = event.tool_input
            
            print(f"Agent wants to call: {tool_name}")
            print(f"With input: {tool_input}")
            
            # Your approval logic here
            should_approve = review_tool_call(tool_name, tool_input)
            
            if should_approve:
                client.beta.agents.sessions.confirm_tool(
                    session.id,
                    event.confirmation_id,
                    approved=True
                )
            else:
                client.beta.agents.sessions.confirm_tool(
                    session.id,
                    event.confirmation_id,
                    approved=False,
                    reason="Request does not meet approval criteria"
                )

The agent receives the confirmation result and handles it appropriately — either proceeding with the tool result or adapting its approach given the denial.

Routing Tool Results in Multi-Agent Sessions

In multi-agent sessions, tool calls happen in specific threads. The confirmation event includes a session_thread_id that identifies which thread's tool call is waiting for confirmation. Route confirmations to the correct thread:

if event.type == "tool_confirmation_required":
    thread_id = event.session_thread_id
    print(f"Tool confirmation needed in thread: {thread_id}")
    # Route to appropriate approval workflow for this thread

Custom Tools

Built-in tools (bash, web_search, file_search) cover common use cases. For domain-specific capabilities — querying an internal database, calling a proprietary API, accessing a custom data source — custom tools extend the agent's action space.

Custom tool definition uses the same JSON Schema format as standard Messages API tools:

custom_database_tool = {
    "name": "query_customer_database",
    "description": "Query the customer database for account information. Use this when you need customer records, account status, or transaction history. Returns structured JSON with customer data.",
    "input_schema": {
        "type": "object",
        "properties": {
            "customer_id": {
                "type": "string",
                "description": "The unique customer identifier (format: CUS-XXXXXXXX)"
            },
            "fields": {
                "type": "array",
                "items": {"type": "string"},
                "description": "List of fields to return. Options: name, email, account_status, recent_transactions, subscription_tier",
                "default": ["name", "email", "account_status"]
            }
        },
        "required": ["customer_id"]
    }
}

agent = client.beta.agents.create(
    name="customer-service-agent",
    model="claude-sonnet-4-6",
    system="...",
    tools=[
        custom_database_tool,
        {"type": "bash"}
    ]
)

Handling Custom Tool Calls

When the agent calls a custom tool, a tool_use event is emitted with the tool name and input. Your code executes the tool and returns the result:

def handle_tool_call(tool_name: str, tool_input: dict) -> str:
    if tool_name == "query_customer_database":
        customer_id = tool_input["customer_id"]
        fields = tool_input.get("fields", ["name", "email", "account_status"])
        
        # Execute against your actual database
        result = database.query_customer(customer_id, fields)
        return json.dumps(result)
    
    raise ValueError(f"Unknown tool: {tool_name}")

with client.beta.agents.sessions.stream(session.id) as stream:
    for event in stream:
        if event.type == "tool_use" and event.tool_name == "query_customer_database":
            result = handle_tool_call(event.tool_name, event.tool_input)
            
            # Return the result to the session
            client.beta.agents.sessions.submit_tool_result(
                session.id,
                tool_use_id=event.tool_use_id,
                content=result
            )

Writing Effective Tool Descriptions

The tool description and input schema are what the agent reads to understand how to use the tool. Write them for the agent, not for a human reading the code:

Description: Explain what the tool does, when to use it (and when not to), and what it returns. "Query the customer database" is insufficient. "Query the customer database for account information. Use this when you need customer records, account status, or transaction history. Returns structured JSON with customer data." gives the agent the context to use the tool correctly.

Parameter descriptions: Every parameter needs a description that explains what it is, what format it expects, and what valid values look like. "description": "The customer ID" is insufficient. "description": "The unique customer identifier (format: CUS-XXXXXXXX)" prevents malformed calls.

Required vs optional: Mark only genuinely required parameters as required. Optional parameters with good defaults reduce the cognitive load on the agent.