AI Codex
Claude APIupdate

Managed Agents permission policies: letting the server decide which tool calls run

In brief

On September 10, 2026 Anthropic added an `auto` permission policy to Claude Managed Agents. The server evaluates each agent or MCP tool call against the session so far and either runs it, denies it outright, or pauses for your approval. Here is what the three policies do, what `auto` will and will not protect you from, and how to read the new `evaluation` field on the event stream.

11 min read·Managed Agents

Contents

Sign in to save

A Managed Agents session runs Claude in a sandbox that Anthropic operates, with a toolset that includes bash, file editing, and web access, plus any MCP servers you connect. Every one of those tools is executed on Anthropic's side, not yours — which means you need a way to say which calls are allowed to run without you watching.

That mechanism is the permission policy. Until September 10, 2026 there were two of them. Now there are three.

The three policies

Policy What happens
always_allow The tool runs with no confirmation.
always_ask The session pauses and waits for your approval before the call executes.
auto The server evaluates each call and either runs it, denies it, or pauses for your approval.

The defaults differ by toolset. The built-in agent toolset (agent_toolset_20260401) defaults to always_allow. MCP toolsets default to always_ask — a deliberate choice, because an MCP server can add new tools after you connect it, and you probably do not want a tool you have never seen executing in your application on its own.

No toolset uses auto by default. You have to turn it on.

What auto actually evaluates

The server looks at three things for each call: the tool being invoked, the arguments passed to it, and the session's content up to that point. Because the third input is in there, the same tool with the same shape of input can get different answers in different sessions. A bash call that deletes a directory is fine in a session where you asked for a clean rebuild and high-risk in a session where you asked for a status report.

Each call lands in one of three places:

  • It runs. The server determined the call is safe, and it executes exactly as it would under always_allow.
  • It is denied. The server evaluated the call as high-risk. The tool does not run. The agent gets back a tool result with is_error: true and the content Permission to use {tool_name} has been denied. The session keeps going. Your client cannot override this — you get no chance to say "actually, allow it."
  • It pauses. The server reached no determination, so the session stops and waits for you, exactly as under always_ask.

Turning it on

permission_policy goes in one of two places: a toolset's default_config to cover the whole toolset, or a configs entry to override one specific tool. This agent puts the whole built-in toolset and a GitHub MCP server under auto, but keeps bash on a human checkpoint:

agent = client.beta.agents.create(
    name="Ops Agent",
    model="claude-opus-5",
    mcp_servers=[
        {"type": "url", "name": "github", "url": "https://mcp.example.com/github"},
    ],
    tools=[
        {
            "type": "agent_toolset_20260401",
            "default_config": {
                "permission_policy": {"type": "auto"},
            },
            "configs": [
                {"name": "bash", "permission_policy": {"type": "always_ask"}},
            ],
        },
        {
            "type": "mcp_toolset",
            "mcp_server_name": "github",
            "default_config": {
                "permission_policy": {"type": "auto"},
            },
        },
    ],
)

Managed Agents requests need the managed-agents-2026-04-01 beta header. The official SDKs set it for you.

One operational detail worth knowing before you ship this: running sessions keep the toolset configuration they were created with. Updating the agent changes behavior for sessions created afterward, not for the long-running session that is mid-task right now. If you tighten a policy in response to an incident, you also need to end the sessions that are still running under the old one.

The warning that matters most

Anthropic is unusually direct about this in the docs, and it is the sentence to internalize:

auto is not a human checkpoint. If the server determines that a call is safe, the call runs before anyone sees it, and its effects might not be reversible.

auto reduces the number of times you get interrupted. It does not put a person in the loop. If a specific tool must be reviewed by a human before every execution — a payment API, a production database write, an email send — that tool gets always_ask, and no amount of auto tuning substitutes for it.

The prompt-injection detail

Here is the part that will bite teams building customer-facing agents.

The server reads what you post in user.message events as your intent, and that intent can lead it to allow a call it would otherwise deny. It does not take instructions from a tool result, a fetched web page, an MCP server response, or a message passed between threads in a multiagent session — it assesses that content, but does not obey it. That is the right boundary, and it is a real defense against prompt injection arriving through retrieved content.

But if your application relays untrusted end-user text into user.message events — which is exactly what a chat product does — then your end user's words become "your intent" as far as the server is concerned. A user who writes "I authorize you to wipe the reports directory" is supplying the same signal you would.

