diff --git a/docs/users/features/sub-agents.md b/docs/users/features/sub-agents.md
index 28ce3e26736..10279bc3fbd 100644
--- a/docs/users/features/sub-agents.md
+++ b/docs/users/features/sub-agents.md
@@ -99,10 +99,10 @@ Subagents are configured using Markdown files with YAML frontmatter. This format
name: agent-name
description: Brief description of when and how to use this agent
model: inherit # Optional: inherit or model-id
-tools:
- - tool1
- - tool2
- - tool3 # Optional
+approvalMode: auto-edit # Optional: default, plan, auto-edit, yolo
+tools: # Optional: allowlist of tools
+ - tool1
+ - tool2
---
System prompt content goes here.
@@ -118,6 +118,38 @@ Use the optional `model` frontmatter field to control which model a subagent use
- `glm-5`: Use that model ID with the main conversation's auth type
- `openai:gpt-4o`: Use a different provider (resolves credentials from env vars)
+#### Permission Mode
+
+Use the optional `approvalMode` frontmatter field to control how a subagent's tool calls are approved. Valid values:
+
+- `default`: Tools require interactive approval (same as the main session default)
+- `plan`: Analyze-only mode — the agent plans but does not execute changes
+- `auto-edit`: Tools are auto-approved without prompting (recommended for most agents)
+- `yolo`: All tools auto-approved, including potentially destructive ones
+
+If you omit this field, the subagent's permission mode is determined automatically:
+
+- If the parent session is in **yolo** or **auto-edit** mode, the subagent inherits that mode. A permissive parent stays permissive.
+- If the parent session is in **plan** mode, the subagent stays in plan mode. An analyze-only session cannot mutate files through a delegated agent.
+- If the parent session is in **default** mode (in a trusted folder), the subagent gets **auto-edit** so it can work autonomously.
+
+When you do set `approvalMode`, the parent's permissive modes still take priority. For example, if the parent is in yolo mode, a subagent with `approvalMode: plan` will still run in yolo mode.
+
+```
+---
+name: cautious-reviewer
+description: Reviews code without making changes
+approvalMode: plan
+tools:
+ - read_file
+ - grep_search
+ - glob
+---
+
+You are a code reviewer. Analyze the code and report findings.
+Do not modify any files.
+```
+
#### Example Usage
```
@@ -501,6 +533,7 @@ Always follow these standards:
## Security Considerations
- **Tool Restrictions**: Subagents only have access to their configured tools
+- **Permission Mode**: Subagents inherit their parent's permission mode by default. Plan-mode sessions cannot escalate to auto-edit through delegated agents. Privileged modes (auto-edit, yolo) are blocked in untrusted folders.
- **Sandboxing**: All tool execution follows the same security model as direct tool use
- **Audit Trail**: All Subagents actions are logged and visible in real-time
- **Access Control**: Project and user-level separation provides appropriate boundaries
diff --git a/packages/cli/src/ui/components/InputPrompt.test.tsx b/packages/cli/src/ui/components/InputPrompt.test.tsx
index d12a27569d5..e26db52fe93 100644
--- a/packages/cli/src/ui/components/InputPrompt.test.tsx
+++ b/packages/cli/src/ui/components/InputPrompt.test.tsx
@@ -212,19 +212,6 @@ describe('InputPrompt', () => {
const wait = (ms = 50) => new Promise((resolve) => setTimeout(resolve, ms));
describe('prompt suggestions', () => {
- it('accepts the visible prompt suggestion on tab when the buffer is empty', async () => {
- const { stdin, unmount } = renderWithProviders(
- ,
- );
- await wait(350);
-
- stdin.write('\t');
- await wait();
-
- expect(mockBuffer.insert).toHaveBeenCalledWith('commit this');
- unmount();
- });
-
it('does not accept the prompt suggestion on shift+tab', async () => {
const { stdin, unmount } = renderWithProviders(
,
diff --git a/packages/core/src/core/permission-helpers.ts b/packages/core/src/core/permission-helpers.ts
index cafe985e593..6d537e7e6b1 100644
--- a/packages/core/src/core/permission-helpers.ts
+++ b/packages/core/src/core/permission-helpers.ts
@@ -182,9 +182,7 @@ export async function persistPermissionOutcome(
}
const scope =
- outcome === ToolConfirmationOutcome.ProceedAlwaysUser
- ? 'user'
- : 'project';
+ outcome === ToolConfirmationOutcome.ProceedAlwaysUser ? 'user' : 'project';
// Read permissionRules from the stored confirmation details first,
// falling back to payload for backward compatibility.
diff --git a/packages/core/src/extension/claude-converter.ts b/packages/core/src/extension/claude-converter.ts
index b8da31672f0..75d28be7a4c 100644
--- a/packages/core/src/extension/claude-converter.ts
+++ b/packages/core/src/extension/claude-converter.ts
@@ -187,10 +187,24 @@ export function convertClaudeAgentConfig(
qwenAgent['model'] = claudeAgent.model;
}
- // Preserve unsupported fields as-is for potential future compatibility
- // These fields are not supported by Qwen Code SubagentConfig but we keep them
+ // Map Claude permission mode aliases to Qwen ApprovalMode values.
+ // Note: Claude's `dontAsk` denies any tool call that would prompt the user,
+ // making it restrictive. We map it to `default` (which also requires approval)
+ // rather than `auto-edit` (which auto-approves), preserving the restrictive
+ // intent. `bypassPermissions` is the Claude mode that auto-approves everything.
if (claudeAgent.permissionMode) {
- qwenAgent['permissionMode'] = claudeAgent.permissionMode;
+ const claudeToQwenMode: Record = {
+ default: 'default',
+ plan: 'plan',
+ acceptEdits: 'auto-edit',
+ dontAsk: 'default',
+ bypassPermissions: 'yolo',
+ auto: 'auto-edit',
+ };
+ const mapped =
+ claudeToQwenMode[claudeAgent.permissionMode] ??
+ claudeAgent.permissionMode;
+ qwenAgent['approvalMode'] = mapped;
}
if (claudeAgent.hooks) {
qwenAgent['hooks'] = claudeAgent.hooks;
diff --git a/packages/core/src/subagents/subagent-manager.ts b/packages/core/src/subagents/subagent-manager.ts
index 2d4123619a0..7887070a7f5 100644
--- a/packages/core/src/subagents/subagent-manager.ts
+++ b/packages/core/src/subagents/subagent-manager.ts
@@ -34,6 +34,7 @@ import type {
AgentHooks,
} from '../agents/runtime/agent-events.js';
import type { Config } from '../config/config.js';
+import { APPROVAL_MODES } from '../config/config.js';
import {
type AuthType,
type ContentGenerator,
@@ -594,6 +595,13 @@ export class SubagentManager {
frontmatter['color'] = config.color;
}
+ if (
+ config.approvalMode &&
+ APPROVAL_MODES.includes(config.approvalMode as never)
+ ) {
+ frontmatter['approvalMode'] = config.approvalMode;
+ }
+
// Serialize to YAML
const yamlContent = stringifyYaml(frontmatter, {
lineWidth: 0, // Disable line wrapping
@@ -1024,6 +1032,28 @@ function parseSubagentContent(
| Record
| undefined;
const color = frontmatter['color'] as string | undefined;
+ const approvalModeRaw = frontmatter['approvalMode'];
+ if (
+ approvalModeRaw !== undefined &&
+ approvalModeRaw !== null &&
+ typeof approvalModeRaw !== 'string'
+ ) {
+ throw new Error(
+ `Invalid "approvalMode" value: expected a string, got ${typeof approvalModeRaw}. Valid values: ${APPROVAL_MODES.join(', ')}`,
+ );
+ }
+ const approvalMode =
+ typeof approvalModeRaw === 'string' && approvalModeRaw !== ''
+ ? approvalModeRaw
+ : undefined;
+ if (
+ approvalMode !== undefined &&
+ !APPROVAL_MODES.includes(approvalMode as never)
+ ) {
+ throw new Error(
+ `Invalid "approvalMode" value "${approvalMode}". Valid values: ${APPROVAL_MODES.join(', ')}`,
+ );
+ }
const model =
modelRaw != null && modelRaw !== ''
? String(modelRaw)
@@ -1035,6 +1065,7 @@ function parseSubagentContent(
name,
description,
tools,
+ approvalMode,
systemPrompt: systemPrompt.trim(),
filePath,
model,
diff --git a/packages/core/src/subagents/types.ts b/packages/core/src/subagents/types.ts
index 3e67869a58f..f9d0b455388 100644
--- a/packages/core/src/subagents/types.ts
+++ b/packages/core/src/subagents/types.ts
@@ -51,6 +51,15 @@ export interface SubagentConfig {
*/
tools?: string[];
+ /**
+ * Optional permission mode for this subagent.
+ * Controls how tool calls are approved during execution.
+ * Valid values: 'default', 'plan', 'auto-edit', 'yolo'.
+ * If omitted, the resolved mode depends on the parent's mode
+ * (permissive parent modes win; otherwise defaults to 'auto-edit').
+ */
+ approvalMode?: string;
+
/**
* System prompt content that defines the subagent's behavior.
* Supports ${variable} templating via ContextState.
diff --git a/packages/core/src/tools/agent.test.ts b/packages/core/src/tools/agent.test.ts
index 505b434a041..bb6fd049195 100644
--- a/packages/core/src/tools/agent.test.ts
+++ b/packages/core/src/tools/agent.test.ts
@@ -5,11 +5,15 @@
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
-import { AgentTool, type AgentParams } from './agent.js';
+import {
+ AgentTool,
+ type AgentParams,
+ resolveSubagentApprovalMode,
+} from './agent.js';
import type { PartListUnion } from '@google/genai';
import type { ToolResultDisplay, AgentResultDisplay } from './tools.js';
import { ToolConfirmationOutcome } from './tools.js';
-import type { Config } from '../config/config.js';
+import { type Config, ApprovalMode } from '../config/config.js';
import { SubagentManager } from '../subagents/subagent-manager.js';
import type { SubagentConfig } from '../subagents/types.js';
import { AgentTerminateMode } from '../agents/runtime/agent-types.js';
@@ -87,6 +91,8 @@ describe('AgentTool', () => {
getGeminiClient: vi.fn().mockReturnValue(undefined),
getHookSystem: vi.fn().mockReturnValue(undefined),
getTranscriptPath: vi.fn().mockReturnValue('/test/transcript'),
+ getApprovalMode: vi.fn().mockReturnValue('default'),
+ isTrustedFolder: vi.fn().mockReturnValue(true),
} as unknown as Config;
changeListeners = [];
@@ -392,7 +398,7 @@ describe('AgentTool', () => {
);
expect(mockSubagentManager.createAgentHeadless).toHaveBeenCalledWith(
mockSubagents[0],
- config,
+ expect.any(Object), // config (may be approval-mode override)
expect.any(Object), // eventEmitter parameter
);
expect(mockAgent.execute).toHaveBeenCalledWith(
@@ -627,7 +633,7 @@ describe('AgentTool', () => {
expect(mockHookSystem.fireSubagentStartEvent).toHaveBeenCalledWith(
expect.stringContaining('file-search-'),
'file-search',
- PermissionMode.Default,
+ PermissionMode.AutoEdit,
undefined,
);
});
@@ -809,7 +815,7 @@ describe('AgentTool', () => {
'/test/transcript',
'Task completed successfully',
false,
- PermissionMode.Default,
+ PermissionMode.AutoEdit,
undefined,
);
});
@@ -854,7 +860,7 @@ describe('AgentTool', () => {
'/test/transcript',
'Task completed successfully',
true,
- PermissionMode.Default,
+ PermissionMode.AutoEdit,
undefined,
);
});
@@ -1304,3 +1310,80 @@ describe('AgentTool', () => {
});
});
});
+
+describe('resolveSubagentApprovalMode', () => {
+ it('should return yolo when parent is yolo, regardless of agent config', () => {
+ expect(resolveSubagentApprovalMode(ApprovalMode.YOLO, 'plan', true)).toBe(
+ PermissionMode.Yolo,
+ );
+ expect(
+ resolveSubagentApprovalMode(ApprovalMode.YOLO, undefined, false),
+ ).toBe(PermissionMode.Yolo);
+ });
+
+ it('should return auto-edit when parent is auto-edit, regardless of agent config', () => {
+ expect(
+ resolveSubagentApprovalMode(ApprovalMode.AUTO_EDIT, 'plan', true),
+ ).toBe(PermissionMode.AutoEdit);
+ expect(
+ resolveSubagentApprovalMode(ApprovalMode.AUTO_EDIT, 'default', false),
+ ).toBe(PermissionMode.AutoEdit);
+ });
+
+ it('should respect agent-declared mode when parent is default and folder is trusted', () => {
+ expect(
+ resolveSubagentApprovalMode(ApprovalMode.DEFAULT, 'plan', true),
+ ).toBe(PermissionMode.Plan);
+ expect(
+ resolveSubagentApprovalMode(ApprovalMode.DEFAULT, 'auto-edit', true),
+ ).toBe(PermissionMode.AutoEdit);
+ expect(
+ resolveSubagentApprovalMode(ApprovalMode.DEFAULT, 'yolo', true),
+ ).toBe(PermissionMode.Yolo);
+ });
+
+ it('should block privileged agent-declared modes in untrusted folders', () => {
+ expect(
+ resolveSubagentApprovalMode(ApprovalMode.DEFAULT, 'auto-edit', false),
+ ).toBe(PermissionMode.Default);
+ expect(
+ resolveSubagentApprovalMode(ApprovalMode.DEFAULT, 'yolo', false),
+ ).toBe(PermissionMode.Default);
+ });
+
+ it('should allow non-privileged agent-declared modes in untrusted folders', () => {
+ expect(
+ resolveSubagentApprovalMode(ApprovalMode.DEFAULT, 'plan', false),
+ ).toBe(PermissionMode.Plan);
+ expect(
+ resolveSubagentApprovalMode(ApprovalMode.DEFAULT, 'default', false),
+ ).toBe(PermissionMode.Default);
+ });
+
+ it('should default to plan when parent is plan and no agent config', () => {
+ expect(
+ resolveSubagentApprovalMode(ApprovalMode.PLAN, undefined, true),
+ ).toBe(PermissionMode.Plan);
+ expect(
+ resolveSubagentApprovalMode(ApprovalMode.PLAN, undefined, false),
+ ).toBe(PermissionMode.Plan);
+ });
+
+ it('should allow agent-declared mode to override plan parent', () => {
+ expect(
+ resolveSubagentApprovalMode(ApprovalMode.PLAN, 'auto-edit', true),
+ ).toBe(PermissionMode.AutoEdit);
+ });
+
+ it('should default to auto-edit when parent is default and folder is trusted', () => {
+ expect(
+ resolveSubagentApprovalMode(ApprovalMode.DEFAULT, undefined, true),
+ ).toBe(PermissionMode.AutoEdit);
+ });
+
+ it('should default to parent mode when parent is default and folder is untrusted', () => {
+ expect(
+ resolveSubagentApprovalMode(ApprovalMode.DEFAULT, undefined, false),
+ ).toBe(PermissionMode.Default);
+ });
+});
diff --git a/packages/core/src/tools/agent.ts b/packages/core/src/tools/agent.ts
index 96be07d2037..7c7fb1fe645 100644
--- a/packages/core/src/tools/agent.ts
+++ b/packages/core/src/tools/agent.ts
@@ -36,6 +36,7 @@ import { BuiltinAgentRegistry } from '../subagents/builtin-agents.js';
import { createDebugLogger } from '../utils/debugLogger.js';
import { PermissionMode } from '../hooks/types.js';
import type { StopHookOutput } from '../hooks/types.js';
+import { ApprovalMode } from '../config/config.js';
export interface AgentParams {
description: string;
@@ -45,6 +46,98 @@ export interface AgentParams {
const debugLogger = createDebugLogger('AGENT');
+/**
+ * Maps ApprovalMode to PermissionMode for hook events.
+ */
+function approvalModeToPermissionMode(mode: ApprovalMode): PermissionMode {
+ switch (mode) {
+ case ApprovalMode.YOLO:
+ return PermissionMode.Yolo;
+ case ApprovalMode.AUTO_EDIT:
+ return PermissionMode.AutoEdit;
+ case ApprovalMode.PLAN:
+ return PermissionMode.Plan;
+ case ApprovalMode.DEFAULT:
+ default:
+ return PermissionMode.Default;
+ }
+}
+
+/**
+ * Resolves the effective permission mode for a sub-agent.
+ *
+ * Rules (matching claw-code):
+ * - Permissive parent modes (yolo, auto-edit) always win
+ * - Otherwise, the agent definition's mode applies if set
+ * - Default fallback is auto-edit (sub-agents need autonomy)
+ */
+export function resolveSubagentApprovalMode(
+ parentApprovalMode: ApprovalMode,
+ agentApprovalMode?: string,
+ isTrustedFolder?: boolean,
+): PermissionMode {
+ // Permissive parent modes always win
+ if (
+ parentApprovalMode === ApprovalMode.YOLO ||
+ parentApprovalMode === ApprovalMode.AUTO_EDIT
+ ) {
+ return approvalModeToPermissionMode(parentApprovalMode);
+ }
+
+ // Agent definition's mode applies if set
+ if (agentApprovalMode) {
+ const resolved = approvalModeToPermissionMode(
+ agentApprovalMode as ApprovalMode,
+ );
+ // Privileged modes require trusted folder
+ if (
+ !isTrustedFolder &&
+ (resolved === PermissionMode.Yolo || resolved === PermissionMode.AutoEdit)
+ ) {
+ return approvalModeToPermissionMode(parentApprovalMode);
+ }
+ return resolved;
+ }
+
+ // Default: match parent mode. In plan mode, stay in plan.
+ // In default mode in trusted folders, auto-edit for autonomy.
+ if (parentApprovalMode === ApprovalMode.PLAN) {
+ return PermissionMode.Plan;
+ }
+ if (isTrustedFolder) {
+ return PermissionMode.AutoEdit;
+ }
+ return approvalModeToPermissionMode(parentApprovalMode);
+}
+
+/**
+ * Maps PermissionMode back to ApprovalMode.
+ */
+function permissionModeToApprovalMode(mode: PermissionMode): ApprovalMode {
+ switch (mode) {
+ case PermissionMode.Yolo:
+ return ApprovalMode.YOLO;
+ case PermissionMode.AutoEdit:
+ return ApprovalMode.AUTO_EDIT;
+ case PermissionMode.Plan:
+ return ApprovalMode.PLAN;
+ case PermissionMode.Default:
+ default:
+ return ApprovalMode.DEFAULT;
+ }
+}
+
+/**
+ * Creates a Config override with a different approval mode.
+ * Uses prototype delegation to avoid mutating the parent config.
+ */
+function createApprovalModeOverride(base: Config, mode: ApprovalMode): Config {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const override = Object.create(base) as any;
+ override.getApprovalMode = (): ApprovalMode => mode;
+ return override as Config;
+}
+
/**
* Agent tool that enables primary agents to delegate tasks to specialized agents.
* The tool dynamically loads available agents and includes them in its description
@@ -521,9 +614,24 @@ class AgentToolInvocation extends BaseToolInvocation {
if (updateOutput) {
updateOutput(this.currentDisplay);
}
+ // Resolve the subagent's permission mode before creating it
+ const resolvedMode = resolveSubagentApprovalMode(
+ this.config.getApprovalMode(),
+ subagentConfig.approvalMode,
+ this.config.isTrustedFolder(),
+ );
+
+ // Create a config override with the resolved approval mode so the
+ // subagent's tool scheduler uses the correct mode for permission checks.
+ const resolvedApprovalMode = permissionModeToApprovalMode(resolvedMode);
+ const agentConfig =
+ resolvedApprovalMode !== this.config.getApprovalMode()
+ ? createApprovalModeOverride(this.config, resolvedApprovalMode)
+ : this.config;
+
const subagent = await this.subagentManager.createAgentHeadless(
subagentConfig,
- this.config,
+ agentConfig,
{ eventEmitter: this.eventEmitter },
);
@@ -541,7 +649,7 @@ class AgentToolInvocation extends BaseToolInvocation {
const startHookOutput = await hookSystem.fireSubagentStartEvent(
agentId,
agentType,
- PermissionMode.Default,
+ resolvedMode,
signal,
);
@@ -589,7 +697,7 @@ class AgentToolInvocation extends BaseToolInvocation {
transcriptPath,
subagent.getFinalText(),
stopHookActive,
- PermissionMode.Default,
+ resolvedMode,
signal,
);