From e3255e5615df121831f626300510c934518d2cca Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Sun, 22 Feb 2026 22:51:40 +0700 Subject: [PATCH] fix(persist): add auto-approve permission flags --- src/commands/persist-command.ts | 109 +++++++++++++++++++- tests/unit/commands/persist-command.test.js | 94 ++++++++++++++++- 2 files changed, 199 insertions(+), 4 deletions(-) diff --git a/src/commands/persist-command.ts b/src/commands/persist-command.ts index b02e41ed..aad2b6fb 100644 --- a/src/commands/persist-command.ts +++ b/src/commands/persist-command.ts @@ -29,6 +29,9 @@ interface PersistCommandArgs { yes?: boolean; listBackups?: boolean; restore?: string | boolean; + permissionMode?: PermissionMode; + dangerouslySkipPermissions?: boolean; + parseError?: string; } interface ResolvedEnv { @@ -37,6 +40,40 @@ interface ResolvedEnv { warning?: string; } +const PERSIST_KNOWN_FLAGS = [ + '--yes', + '-y', + '--list-backups', + '--restore', + '--permission-mode', + '--dangerously-skip-permissions', + '--auto-approve', + '--help', + '-h', +] as const; + +const VALID_PERMISSION_MODES = ['default', 'plan', 'acceptEdits', 'bypassPermissions'] as const; + +type PermissionMode = (typeof VALID_PERMISSION_MODES)[number]; + +function isPermissionMode(value: string): value is PermissionMode { + return VALID_PERMISSION_MODES.includes(value as PermissionMode); +} + +function resolvePermissionMode(parsedArgs: PersistCommandArgs): PermissionMode | undefined { + if (!parsedArgs.dangerouslySkipPermissions) { + return parsedArgs.permissionMode; + } + + if (parsedArgs.permissionMode && parsedArgs.permissionMode !== 'bypassPermissions') { + throw new Error( + '--dangerously-skip-permissions conflicts with --permission-mode. Use bypassPermissions or remove one flag.' + ); + } + + return 'bypassPermissions'; +} + /** Parse command line arguments */ function parseArgs(args: string[]): PersistCommandArgs { const result: PersistCommandArgs = { @@ -49,7 +86,27 @@ function parseArgs(args: string[]): PersistCommandArgs { result.restore = restoreOption.missingValue ? true : restoreOption.value || true; } - for (const arg of restoreOption.remainingArgs) { + const permissionModeOption = extractOption(restoreOption.remainingArgs, ['--permission-mode'], { + knownFlags: PERSIST_KNOWN_FLAGS, + }); + if (permissionModeOption.found) { + if (permissionModeOption.missingValue) { + result.parseError = 'Missing value for --permission-mode'; + } else if (permissionModeOption.value) { + if (!isPermissionMode(permissionModeOption.value)) { + result.parseError = `Invalid --permission-mode "${permissionModeOption.value}". Valid modes: ${VALID_PERMISSION_MODES.join(', ')}`; + } else { + result.permissionMode = permissionModeOption.value; + } + } + } + + result.dangerouslySkipPermissions = hasAnyFlag(permissionModeOption.remainingArgs, [ + '--dangerously-skip-permissions', + '--auto-approve', + ]); + + for (const arg of permissionModeOption.remainingArgs) { if (!arg.startsWith('-')) { result.profile = arg; break; @@ -415,6 +472,13 @@ async function showHelp(): Promise { console.log(''); console.log(subheader('Options')); console.log(` ${color('--yes, -y', 'command')} Skip confirmation prompts (auto-backup)`); + console.log( + ` ${color('--permission-mode ', 'command')} Set default permission mode in settings.json` + ); + console.log( + ` ${color('--dangerously-skip-permissions', 'command')} Persist auto-approve (bypassPermissions)` + ); + console.log(` ${color('--auto-approve', 'command')} Alias for --dangerously-skip-permissions`); console.log(` ${color('--help, -h', 'command')} Show this help message`); console.log(''); console.log(subheader('Backup Management')); @@ -437,6 +501,12 @@ async function showHelp(): Promise { console.log(` ${dim('# Persist with auto-confirmation')}`); console.log(` ${color('ccs persist gemini --yes', 'command')}`); console.log(''); + console.log(` ${dim('# Persist with default permission mode')}`); + console.log(` ${color('ccs persist glm --permission-mode acceptEdits', 'command')}`); + console.log(''); + console.log(` ${dim('# Persist with auto-approve enabled')}`); + console.log(` ${color('ccs persist codex --dangerously-skip-permissions', 'command')}`); + console.log(''); console.log(` ${dim('# List all backups')}`); console.log(` ${color('ccs persist --list-backups', 'command')}`); console.log(''); @@ -474,6 +544,17 @@ export async function handlePersistCommand(args: string[]): Promise { return; } await initUI(); + if (parsedArgs.parseError) { + console.log(fail(parsedArgs.parseError)); + process.exit(1); + } + let resolvedPermissionMode: PermissionMode | undefined; + try { + resolvedPermissionMode = resolvePermissionMode(parsedArgs); + } catch (error) { + console.log(fail((error as Error).message)); + process.exit(1); + } if (!parsedArgs.profile) { console.log(fail('Profile name is required')); console.log(''); @@ -529,6 +610,13 @@ export async function handlePersistCommand(args: string[]): Promise { console.log(` ${color(paddedKey, 'command')} = ${displayValue}`); } console.log(''); + if (resolvedPermissionMode) { + console.log(`Default permission mode: ${color(resolvedPermissionMode, 'command')}`); + if (resolvedPermissionMode === 'bypassPermissions') { + console.log(warn('Auto-approve enabled: Claude will skip permission prompts by default.')); + } + console.log(''); + } // Show warning if applicable if (resolved.warning) { console.log(warn(resolved.warning)); @@ -582,13 +670,30 @@ export async function handlePersistCommand(args: string[]): Promise { existingEnv = rawEnv as Record; } } - const mergedSettings = { + const mergedSettings: Record = { ...existingSettings, env: { ...existingEnv, ...resolved.env, }, }; + if (resolvedPermissionMode) { + const rawPermissions = existingSettings.permissions; + let existingPermissions: Record = {}; + if (rawPermissions !== undefined && rawPermissions !== null) { + if (typeof rawPermissions !== 'object' || Array.isArray(rawPermissions)) { + console.log( + warn('Existing permissions in settings.json is not an object - it will be replaced') + ); + } else { + existingPermissions = rawPermissions as Record; + } + } + mergedSettings.permissions = { + ...existingPermissions, + defaultMode: resolvedPermissionMode, + }; + } // Write merged settings try { writeClaudeSettings(mergedSettings); diff --git a/tests/unit/commands/persist-command.test.js b/tests/unit/commands/persist-command.test.js index 7282ac37..9674da5c 100644 --- a/tests/unit/commands/persist-command.test.js +++ b/tests/unit/commands/persist-command.test.js @@ -20,11 +20,15 @@ describe('Persist Command', () => { * Simulates the argument parsing logic from persist-command.ts */ function parseArgs(args) { + const validPermissionModes = ['default', 'plan', 'acceptEdits', 'bypassPermissions']; const result = { profile: undefined, yes: false, listBackups: false, restore: undefined, + permissionMode: undefined, + dangerouslySkipPermissions: false, + parseError: undefined, }; for (let i = 0; i < args.length; i++) { const arg = args[i]; @@ -43,6 +47,27 @@ describe('Persist Command', () => { } else { result.restore = true; // Use latest } + } else if (arg === '--permission-mode') { + const nextArg = args[i + 1]; + if (!nextArg || nextArg.startsWith('-')) { + result.parseError = 'Missing value for --permission-mode'; + } else if (!validPermissionModes.includes(nextArg)) { + result.parseError = `Invalid --permission-mode "${nextArg}". Valid modes: ${validPermissionModes.join(', ')}`; + } else { + result.permissionMode = nextArg; + i++; // Skip next arg + } + } else if (arg.startsWith('--permission-mode=')) { + const mode = arg.split('=').slice(1).join('='); + if (!mode.trim()) { + result.parseError = 'Missing value for --permission-mode'; + } else if (!validPermissionModes.includes(mode)) { + result.parseError = `Invalid --permission-mode "${mode}". Valid modes: ${validPermissionModes.join(', ')}`; + } else { + result.permissionMode = mode; + } + } else if (arg === '--dangerously-skip-permissions' || arg === '--auto-approve') { + result.dangerouslySkipPermissions = true; } else if (!arg.startsWith('-') && !result.profile) { result.profile = arg; } @@ -117,6 +142,34 @@ describe('Persist Command', () => { assert.strictEqual(result.restore, '20260110_205324'); assert.strictEqual(result.yes, true); }); + + it('parses --permission-mode with valid mode', () => { + const result = parseArgs(['glm', '--permission-mode', 'acceptEdits']); + assert.strictEqual(result.profile, 'glm'); + assert.strictEqual(result.permissionMode, 'acceptEdits'); + assert.strictEqual(result.parseError, undefined); + }); + + it('parses --permission-mode=value syntax', () => { + const result = parseArgs(['glm', '--permission-mode=bypassPermissions']); + assert.strictEqual(result.permissionMode, 'bypassPermissions'); + assert.strictEqual(result.parseError, undefined); + }); + + it('sets parseError for invalid --permission-mode', () => { + const result = parseArgs(['glm', '--permission-mode', 'invalid']); + assert.match(result.parseError, /Invalid --permission-mode/); + }); + + it('parses --dangerously-skip-permissions flag', () => { + const result = parseArgs(['glm', '--dangerously-skip-permissions']); + assert.strictEqual(result.dangerouslySkipPermissions, true); + }); + + it('parses --auto-approve as alias flag', () => { + const result = parseArgs(['glm', '--auto-approve']); + assert.strictEqual(result.dangerouslySkipPermissions, true); + }); }); // ========================================================================= @@ -170,15 +223,28 @@ describe('Persist Command', () => { /** * Simulates the merge logic from persist-command.ts */ - function mergeSettings(existing, newEnv) { + function mergeSettings(existing, newEnv, permissionMode) { const existingEnv = existing.env || {}; - return { + const merged = { ...existing, env: { ...existingEnv, ...newEnv, }, }; + + if (permissionMode) { + const existingPermissions = + existing.permissions && typeof existing.permissions === 'object' + ? existing.permissions + : {}; + merged.permissions = { + ...existingPermissions, + defaultMode: permissionMode, + }; + } + + return merged; } it('merges env vars into empty settings', () => { @@ -258,6 +324,30 @@ describe('Persist Command', () => { assert.strictEqual(result.env.ANTHROPIC_BASE_URL, 'http://test.com'); assert.strictEqual(result.env.ANTHROPIC_MODEL, 'test'); }); + + it('sets permissions.defaultMode when permission mode is provided', () => { + const existing = { + hooks: { PreToolUse: [] }, + }; + const result = mergeSettings(existing, { ANTHROPIC_MODEL: 'test' }, 'acceptEdits'); + + assert.strictEqual(result.permissions.defaultMode, 'acceptEdits'); + assert.deepStrictEqual(result.hooks, existing.hooks); + }); + + it('preserves existing permissions allow/deny when setting defaultMode', () => { + const existing = { + permissions: { + allow: ['Bash(ls:*)'], + deny: ['Bash(rm:*)'], + }, + }; + const result = mergeSettings(existing, { ANTHROPIC_MODEL: 'test' }, 'bypassPermissions'); + + assert.deepStrictEqual(result.permissions.allow, ['Bash(ls:*)']); + assert.deepStrictEqual(result.permissions.deny, ['Bash(rm:*)']); + assert.strictEqual(result.permissions.defaultMode, 'bypassPermissions'); + }); }); // =========================================================================