Permissions & approvals

Decide which tool calls run on their own, which wait for a person, and which never run.

Every tool call gets one of three answers before it runs: allow, ask (wait for a person to approve) or deny. You set the rules per agent; threads records every decision in the thread's log.

Out of the box, only read-only work runs on its own. Any tool you don't declare as effect: "read_only" asks for approval in the default mode. Allow it with a rule, or mark it read-only if it really is.

Set permissions

const coder = agent({
  name: "coder",
  model: scriptedModel({ responses: [{ content: [{ type: "text", text: "ok" }], stop_reason: "end_turn", usage: { input_tokens: 1, output_tokens: 1 } }] }),
  permissions: {
    mode: "accept_edits",
    allow: ["bash(git status)", "bash(npm test:*)", "web_fetch(domain:docs.example.com)"],
    ask: ["bash(git push:*)"],
    deny: ["bash(rm:*)", "read(secrets/**)", "mcp__github__*"],
  },
  approvers: [{ issuer: "slack:T024BE7LD", tenant: "acme", subject: "U42" }],
});

In TypeScript you pass only the fields you change; the rest keep their defaults. In Python, Permissions (from threads.log) takes every field; the defaults are shown above.

FieldDefaultMeaning
mode"default"The starting mode.
allow / ask / deny[]Rules.
protected_paths.git/**, .threads/**, .claude/**, .mcp.json, shell rc files, .gitconfig, .ssh/**Edits to these always ask, even when a mode or rule would allow them.
allow_bypassfalseWhether the thread may ever switch to bypass.

How a call is decided

The first step that gives an answer wins:

Config guard

Any call that could change threads' own config (a .threads path in any argument, outside read-only tools) is denied. An agent can never rewrite its own setup.

Deny rules

A matching deny rule denies.

Plan mode

In plan mode, anything that isn't read-only is denied, except the agent's todo list.

Protected paths

An edit to a protected path asks.

Ask, then allow rules

A matching ask rule asks; otherwise a matching allow rule allows.

The mode

Nothing matched, so the current mode decides.

Hooks run alongside this: a beforeTool hook can deny or ask for any call, and a permissionRequest hook can answer a call that would ask. See Hooks.

Modes

Tools fall into three groups: read-only (tools declared effect: "read_only", such as read, ls, glob, grep), edits (write, edit, notebook_edit) and everything else (shell, web, your side-effecting tools). Reads inside the sandbox workspace are always allowed.

ModeReads outside the workspaceEdits in the workspaceEverything else
defaultaskaskask
accept_editsaskallowask
planaskdenydeny
dont_askdenydenydeny
bypassallowallowallow

In dont_ask, anything that would ask is denied instead, which suits unattended jobs. plan lets the agent look around and plan without changing anything; its todo list still works. Leave plan mode by switching the thread's mode yourself. bypass needs allow_bypass: true.

Change the mode

Switch a thread's mode between runs. The change is recorded in the log and applies from the next step.

const planned = await thread.setMode("plan", me); // read-only from the next step
if (!planned.ok) console.log(planned.error.code);
const back = await thread.setMode("default", me);
if (!back.ok) console.log(back.error.code);

const bypass = await thread.setMode("bypass", me);
if (!bypass.ok) console.log(bypass.error.code); // invalid_transition unless allow_bypass: true

thread is a thread handle (see Human-in-the-loop) and me is the principal making the change.

Rules

A rule is a tool name, optionally with a specifier in parentheses: tool or tool(specifier). A tool name ending in * matches by prefix, such as mcp__github__* for every tool of one MCP server.

ToolSpecifierExample
File tools (read, write, edit, ls, glob, grep, notebook_edit)A gitignore-style glob, relative to the workspaceedit(src/**), read(secrets/**)
bashAn exact command, or a prefix ending in :*bash(git status), bash(npm test:*)
web_fetchdomain: a host, or domain:*. a parent domainweb_fetch(domain:*.example.com)
spawn_agent, handoffAn agent namespawn_agent(researcher)
Any toolNonesend_email, mcp__github__*

For bash, a command is split into its simple commands (a && b | c). A deny or ask rule matches if any of them matches. An allow rule allows only if the command parses and every one of them is allowed.

Approvals

When a call asks, the run stops and returns parked with reason awaiting_approval. A person approves or denies it, then the thread continues. The full flow is on Human-in-the-loop.

When approving, you can keep one of the call's suggested rules (such as send_email or bash(npm test:*)), so later matching calls on that thread run without asking.

Who may approve

approvers lists the principals who may answer this agent's approvals through the host, including for its subagents and handoff targets.

When approvers is unset, the default differs by language today. TypeScript: any authenticated principal of the thread's tenant through the HTTP API, and nobody through a channel. Python: the principal who started the run. Set approvers explicitly if it matters.

Ceilings

Pass ceiling to a run to cap everything it starts. Each call is decided under the agent's own permissions and again under the ceiling, and the stricter answer wins. A ceiling covers the run's subagents and any agent it hands off to.

const result = await coder.run("hi", { store, ceiling: { deny: ["bash(curl:*)"] } });

Subagents are always capped by their parent's permissions this way. See Subagents.

Edit on GitHub

On this page