diff --git a/src/session-cli-builder.ts b/src/session-cli-builder.ts index 95f9f0da..546ce557 100644 --- a/src/session-cli-builder.ts +++ b/src/session-cli-builder.ts @@ -21,6 +21,8 @@ function buildPermissionArgs(claudeMode: ClaudeMode, allowedTools?: string): str switch (claudeMode) { case 'dangerously-skip-permissions': return ['--dangerously-skip-permissions']; + case 'auto': + return ['--permission-mode', 'auto']; case 'allowedTools': if (allowedTools) { return ['--allowedTools', allowedTools]; diff --git a/src/session.ts b/src/session.ts index 02ce3e1b..dd589f8d 100644 --- a/src/session.ts +++ b/src/session.ts @@ -1523,7 +1523,7 @@ export class Session extends EventEmitter { // === Auto-accept workspace trust dialog === // Claude CLI 2.x shows "Yes, I trust this folder" prompt on first launch per directory. - // Codeman sessions always use --dangerously-skip-permissions, so auto-accept. + // Codeman sessions run permission-skipping or classifier-guarded (auto) modes, so auto-accept. if (!this._trustDialogAccepted && data.includes('trust this folder')) { this._trustDialogAccepted = true; console.log(`[Session] Auto-accepting workspace trust dialog for: ${this.id}`); diff --git a/src/tmux-manager.ts b/src/tmux-manager.ts index 13200fe8..8f8bd982 100644 --- a/src/tmux-manager.ts +++ b/src/tmux-manager.ts @@ -563,6 +563,8 @@ function buildClaudePermissionFlags(claudeMode?: ClaudeMode, allowedTools?: stri switch (mode) { case 'dangerously-skip-permissions': return ' --dangerously-skip-permissions'; + case 'auto': + return ' --permission-mode auto'; case 'allowedTools': if (allowedTools) { // Sanitize: allow tool names with patterns like Bash(git:*), space/comma-separated @@ -674,7 +676,7 @@ function buildEffortSettingsFlag(effort?: EffortLevel): string { return flag && value ? ` ${flag} '${value}'` : ''; } -function buildSpawnCommand(options: { +export function buildSpawnCommand(options: { mode: SessionMode; sessionId: string; model?: string; diff --git a/src/types/session.ts b/src/types/session.ts index 0744beab..ad534587 100644 --- a/src/types/session.ts +++ b/src/types/session.ts @@ -35,10 +35,11 @@ export type SessionStatus = 'idle' | 'busy' | 'stopped' | 'error'; /** * Claude CLI startup permission mode. * - `'dangerously-skip-permissions'`: Bypass all permission prompts (default) + * - `'auto'`: Anthropic's classifier-guarded low-prompt mode (`--permission-mode auto`) * - `'normal'`: Standard mode with permission prompts * - `'allowedTools'`: Only allow specific tools (requires allowedTools list) */ -export type ClaudeMode = 'dangerously-skip-permissions' | 'normal' | 'allowedTools'; +export type ClaudeMode = 'dangerously-skip-permissions' | 'auto' | 'normal' | 'allowedTools'; /** Session mode: which CLI backend a session runs */ export type SessionMode = 'claude' | 'shell' | 'opencode' | 'codex' | 'gemini'; diff --git a/src/web/public/index.html b/src/web/public/index.html index a70f360e..c3a6a7be 100644 --- a/src/web/public/index.html +++ b/src/web/public/index.html @@ -1449,6 +1449,7 @@ diff --git a/src/web/server.ts b/src/web/server.ts index f3a2187b..649929eb 100644 --- a/src/web/server.ts +++ b/src/web/server.ts @@ -1514,7 +1514,12 @@ export class WebServer extends EventEmitter { const claudeMode = settings.claudeMode as string | undefined; const allowedTools = settings.allowedTools as string | undefined; // Only return valid modes - if (claudeMode === 'dangerously-skip-permissions' || claudeMode === 'normal' || claudeMode === 'allowedTools') { + if ( + claudeMode === 'dangerously-skip-permissions' || + claudeMode === 'auto' || + claudeMode === 'normal' || + claudeMode === 'allowedTools' + ) { return { claudeMode, allowedTools }; } return {}; diff --git a/test/claude-permission-mode.test.ts b/test/claude-permission-mode.test.ts new file mode 100644 index 00000000..a10a1caa --- /dev/null +++ b/test/claude-permission-mode.test.ts @@ -0,0 +1,83 @@ +/** + * @fileoverview Tests for Claude CLI startup permission modes, focused on the + * 'auto' mode (`--permission-mode auto`, Anthropic's recommended low-prompt mode) + * added alongside the default `--dangerously-skip-permissions`. + * + * Covers BOTH spawn paths, which build the permission flags independently: + * - session-cli-builder.buildInteractiveArgs (direct PTY, non-mux fallback) + * - tmux-manager.buildSpawnCommand (tmux pane command string) + * The default must stay 'dangerously-skip-permissions' when the setting is unset. + */ + +import { describe, it, expect } from 'vitest'; +import { buildInteractiveArgs } from '../src/session-cli-builder.js'; +import { buildSpawnCommand } from '../src/tmux-manager.js'; + +describe('buildInteractiveArgs permission modes (direct PTY path)', () => { + it('keeps --dangerously-skip-permissions as the skip-mode flag', () => { + const args = buildInteractiveArgs('sid-1', 'dangerously-skip-permissions'); + expect(args).toContain('--dangerously-skip-permissions'); + expect(args).not.toContain('--permission-mode'); + }); + + it('auto mode emits --permission-mode auto and never the skip flag', () => { + const args = buildInteractiveArgs('sid-1', 'auto'); + const idx = args.indexOf('--permission-mode'); + expect(idx).toBeGreaterThanOrEqual(0); + expect(args[idx + 1]).toBe('auto'); + expect(args).not.toContain('--dangerously-skip-permissions'); + }); + + it('normal mode emits no permission flag at all', () => { + const args = buildInteractiveArgs('sid-1', 'normal'); + expect(args).not.toContain('--dangerously-skip-permissions'); + expect(args).not.toContain('--permission-mode'); + }); + + it('allowedTools mode is unchanged by the auto addition', () => { + const args = buildInteractiveArgs('sid-1', 'allowedTools', undefined, 'Read,Grep'); + expect(args).toEqual(expect.arrayContaining(['--allowedTools', 'Read,Grep'])); + expect(args).not.toContain('--permission-mode'); + }); + + it('auto mode composes with model and effort flags', () => { + const args = buildInteractiveArgs('sid-1', 'auto', 'opus', undefined, 'high'); + expect(args).toEqual(expect.arrayContaining(['--permission-mode', 'auto', '--model', 'opus', '--effort', 'high'])); + }); +}); + +describe('buildSpawnCommand permission modes (tmux path)', () => { + it('unset claudeMode defaults to --dangerously-skip-permissions', () => { + const cmd = buildSpawnCommand({ mode: 'claude', sessionId: 'sid-1' }); + expect(cmd).toContain('claude --dangerously-skip-permissions --session-id "sid-1"'); + expect(cmd).not.toContain('--permission-mode'); + }); + + it('auto mode emits --permission-mode auto and never the skip flag', () => { + const cmd = buildSpawnCommand({ mode: 'claude', sessionId: 'sid-1', claudeMode: 'auto' }); + expect(cmd).toContain('claude --permission-mode auto --session-id "sid-1"'); + expect(cmd).not.toContain('--dangerously-skip-permissions'); + }); + + it('auto mode carries into BOTH legs of the resume fallback command', () => { + const cmd = buildSpawnCommand({ + mode: 'claude', + sessionId: 'sid-1', + claudeMode: 'auto', + resumeSessionId: 'abc-123', + }); + const [resumeLeg, fallbackLeg] = cmd.split('||'); + expect(resumeLeg).toContain('--permission-mode auto'); + expect(resumeLeg).toContain('--resume "abc-123"'); + expect(fallbackLeg).toContain('--permission-mode auto'); + expect(fallbackLeg).toContain('--session-id "sid-1"'); + expect(cmd).not.toContain('--dangerously-skip-permissions'); + }); + + it('normal mode emits no permission flag', () => { + const cmd = buildSpawnCommand({ mode: 'claude', sessionId: 'sid-1', claudeMode: 'normal' }); + expect(cmd).toContain('claude --session-id "sid-1"'); + expect(cmd).not.toContain('--permission-mode'); + expect(cmd).not.toContain('--dangerously-skip-permissions'); + }); +});