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.
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.
How It Actually Works
The Defaults
The defaults are set by toolset type and are important to know:
Agent toolset (
agent_toolset_20260401): defaults toalways_allow. If you omit thedefault_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
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"},
},
],
},
]
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
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:
- The session emits an
agent.tool_useoragent.mcp_tool_useevent — Claude is requesting to use a tool. - The session pauses with a
session.status_idleevent containingstop_reason: requires_action. The blocking event IDs are in thestop_reason.requires_action.event_idsarray. - Your application reviews the pending tool call and sends a
user.tool_confirmationevent. - Once all blocking events are resolved, the session transitions back to
running.
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.",
},
],
)
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.
Try It Yourself
Create an agent with
bashonalways_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, )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."
Watch the confirmation flow. When the agent tries to run
bash, the session will pause withsession.status_idleandstop_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", }, ], )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.
Review the event log after the session. List all events for the session and identify the
agent.tool_useevents, thesession.status_idleevents withrequires_action, and theuser.tool_confirmationevents. This is your audit trail.
Common Pitfalls
Leaving all MCP tools on
always_askand 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 inidlewithrequires_actionuntil 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_askis purely protective. It is protective, but it's also a workflow tool. You can usealways_askon 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_askfor the built-in toolset, your custom tools are also gated.Setting
always_askon read tools. This creates unnecessary friction.read,glob, andgrepare 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
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) andalways_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 toalways_ask. This asymmetry is intentional — new MCP tools shouldn't execute automatically without your review. - The tool confirmation flow: session pauses with
session.status_idle→stop_reason: requires_action→ you senduser.tool_confirmationwithallowordeny→ session resumes. Include adeny_messagewhen denying — Claude uses it to adjust its approach.