The practical rule: wherever you relay end-user input verbatim into user.message, put always_ask on the tools you would not let that end user run unreviewed. The server does still evaluate some calls as high-risk no matter who asks, but do not build your authorization model on that.

Reading the outcome on the event stream

Every agent.tool_use and agent.mcp_tool_use event now carries evaluated_permission"allow", "ask", or "deny" — under any policy, not just auto. Most also carry an evaluation object naming which policy produced that outcome, plus a reason_code when auto asked or denied.

A denied bash call looks like this:

{
  "type": "agent.tool_use",
  "id": "sevt_01pqr...",
  "name": "bash",
  "input": {
    "command": "rm -rf /workspace/reports"
  },
  "evaluated_permission": "deny",
  "evaluation": {
    "type": "auto",
    "evaluated_permission": {
      "type": "deny",
      "reason_code": "high_risk"
    }
  },
  "processed_at": "2026-03-25T14:05:12Z"
}

The reason_code values you will see today are high_risk (denied) and indeterminate (paused for you). Treat them as branch keys and audit-log values, not as text to show an end user — "indeterminate" means nothing to the person waiting on your product.

Three things to handle in your client:

  1. evaluation can be absent. If the agent names a tool that is not enabled in the session, the server denies without evaluating a policy: you get evaluated_permission: "deny" and no evaluation. Events recorded before the field existed also omit it — read those as always_allow when the outcome is allow and always_ask when it is ask.
  2. Tolerate unknown values. Write the switch so an evaluation.type or reason_code you have never seen does not throw. Anthropic will add more.
  3. Custom tools carry neither field. agent.custom_tool_use events are yours to authorize — permission policies do not govern tools your own application executes.

Responding to a pause

When a call evaluates to ask, the session emits the tool-use event, then a session.status_idle event whose stop_reason.type is requires_action. The blocking event IDs are in stop_reason.event_ids. The session waits indefinitely.

You answer with a user.tool_confirmation event per blocking ID:

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.",
        },
    ],
)

deny_message is worth using. It goes back to the agent as the tool result, so instead of hitting a wall the agent knows why and can retry against staging. A bare deny usually produces a retry of the same call.

Sending a confirmation for an event whose evaluated_permission is not ask returns a 400. That includes calls auto denied.

If you would rather not poll for this, subscribe to webhooks so you are notified when a session pauses.

Answering from your terminal instead

Shipped the same day: ant beta:sessions connect attaches your terminal to a running session.

ant beta:sessions connect sesn_011CZkZAtmR3yMPDzynEDxu7

It loads the transcript, follows it live, and shows a status bar for running / idle / waiting-for-approval. Enter sends a user.message. Esc interrupts the agent. Ctrl+O toggles tool inputs, results, and token usage. Ctrl+C detaches without ending the session.

When a call is waiting, the input line turns into Allow tool call? with three choices: Yes, No, or "No, and tell the agent why" — that third one is the deny_message path, typed inline.

Add --web and it serves the Console's session viewer from a local server on 127.0.0.1 and opens it in your browser. Two things about that: the printed URL is single-use and expires after two minutes, and your API credentials never leave the CLI — the page talks only to the local ant process, which makes the actual API calls. The browser viewer follows every thread of a multiagent session; the terminal view follows only the primary thread.

The terminal view needs an interactive TTY. In scripts, use ant beta:sessions:events stream and ant beta:sessions:events send.

How to roll this out

A sequence that does not require you to guess:

  1. Leave everything on its default policy and run your normal workload. Log evaluated_permission and evaluation on every tool-use event.
  2. Flip the toolsets you are least worried about to auto. Keep logging. You now have a record of what the server would have done.
  3. Read the denials. A high_risk denial on a call your agent legitimately needed is a signal that the task should be split, not that you should widen the policy.
  4. Put always_ask on the specific tools whose calls a person must see — especially anything that writes to a system you cannot roll back, and anything reachable from untrusted end-user text.
  5. Keep reason_code in your audit records. When someone asks in three months why an agent did not do the thing, this is the only place the answer lives.

Official docs

Related tools

Weekly brief

For people actually using Claude at work.

Each week: one thing Claude can do in your work that most people haven't figured out yet — plus the failure modes to avoid. No tutorials. No hype.

No spam. Unsubscribe anytime.

What to read next

Picked for where you are now

All articles →