diff --git a/docs/approval-mode.md b/docs/approval-mode.md index 20b658ca4..910d4e955 100644 --- a/docs/approval-mode.md +++ b/docs/approval-mode.md @@ -14,13 +14,15 @@ Tools without an `approval` declaration are treated as `exec`. This is the safe Configure with `tools.approvalMode`: +## Modes + | 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` | +| `yolo` (default) | `read`, `write`, `exec` | none | -`--auto-approve` and `--yolo` force `tools.approvalMode: yolo` for the session. They do **not** bypass tool safety overrides. +`--auto-approve` and `--yolo` force `tools.approvalMode: yolo` for the session. ## User overrides @@ -38,11 +40,10 @@ tools: Resolution per tool call: 1. Compute the tool's approval decision from `tool.approval(args)`; omitted means `exec`. -2. If the decision has `override: true`: - - `tools.approval.: deny` blocks the call. - - every other policy prompts, even in `yolo`. -3. Otherwise, a valid `tools.approval.` value wins. -4. Otherwise, the active mode auto-approves or prompts by tier. +2. A user policy in `tools.approval.` is always applied. +3. In `yolo` mode, with no user policy, the call is auto-approved. +4. In non-yolo modes, if the tool sets `override: true`, `deny` is blocked and all other cases prompt. +5. Otherwise, the active mode auto-approves or prompts by tier. Invalid policy values are ignored and fall back to the tool tier/mode decision. @@ -54,7 +55,7 @@ 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. + `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 surface as `reason` in the approval prompt, but in `yolo` mode they are auto-approved unless a user policy for the tool is set to `prompt` or `deny`. ## Per-tool prompt details @@ -92,4 +93,4 @@ approval: args => isCritical(args.command) ## 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. +Subagents run headless with `tools.approvalMode: yolo` so they do not stall waiting for UI. The parent `task` approval is the authorization boundary. User `tools.approval.` settings continue to control whether a tool is allowed, prompted, or blocked. diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index d923ffe97..431d6d694 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -4,7 +4,8 @@ ### Changed -- Changed the default `task.simple` mode from `default` to `schema-free`, so task-call `schema` inputs are disabled by default while shared `context` and agent/session-defined output schemas remain available +- Changed the default `task.simple` mode from `default` to `schema-free`, so task-call `schema` inputs are disabled by default while shared `context` and user prompt/session-defined output schemas remain available +- Changed `tools.approvalMode: yolo` to auto-approve tool calls even when a tool marks `override: true`; user `tools.approval.` policies (`allow`/`prompt`/`deny`) now remain the only controls for yolo mode. ## [15.5.1] - 2026-05-26 diff --git a/packages/coding-agent/src/config/settings-schema.ts b/packages/coding-agent/src/config/settings-schema.ts index 7767956d3..49b928088 100644 --- a/packages/coding-agent/src/config/settings-schema.ts +++ b/packages/coding-agent/src/config/settings-schema.ts @@ -1805,7 +1805,7 @@ export const SETTINGS_SCHEMA = { // Default tool approval mode (interaction tab, but governs the tool wrapper). // "always-ask" — auto-approves read-tier tools only; prompts for write/exec. // "write" — auto-approves read and write-tier tools; prompts for exec. - // "yolo" — auto-approves every tier unless a tool declares `override: true`. + // "yolo" — auto-approves every tier. "tools.approvalMode": { type: "enum", values: ["always-ask", "write", "yolo"] as const, @@ -1814,7 +1814,7 @@ export const SETTINGS_SCHEMA = { tab: "interaction", label: "Tool Approval", description: - "Default approval behaviour for tool calls. 'Always ask' auto-approves read-only tools only. 'Write' auto-approves read and workspace-write tools. 'Yolo' auto-approves every tier unless a tool declares a safety override. `tools.approval.` overrides are honored in every mode.", + "Default approval behaviour for tool calls. 'Always ask' auto-approves read-only tools only. 'Write' auto-approves read and workspace-write tools. 'Yolo' auto-approves all tiers; user policy may still prompt or block.", options: [ { value: "always-ask", @@ -1831,7 +1831,7 @@ export const SETTINGS_SCHEMA = { value: "yolo", label: "Yolo", description: - "Auto-approve read, write, and exec tools. Safety overrides declared by tools (for example critical bash patterns) still require confirmation.", + "Auto-approve read, write, and exec tools. User policy can still require confirmation or block calls.", }, ], }, diff --git a/packages/coding-agent/src/extensibility/extensions/wrapper.ts b/packages/coding-agent/src/extensibility/extensions/wrapper.ts index 21ca94e0d..8d9f8374a 100644 --- a/packages/coding-agent/src/extensibility/extensions/wrapper.ts +++ b/packages/coding-agent/src/extensibility/extensions/wrapper.ts @@ -111,9 +111,8 @@ export class ExtensionToolWrapper` - // policies are honored in every mode. + // CLI `--auto-approve` / `--yolo` sets approval mode to yolo. + // User `tools.approval.` policies are still applied in all modes. const cliAutoApprove = context?.autoApprove === true; const settings: Settings | undefined = context?.settings; const configuredMode = (settings?.get("tools.approvalMode") ?? "yolo") as ApprovalMode; diff --git a/packages/coding-agent/src/task/executor.ts b/packages/coding-agent/src/task/executor.ts index 5a393692b..30fb9efe5 100644 --- a/packages/coding-agent/src/task/executor.ts +++ b/packages/coding-agent/src/task/executor.ts @@ -532,10 +532,10 @@ function createSubagentSettings(baseSettings: Settings): Settings { ...snapshot, "async.enabled": false, "bash.autoBackground.enabled": false, + // Subagents run headless — there is no UI to confirm prompts against, so // the parent task approval is the authorization boundary. Use yolo mode - // to preserve unattended subagent execution while still honoring any - // tool-level safety override that can be handled before execution. + // to preserve unattended subagent execution. User `tools.approval` policies still apply. "tools.approvalMode": "yolo", }); } diff --git a/packages/coding-agent/src/tools/approval.ts b/packages/coding-agent/src/tools/approval.ts index 992d577b5..90d56369b 100644 --- a/packages/coding-agent/src/tools/approval.ts +++ b/packages/coding-agent/src/tools/approval.ts @@ -87,8 +87,8 @@ function modeApprovesTier(mode: ApprovalMode, tier: ToolTier): boolean { * 2. User per-tool override, if set and valid. * 3. Active mode tier comparison. * - * Tool decisions with `override: true` force a prompt in every mode unless the - * user explicitly denies the tool; deny remains the strongest policy. + * In yolo mode, override-based tool prompts are ignored; user `tools.approval` + * settings remain authoritative. */ export function resolveApproval( tool: ApprovalSubject, @@ -99,6 +99,10 @@ export function resolveApproval( const decision = getToolDecision(tool, args); const userPolicy = Object.hasOwn(userConfig, tool.name) ? normalizePolicy(userConfig[tool.name]) : undefined; + if (mode === "yolo") { + return { policy: userPolicy ?? "allow", tier: decision.tier, override: false }; + } + if (decision.override) { if (userPolicy === "deny") { return { policy: "deny", tier: decision.tier, override: true }; diff --git a/packages/coding-agent/src/tools/bash.ts b/packages/coding-agent/src/tools/bash.ts index 5615a962a..c5babe7ca 100644 --- a/packages/coding-agent/src/tools/bash.ts +++ b/packages/coding-agent/src/tools/bash.ts @@ -42,11 +42,11 @@ const BASH_ENV_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/; const DEFAULT_AUTO_BACKGROUND_THRESHOLD_MS = 60_000; /** - * Bash patterns that force an approval prompt even in yolo mode. + * Bash patterns flagged as safety critical for approval policy. * - * Kept intentionally tight — the cost of a false positive is one extra prompt; - * the cost of a false negative is data loss or a compromised host. New patterns - * should target shapes that are virtually never legitimate in automation. + * Kept intentionally tight — the cost of a false negative is data loss or a compromised host, + * while false positives remain actionable through user policy control. + * New patterns should target shapes that are virtually never legitimate in automation. */ export const CRITICAL_BASH_PATTERNS = [ // Recursive destruction. diff --git a/packages/coding-agent/test/tools/approval-mode.test.ts b/packages/coding-agent/test/tools/approval-mode.test.ts index f56f8fd9f..98b3707c0 100644 --- a/packages/coding-agent/test/tools/approval-mode.test.ts +++ b/packages/coding-agent/test/tools/approval-mode.test.ts @@ -156,7 +156,7 @@ describe("tools.approvalMode setting", () => { } }); - it("critical bash patterns prompt even in yolo mode when bash is user-allowed", async () => { + it("critical bash patterns do not prompt in yolo mode with bash allowed", async () => { const { tempDir, session, settings } = await makeSession({ "tools.approvalMode": "yolo", "tools.approval": { bash: "allow" }, @@ -165,11 +165,17 @@ describe("tools.approvalMode setting", () => { try { const bash = session.getToolByName("bash"); if (!bash) throw new Error("Expected bash tool"); - await expect( - bash.execute("critical", { command: "rm -rf /" }, undefined, undefined, { + + const result = await bash.execute( + "critical", + { command: "rm -f /tmp/bun-fake-timer-probe.test.ts" }, + undefined, + undefined, + { settings, - } as AgentToolContext), - ).rejects.toThrow(/requires approval but no interactive UI available/); + } as AgentToolContext, + ); + expect(textOf(result)).toBe("(no output)"); } finally { await session.dispose(); } @@ -193,7 +199,7 @@ describe("tools.approvalMode setting", () => { } }); - it("CLI --auto-approve does not bypass tool safety overrides", async () => { + it("CLI --auto-approve also bypasses safety-override patterns", async () => { const { tempDir, session, settings } = await makeSession({ "tools.approvalMode": "always-ask", }); @@ -201,12 +207,17 @@ describe("tools.approvalMode setting", () => { try { const bash = session.getToolByName("bash"); if (!bash) throw new Error("Expected bash tool"); - await expect( - bash.execute("cli-critical", { command: "rm -rf /" }, undefined, undefined, { + const result = await bash.execute( + "cli-critical", + { command: "rm -f /tmp/bun-fake-timer-probe.test.ts" }, + undefined, + undefined, + { settings, autoApprove: true, - } as AgentToolContext), - ).rejects.toThrow(/requires approval but no interactive UI available/); + } as AgentToolContext, + ); + expect(textOf(result)).toBe("(no output)"); } finally { await session.dispose(); } diff --git a/packages/coding-agent/test/tools/approval.test.ts b/packages/coding-agent/test/tools/approval.test.ts index 28b08959f..04761a543 100644 --- a/packages/coding-agent/test/tools/approval.test.ts +++ b/packages/coding-agent/test/tools/approval.test.ts @@ -79,14 +79,15 @@ describe("resolveApproval tier matrix", () => { describe("resolveApproval override and user policy", () => { const dangerous = tool("bash", { tier: "exec", override: true, reason: "Critical pattern detected" }); - it("tool override prompts even in yolo mode", () => { + it("ignores override-based prompts in yolo mode", () => { const result = resolveApproval(dangerous, {}, "yolo"); - expect(result).toMatchObject({ policy: "prompt", tier: "exec", override: true }); - expect(result.reason).toBe("Critical pattern detected"); + expect(result).toMatchObject({ policy: "allow", tier: "exec", override: false }); + expect(result.reason).toBeUndefined(); }); - it("deny wins over a tool override", () => { - expect(resolveApproval(dangerous, {}, "yolo", { bash: "allow" }).policy).toBe("prompt"); + it("user policy still controls execution in yolo mode", () => { + expect(resolveApproval(dangerous, {}, "yolo", { bash: "allow" }).policy).toBe("allow"); + expect(resolveApproval(dangerous, {}, "yolo", { bash: "prompt" }).policy).toBe("prompt"); expect(resolveApproval(dangerous, {}, "yolo", { bash: "deny" }).policy).toBe("deny"); expect(() => requiresApproval(dangerous, {}, "yolo", { bash: "deny" })).toThrow( 'Tool "bash" is blocked by user policy',