Chapter 08 of 14 · Part 2: Build Your First Agent

Chapter 8: Permission Policies — Letting Claude Act Safely

By the end of this chapter, you will know how to configure permission policies for every tool in your agent, understand the tool confirmation flow, and be able to design a permission model that balances autonomy with oversight.


The Big Idea

An agent with no oversight is a liability. An agent that asks for permission on every action is useless. Permission policies are the mechanism that lets you define exactly where that line is — per tool, per agent — so Claude acts autonomously on routine operations and pauses for human judgment on consequential ones.

According to the permission policies documentation, "Permission policies control whether server-executed tools (the pre-built agent toolset and MCP toolset) run automatically or wait for your approval. Custom tools are executed by your application and controlled by you, so they are not governed by permission policies."

There are only two policy types:

Policy Behavior
always_allow The tool executes automatically with no confirmation
always_ask The session emits a session.status_idle event and waits for a user.tool_confirmation event before executing

Two policies, cleanly applied at the toolset or individual-tool level. This simplicity is deliberate — you don't need a complex permission model when you have a binary choice at every level of granularity you need.

Permission Flow — always_allow vs always_ask Policy A sequence diagram showing how permission policies work. User request reaches the agent, which plans a tool_use. The permission policy check branches: always_allow executes automatically, while always_ask emits session.status_idle and waits for user.tool_confirmation — the user approves or denies — before execution proceeds or stops. Permission Policies Control whether server-executed tools run automatically or wait for your approval Your App Permission Policy Agent / Tool user.message + tool_use plan policy check always_allow auto-executes bash, read, write… tool_result → agent continues always_ask session.status_idle stop_reason: requires_action awaiting confirmation approve or deny? allow user.tool_confirmation result: "allow" ✓ tool executes deny ✗ tool skipped result: "deny" deny_message: "use staging" always_allow — auto-executes always_ask — awaits approval MCP tools default to always_ask
Two large toggle switches side by side. Left toggle in "on" position labeled "always_allow" — underneath: "Tool executes immediately. No pause." Right toggle in "off/pause" position labeled "always_ask" — underneath: "Session pauses. Waits for your approval." Between them, a small text: "Set this per tool, per agent."

The Analogy

Think of permission policies like the authorization levels in a large organization.

Some employees can approve purchases up to $500 without a supervisor signature. That's always_allow — fast, autonomous, trusted for routine amounts.

Anything over $500 requires a manager's approval before the purchase is made. That's always_ask — the action pauses, the request surfaces, a human decides.

The approval threshold is set by policy, not by the employee. A coding agent that reads and writes files is like an employee with authority to purchase office supplies. A coding agent that runs bash commands against a production database is like one requesting equipment worth $50,000 — you want a signature before that happens.

The key insight: well-designed permission policies mean your agent moves fast on low-risk actions and slows down at the right moments. The goal isn't maximum restriction or maximum autonomy — it's the right line in the right place.

DiagramOrg chart approval flow. Low-risk tools (read, glob, grep, web_search) on the left — green path directly to "Executes." High-risk tools (bash, write, edit) in the middle — yellow path through an "Approval Gate" box before "Executes." Label: "always_allow = direct path. always_ask = approval gate."

How It Actually Works

The Defaults

The defaults are set by toolset type and are important to know:

  • Agent toolset (agent_toolset_20260401): defaults to always_allow. If you omit the default_config, every built-in tool runs automatically without confirmation. (Permission policies)

  • MCP toolset: defaults to always_ask. "This ensures that new tools that are added to an MCP server do not execute in your application without approval." (Permission policies)

The MCP default is worth understanding: when you connect an MCP server, new tools might be added to it over time by the server operator. Defaulting to always_ask means those new tools can't execute automatically in your agent just because they appeared on the server — you have to explicitly allow them.

Setting a Policy for All Tools in a Toolset

To require approval for every built-in tool:

ant beta:agents create <<'YAML'
name: Coding Assistant
model: claude-opus-4-7
tools:
  - type: agent_toolset_20260401
    default_config:
      permission_policy:
        type: always_ask
YAML

(Permission policies)

