fix(native-chat): seed the chat permission default from Claude's and Codex's own arguments (#28100)

* 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.

* 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.
This commit is contained in:
Brennan Benson
2026-10-12 23:47:37 -07:00
committed by GitHub
parent 52e88e4469
commit f0ccfcc0d3
4 changed files with 62 additions and 25 deletions
@@ -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,
+46 -15
View File
@@ -79,40 +79,71 @@ describe('the mode a chat runs', () => {
})
describe('the new-chat setting as loaded', () => {
const seed = (terminal: Parameters<typeof loadedChatPermissionSetting>[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 mixed Agent Permissions as Ask', () => {
const terminal = applyAgentPermissionMode({ mode: 'yolo' })
terminal.agentDefaultArgs.claude = ''
expect(loadedChatPermissionSetting(undefined, terminal)).toBe('ask')
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')
})
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.
+9 -4
View File
@@ -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,13 +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<Pick<GlobalSettings, 'agentDefaultArgs' | 'agentDefaultEnv'>>
terminal: Partial<Pick<GlobalSettings, 'agentDefaultArgs' | 'terminalWindowsShell'>>,
platform: NodeJS.Platform
): AgentChatPermissionMode {
if (saved !== undefined) {
return isAgentChatPermissionMode(saved) ? saved : 'ask'
}
return resolveAgentPermissionModeSummary(terminal) === 'yolo' ? 'full-access' : 'ask'
// 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'
}
+2 -5
View File
@@ -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,