From 91070f5dda91ba9ae13701ed8d5422c760028fd7 Mon Sep 17 00:00:00 2001 From: Codeman maintainer Date: Mon, 20 Jul 2026 02:48:14 +0200 Subject: [PATCH] feat(claude): add 'auto' startup permission mode Adds Anthropic's classifier-guarded low-prompt mode (--permission-mode auto) as a fourth ClaudeMode alongside skip-permissions/normal/allowedTools. Wired through both spawn paths (buildPermissionArgs for direct PTY, buildClaudePermissionFlags for tmux), the getClaudeModeConfig validator, and the App Settings Startup Mode picker. Exports buildSpawnCommand for test coverage. This is the prerequisite for multi-user mode section 6.3, which downgrades non-granted users' sessions to 'auto'. Co-Authored-By: Claude Fable 5 --- src/session-cli-builder.ts | 2 + src/session.ts | 2 +- src/tmux-manager.ts | 4 +- src/types/session.ts | 3 +- src/web/public/index.html | 1 + src/web/server.ts | 7 ++- test/claude-permission-mode.test.ts | 83 +++++++++++++++++++++++++++++ 7 files changed, 98 insertions(+), 4 deletions(-) create mode 100644 test/claude-permission-mode.test.ts 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'); + }); +});