- Added `ToolTier`, `ToolApproval`, and `ToolApprovalDecision` types and exported approval APIs. - Updated approval-mode options from `auto|prompt|custom` to `always-ask|write|yolo` and defaulted mode to `yolo`. - Changed approval resolution to apply per-tool decisions first, then mode-tier limits, with legacy-mode migration. - Assigned read/write/exec `approval` and approval-detail prompts across built-in, custom, extension, and MCP tools.
3.4 KiB
Tool approval mode
Tool approval has two independent inputs:
- Tool declaration — every tool may declare an
approvaltier:read: reads data or updates UI-only session metadata.write: mutates workspace/session state but does not execute arbitrary code.exec: executes code, shells out, drives a browser, spawns agents, or performs similarly broad actions.
- User policy —
tools.approval.<toolName>: allow | deny | promptoverrides the mode for that tool.
Tools without an approval declaration are treated as exec. This is the safe default for MCP and unknown custom tools.
Modes
Configure with tools.approvalMode:
| Mode | Auto-approves | Prompts for |
|---|---|---|
always-ask |
read |
write, exec |
write |
read, write |
exec |
yolo (default) |
read, write, exec |
nothing unless a tool declares override: true |
--auto-approve and --yolo force tools.approvalMode: yolo for the session. They do not bypass tool safety overrides.
User overrides
tools.approval is honored in every mode:
tools:
approvalMode: write
approval:
bash: prompt
read: allow
mcp__filesystem__delete: deny
Resolution per tool call:
- Compute the tool's approval decision from
tool.approval(args); omitted meansexec. - If the decision has
override: true:tools.approval.<tool>: denyblocks the call.- every other policy prompts, even in
yolo.
- Otherwise, a valid
tools.approval.<tool>value wins. - Otherwise, the active mode auto-approves or prompts by tier.
Invalid policy values are ignored and fall back to the tool tier/mode decision.
Safety overrides
A tool can force a prompt with object-form approval:
approval: { tier: "exec", override: true, reason: "Critical pattern detected" }
bash uses this for critical destructive patterns such as rm -rf /, fork bombs, remote-fetch-then-execute, writes to /etc/passwd, and host shutdown commands. These prompt even in yolo; in non-interactive/headless sessions they fail instead of running unattended.
Per-tool prompt details
Tools can add approval-prompt body lines with formatApprovalDetails(args). The standard prompt includes:
Allow tool: <name>Origin: MCP server toolfor unannotatedmcp__...toolsReason: <reason>when the tool decision supplies one- tool-specific details such as command, path, code, browser action, or subagent assignment
Defining approval on tools
Built-in and custom tools share the same shape:
export type ToolTier = "read" | "write" | "exec";
export type ToolApprovalDecision = ToolTier | { tier: ToolTier; reason?: string; override?: boolean };
export type ToolApproval = ToolApprovalDecision | ((args: unknown) => ToolApprovalDecision);
approval?: ToolApproval;
formatApprovalDetails?: (args: unknown) => string | string[] | undefined;
Examples:
approval: "read"
approval: args => LSP_READONLY_ACTIONS.has(args.action) ? "read" : "write"
approval: args => isCritical(args.command)
? { tier: "exec", override: true, reason: "Critical pattern detected" }
: "exec"
Subagents
Subagents run headless with tools.approvalMode: yolo so they do not stall waiting for UI. The parent task approval is the authorization boundary. Tool-level safety overrides still apply; a critical override inside a headless subagent fails rather than running without confirmation.