This is the conservative starting point for new agents — understand what the agent is doing before you trust it to act autonomously. Approve a few hundred tool calls, build confidence, then loosen the policy on tools you're comfortable with.

Overriding a Single Tool's Policy

The most useful pattern in practice: allow low-risk tools automatically, require approval only for high-risk ones.

tools = [
    {
        "type": "agent_toolset_20260401",
        "default_config": {
            "permission_policy": {"type": "always_allow"},
        },
        "configs": [
            {
                "name": "bash",
                "permission_policy": {"type": "always_ask"},
            },
        ],
    },
]

(Permission policies)

This configuration lets the agent read files, write files, search the web, and grep through code automatically — but pauses before executing any bash command. For most coding agents, this is the right balance: bash is powerful enough to cause real damage if something goes wrong.

MCP Tool Policy Example

For an MCP server you trust (like GitHub), you can explicitly allow all its tools:

ant beta:agents create <<'YAML'
name: Dev Assistant
model: claude-opus-4-7
mcp_servers:
  - type: url
    name: github
    url: https://mcp.example.com/github
tools:
  - type: agent_toolset_20260401
  - type: mcp_toolset
    mcp_server_name: github
    default_config:
      permission_policy:
        type: always_allow
YAML

(Permission policies)

The mcp_server_name must match the name you assigned in the mcp_servers array. When you override the MCP toolset's default from always_ask to always_allow, you're explicitly trusting that server's tools to run without confirmation.

The Tool Confirmation Flow

When a tool with always_ask policy is invoked, the following sequence happens:

  1. The session emits an agent.tool_use or agent.mcp_tool_use event — Claude is requesting to use a tool.
  2. The session pauses with a session.status_idle event containing stop_reason: requires_action. The blocking event IDs are in the stop_reason.requires_action.event_ids array.
  3. Your application reviews the pending tool call and sends a user.tool_confirmation event.
  4. Once all blocking events are resolved, the session transitions back to running.

(Permission policies)

Allowing a tool call:

# Allow the tool to execute
client.beta.sessions.events.send(
    session.id,
    events=[
        {
            "type": "user.tool_confirmation",
            "tool_use_id": agent_tool_use_event.id,
            "result": "allow",
        },
    ],
)

Denying a tool call with an explanation:

# Or deny it with an explanation
client.beta.sessions.events.send(
    session.id,
    events=[
        {
            "type": "user.tool_confirmation",
            "tool_use_id": mcp_tool_use_event.id,
            "result": "deny",
            "deny_message": "Don't create issues in the production project. Use the staging project.",
        },
    ],
)

(Permission policies)

The deny_message is passed back to Claude as context. This is important — Claude doesn't just stop; it receives your explanation and can adjust its approach. A well-written denial message is guidance, not just a rejection. "Don't create issues in the production project. Use the staging project." tells Claude exactly what to do differently.

Custom Tools and Permissions

Permission policies apply only to server-executed tools (the built-in toolset and MCP toolset). Custom tools work differently:

"Permission policies do not apply to custom tools. When the agent invokes a custom tool, your application receives an agent.custom_tool_use event and is responsible for deciding whether to execute it before sending back a user.custom_tool_result." (Permission policies)

For custom tools, your application code is the permission layer. You receive the tool call request, decide whether to execute it (based on your own business logic), run it if appropriate, and send the result back.

Designing Your Permission Model

A practical approach:

Low-risk (always_allow): read, glob, grep, web_search, web_fetch — these are read-only or network operations that don't modify state.

Medium-risk (always_allow for trusted agents, always_ask while building confidence): write, edit — these modify files, which is their purpose, but can be reviewed if needed.

High-risk (always_ask until well-tested): bash — arbitrary command execution. Start with approval required, then relax to always_allow once you've reviewed enough sessions to trust the agent's judgment.

MCP tools (always_ask by default, always_allow once trusted): The default always_ask on MCP tools is correct for new integrations. As you build confidence with a specific MCP server, you can upgrade to always_allow.

DiagramRisk-tiered tool permission ladder. Bottom rung (green, always_allow): read, glob, grep, web_search, web_fetch. Middle rung (yellow, always_allow after confidence): write, edit. Top rung (red, always_ask): bash. Separate column for MCP tools: always_ask by default → always_allow after review. Caption: "Start conservative. Loosen as you build confidence."

