From aaefa4ed2bb787271fc7b6e6f553e72f9074531b Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Mon, 12 Oct 2026 15:49:07 -0700 Subject: [PATCH 1/2] fix(native-chat): seed the chat permission default from the Yolo/Manual toggle as it shows The one-time copy into Settings -> Chat -> Permissions gave Full access only when every agent's arguments were exactly the bypass flag, so a profile the toggle shows as Yolo (agents set differently, or an extra flag) got Ask. Follow the toggle: Manual seeds Ask, anything it shows as Yolo seeds Full access. --- src/shared/agent-chat-permission-mode.test.ts | 12 ++++++++---- src/shared/agent-chat-permission-mode.ts | 3 ++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/shared/agent-chat-permission-mode.test.ts b/src/shared/agent-chat-permission-mode.test.ts index be949d7a5305..2fa2372b6598 100644 --- a/src/shared/agent-chat-permission-mode.test.ts +++ b/src/shared/agent-chat-permission-mode.test.ts @@ -88,10 +88,14 @@ describe('the new-chat setting as loaded', () => { ) }) - it('seeds mixed Agent Permissions as Ask', () => { - const terminal = applyAgentPermissionMode({ mode: 'yolo' }) - terminal.agentDefaultArgs.claude = '' - expect(loadedChatPermissionSetting(undefined, terminal)).toBe('ask') + // The Yolo/Manual toggle shows Yolo for mixed, so the chat default follows what it shows. + it('seeds mixed Agent Permissions as Full access', () => { + const oneManual = applyAgentPermissionMode({ mode: 'yolo' }) + oneManual.agentDefaultArgs.claude = '' + expect(loadedChatPermissionSetting(undefined, oneManual)).toBe('full-access') + const extraFlag = applyAgentPermissionMode({ mode: 'yolo' }) + extraFlag.agentDefaultArgs.claude = '--dangerously-skip-permissions --model opus' + expect(loadedChatPermissionSetting(undefined, extraFlag)).toBe('full-access') }) it('seeds a fresh profile from the shipped Yolo defaults', () => { diff --git a/src/shared/agent-chat-permission-mode.ts b/src/shared/agent-chat-permission-mode.ts index 478b3019c886..702595eba48f 100644 --- a/src/shared/agent-chat-permission-mode.ts +++ b/src/shared/agent-chat-permission-mode.ts @@ -112,5 +112,6 @@ export function loadedChatPermissionSetting( if (saved !== undefined) { return isAgentChatPermissionMode(saved) ? saved : 'ask' } - return resolveAgentPermissionModeSummary(terminal) === 'yolo' ? 'full-access' : 'ask' + // As the Yolo/Manual toggle shows it: agents set differently ("mixed") still show Yolo. + return resolveAgentPermissionModeSummary(terminal) === 'manual' ? 'ask' : 'full-access' } From 726f8701b7e57986cd174c6a7fff37f0d518a0a2 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Mon, 12 Oct 2026 16:09:14 -0700 Subject: [PATCH 2/2] fix(native-chat): seed the chat permission default from Claude's and Codex's own arguments The Yolo/Manual toggle has no saved value: it summarises every agent's arguments and shows Yolo for any mix, including after Manual when another agent keeps custom arguments. Seeding from it would grant Full access to people who chose Manual. Read what Claude and Codex chats checked before they had modes instead: Full access when both launch with their bypass flag, otherwise Ask. --- .../normalize-loaded-global-settings.ts | 6 +- src/shared/agent-chat-permission-mode.test.ts | 65 +++++++++++++------ src/shared/agent-chat-permission-mode.ts | 14 ++-- src/shared/default-global-settings.ts | 7 +- 4 files changed, 62 insertions(+), 30 deletions(-) diff --git a/src/main/persistence/loading-store/normalize-loaded-global-settings.ts b/src/main/persistence/loading-store/normalize-loaded-global-settings.ts index f2bb14f81771..526788f078fa 100644 --- a/src/main/persistence/loading-store/normalize-loaded-global-settings.ts +++ b/src/main/persistence/loading-store/normalize-loaded-global-settings.ts @@ -137,7 +137,11 @@ export function normalizeLoadedGlobalSettings( // Why: the next settings save stores the seed; until then a reload derives the same one. nativeChatPermissionMode: loadedChatPermissionSetting( parsed.settings?.nativeChatPermissionMode, - migratedAgentYoloDefaults + { + ...migratedAgentYoloDefaults, + terminalWindowsShell: parsed.settings?.terminalWindowsShell + }, + process.platform ), defaultTaskSource: taskProviderSettings.defaultTaskSource, visibleTaskProviders: taskProviderSettings.visibleTaskProviders, diff --git a/src/shared/agent-chat-permission-mode.test.ts b/src/shared/agent-chat-permission-mode.test.ts index 2fa2372b6598..6f0fb29f268f 100644 --- a/src/shared/agent-chat-permission-mode.test.ts +++ b/src/shared/agent-chat-permission-mode.test.ts @@ -79,44 +79,71 @@ describe('the mode a chat runs', () => { }) describe('the new-chat setting as loaded', () => { + const seed = (terminal: Parameters[1]) => + loadedChatPermissionSetting(undefined, terminal, 'darwin') + const yoloWithArgs = (args: { claude?: string; codex?: string; grok?: string }) => { + const terminal = applyAgentPermissionMode({ mode: 'yolo' }) + Object.assign(terminal.agentDefaultArgs, args) + return terminal + } + it.each([ ['yolo', 'full-access'], ['manual', 'ask'] ] as const)('seeds %s Agent Permissions as %s', (mode, expected) => { - expect(loadedChatPermissionSetting(undefined, applyAgentPermissionMode({ mode }))).toBe( - expected - ) + expect(seed(applyAgentPermissionMode({ mode }))).toBe(expected) + }) + + it('seeds Full access when Claude and Codex carry their bypass flags among other arguments', () => { + expect( + seed( + yoloWithArgs({ + claude: '--dangerously-skip-permissions --model opus', + codex: '--model gpt-5 --dangerously-bypass-approvals-and-sandbox' + }) + ) + ).toBe('full-access') }) - // The Yolo/Manual toggle shows Yolo for mixed, so the chat default follows what it shows. - it('seeds mixed Agent Permissions as Full access', () => { - const oneManual = applyAgentPermissionMode({ mode: 'yolo' }) - oneManual.agentDefaultArgs.claude = '' - expect(loadedChatPermissionSetting(undefined, oneManual)).toBe('full-access') - const extraFlag = applyAgentPermissionMode({ mode: 'yolo' }) - extraFlag.agentDefaultArgs.claude = '--dangerously-skip-permissions --model opus' - expect(loadedChatPermissionSetting(undefined, extraFlag)).toBe('full-access') + it('reads only Claude and Codex, not the other agents', () => { + expect(seed(yoloWithArgs({ grok: '' }))).toBe('full-access') + const othersManual = applyAgentPermissionMode({ mode: 'manual' }) + Object.assign(othersManual.agentDefaultArgs, { + claude: '--dangerously-skip-permissions', + codex: '--dangerously-bypass-approvals-and-sandbox' + }) + expect(seed(othersManual)).toBe('full-access') + }) + + it('seeds Ask when Claude or Codex launches without its bypass flag', () => { + expect(seed(yoloWithArgs({ claude: '' }))).toBe('ask') + expect(seed(yoloWithArgs({ codex: '--model gpt-5' }))).toBe('ask') + }) + + // Manual leaves custom arguments alone, and the all-agent summary still shows Yolo for them. + it('seeds Ask after Manual even when another agent keeps custom arguments', () => { + const custom = yoloWithArgs({ codex: '--model gpt-5', grok: '--model grok-4' }) + expect(seed(applyAgentPermissionMode({ mode: 'manual', ...custom }))).toBe('ask') }) it('seeds a fresh profile from the shipped Yolo defaults', () => { - expect(loadedChatPermissionSetting(undefined, getDefaultSettings('/tmp'))).toBe('full-access') + expect(seed(getDefaultSettings('/tmp'))).toBe('full-access') expect(getDefaultSettings('/tmp').nativeChatPermissionMode).toBe('full-access') }) it.each(AGENT_CHAT_PERMISSION_MODES)( 'keeps a saved %s whatever Agent Permissions say', (mode) => { - expect(loadedChatPermissionSetting(mode, applyAgentPermissionMode({ mode: 'manual' }))).toBe( - mode - ) - expect(loadedChatPermissionSetting(mode, applyAgentPermissionMode({ mode: 'yolo' }))).toBe( - mode - ) + for (const terminal of ['manual', 'yolo'] as const) { + expect( + loadedChatPermissionSetting(mode, applyAgentPermissionMode({ mode: terminal }), 'darwin') + ).toBe(mode) + } } ) it.each(['future', null, 42, 'bypass'])('reads an unknown saved %s as Ask', (value) => { - expect(loadedChatPermissionSetting(value, getDefaultSettings('/tmp'))).toBe('ask') + expect(loadedChatPermissionSetting(value, getDefaultSettings('/tmp'), 'darwin')).toBe('ask') }) // The catalog shape `claude` 2.1.280 lists: the default row is folded into the row it resolves to. diff --git a/src/shared/agent-chat-permission-mode.ts b/src/shared/agent-chat-permission-mode.ts index 702595eba48f..fcf63a8b5e42 100644 --- a/src/shared/agent-chat-permission-mode.ts +++ b/src/shared/agent-chat-permission-mode.ts @@ -1,5 +1,5 @@ import type { GlobalSettings } from './global-settings-types' -import { resolveAgentPermissionModeSummary } from './tui-agent-permissions' +import { resolvedTuiAgentArgsBypassPermissions } from './tui-agent-launch-defaults' /** A chat's own permission choice. Saved in chat records and settings: never rename a value. */ export type AgentChatPermissionMode = 'ask' | 'accept-edits' | 'auto' | 'full-access' @@ -104,14 +104,18 @@ export function newChatPermissionMode( return support ? savedChatPermissionMode(support, setting) : null } -/** Seeded once from Agent Permissions when absent, then stored; an unknown value asks. */ +/** Seeded once when absent, then stored; an unknown value asks. */ export function loadedChatPermissionSetting( saved: unknown, - terminal: Partial> + terminal: Partial>, + platform: NodeJS.Platform ): AgentChatPermissionMode { if (saved !== undefined) { return isAgentChatPermissionMode(saved) ? saved : 'ask' } - // As the Yolo/Manual toggle shows it: agents set differently ("mixed") still show Yolo. - return resolveAgentPermissionModeSummary(terminal) === 'manual' ? 'ask' : 'full-access' + // What chats ran before they had modes: Claude's and Codex's own arguments, not the all-agent + // Yolo/Manual summary, which shows custom arguments on any agent as Yolo even after Manual. + const bypasses = (agent: 'claude' | 'codex'): boolean => + resolvedTuiAgentArgsBypassPermissions(agent, terminal, platform) + return bypasses('claude') && bypasses('codex') ? 'full-access' : 'ask' } diff --git a/src/shared/default-global-settings.ts b/src/shared/default-global-settings.ts index 178ca2e19d5e..e644e997cc04 100644 --- a/src/shared/default-global-settings.ts +++ b/src/shared/default-global-settings.ts @@ -9,7 +9,6 @@ import { DEFAULT_APP_ICON_ID } from './app-icon' import { DEFAULT_OPEN_IN_APPLICATIONS } from './open-in-applications' import { DEFAULT_DISABLED_TUI_AGENTS } from './tui-agent-selection' import { DEFAULT_TUI_AGENT_ARGS, DEFAULT_TUI_AGENT_ENV } from './tui-agent-launch-defaults' -import { loadedChatPermissionSetting } from './agent-chat-permission-mode' import { UI_LANGUAGE_SYSTEM } from './ui-language' import { DEFAULT_LEFT_SIDEBAR_TINT_COLOR, @@ -140,10 +139,8 @@ export function buildDefaultSettings(args: { nativeChatInheritShellEnvironment: true, nativeChatShellEnvironmentVariables: [], nativeChatSessionOptions: {}, - nativeChatPermissionMode: loadedChatPermissionSetting(undefined, { - agentDefaultArgs: DEFAULT_TUI_AGENT_ARGS, - agentDefaultEnv: DEFAULT_TUI_AGENT_ENV - }), + // The shipped Claude and Codex arguments carry their bypass flags. + nativeChatPermissionMode: 'full-access', openInApplications: [...DEFAULT_OPEN_IN_APPLICATIONS], rightSidebarOpenByDefault: true, showGitIgnoredFiles: true,