fix(persist): add auto-approve permission flags

This commit is contained in:
Tam Nhu Tran
2026-02-22 22:51:40 +07:00
parent dcdb2f6284
commit e3255e5615
2 changed files with 199 additions and 4 deletions
+107 -2
View File
@@ -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<void> {
console.log('');
console.log(subheader('Options'));
console.log(` ${color('--yes, -y', 'command')} Skip confirmation prompts (auto-backup)`);
console.log(
` ${color('--permission-mode <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<void> {
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<void> {
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<void> {
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<void> {
existingEnv = rawEnv as Record<string, string>;
}
}
const mergedSettings = {
const mergedSettings: Record<string, unknown> = {
...existingSettings,
env: {
...existingEnv,
...resolved.env,
},
};
if (resolvedPermissionMode) {
const rawPermissions = existingSettings.permissions;
let existingPermissions: Record<string, unknown> = {};
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<string, unknown>;
}
}
mergedSettings.permissions = {
...existingPermissions,
defaultMode: resolvedPermissionMode,
};
}
// Write merged settings
try {
writeClaudeSettings(mergedSettings);
+92 -2
View File
@@ -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');
});
});
// =========================================================================