Try it yourself

Try It Yourself

  1. Create an agent with bash on always_ask:

    tools = [
        {
            "type": "agent_toolset_20260401",
            "default_config": {
                "permission_policy": {"type": "always_allow"},
            },
            "configs": [
                {
                    "name": "bash",
                    "permission_policy": {"type": "always_ask"},
                },
            ],
        },
    ]
    
    agent = client.beta.agents.create(
        name="Careful Coding Agent",
        model="claude-sonnet-4-6",
        system="You are a coding assistant.",
        tools=tools,
    )
    
  2. Send a task that requires bash. Give the agent a coding task that will require running a script — for example, "Write a Python script that calculates prime numbers up to 100 and run it to verify the output."

  3. Watch the confirmation flow. When the agent tries to run bash, the session will pause with session.status_idle and stop_reason: requires_action. Retrieve the event ID and approve it:

    # Approve the bash call
    client.beta.sessions.events.send(
        session.id,
        events=[
            {
                "type": "user.tool_confirmation",
                "tool_use_id": "<event_id_from_stop_reason>",
                "result": "allow",
            },
        ],
    )
    
  4. Try denying a bash call. On a subsequent session, when the agent tries to run bash, send a denial with a redirect:

    client.beta.sessions.events.send(
        session.id,
        events=[
            {
                "type": "user.tool_confirmation",
                "tool_use_id": "<event_id>",
                "result": "deny",
                "deny_message": "Don't run the script yet. First write it to a file and show me the code.",
            },
        ],
    )
    

    Observe how Claude adjusts based on your denial message.

  5. Review the event log after the session. List all events for the session and identify the agent.tool_use events, the session.status_idle events with requires_action, and the user.tool_confirmation events. This is your audit trail.

DiagramSequence diagram of the tool confirmation flow. Left column: "Your Application." Right column: "Session." Arrows: agent.tool_use event (session → app), session.status_idle + requires_action (session → app), user.tool_confirmation (app → session), session running again (session). Annotated with timing labels.

Common pitfalls

Common Pitfalls

  • Leaving all MCP tools on always_ask and never building the approval loop. If you don't implement the confirmation flow in your code, MCP tool calls will block forever. The session will sit in idle with requires_action until you either send a confirmation or the session times out.

  • Using a denial message that doesn't give Claude a direction. "denied" is not a useful denial message. "Don't use bash for this — use the write tool instead and show me the code" is. Claude uses your message as guidance for its next step.

  • Thinking always_ask is purely protective. It is protective, but it's also a workflow tool. You can use always_ask on specific tools as a checkpoint in your pipeline — a way to review what Claude is about to do and give it refined instructions before it proceeds.

  • Not distinguishing between built-in, MCP, and custom tools. Permission policies apply to the first two. Custom tools are your responsibility. Don't assume that because you've set always_ask for the built-in toolset, your custom tools are also gated.

  • Setting always_ask on read tools. This creates unnecessary friction. read, glob, and grep are observation tools — they don't change state. Requiring approval for every file read will make your agent painfully slow with no meaningful security benefit.


Toolkit

Toolkit

  • Permission Policy Design Template — A worksheet for designing your permission model: list each tool, assign its default policy, and note the reasoning. Includes a suggested starting point for five common agent types.

  • Tool Confirmation Loop — Code Snippet Library — Ready-to-use Python code snippets for: (1) approving all pending tool calls automatically, (2) building a human-in-the-loop approval prompt, (3) implementing automatic approval with a deny-list of specific tool patterns.


Chapter Recap

  • Permission policies are binary: always_allow (auto-execute) and always_ask (pause and wait for your confirmation). Set them at the toolset level or override per individual tool.
  • The built-in agent toolset defaults to always_allow. MCP toolsets default to always_ask. This asymmetry is intentional — new MCP tools shouldn't execute automatically without your review.
  • The tool confirmation flow: session pauses with session.status_idlestop_reason: requires_action → you send user.tool_confirmation with allow or deny → session resumes. Include a deny_message when denying — Claude uses it to adjust its approach.