feat: support Claude Code 'auto' permission mode (closes #858) (#879)

Add 'auto' as a first-class HAPI permission mode for claude-flavored sessions,
enforced by Claude's classifier rather than emulated in canCallTool. Includes
mode configuration, CLI respawn on auto transitions, plan-exit targeting, API
extensions, and documentation updates.
This commit is contained in:
weishu
2026-06-11 11:43:51 +08:00
committed by GitHub
parent d464651870
commit 93d004148d
8 changed files with 36 additions and 8 deletions
+1 -1
View File
@@ -145,7 +145,7 @@ Before commit/push/PR: use the **`pre-push-review`** skill (`~/.cursor/skills/pr
- **RPC**: CLI registers handlers (`rpc-register`), hub routes requests via `rpcGateway.ts`
- **Versioned updates**: CLI sends `update-metadata`/`update-state` with version; hub rejects stale
- **Session modes**: `local` (terminal) vs `remote` (web-controlled); switchable mid-session
- **Permission modes**: `default`, `acceptEdits`, `bypassPermissions`, `plan`
- **Permission modes**: `default`, `acceptEdits`, `auto`, `bypassPermissions`, `plan`
- **Namespaces**: Multi-user isolation via `CLI_API_TOKEN:<namespace>` suffix
## Adding new web features — consider an FUE
+4 -1
View File
@@ -153,8 +153,11 @@ export async function runClaude(options: StartOptions = {}): Promise<void> {
setControlledByUser(session, startingMode);
// Import MessageQueue2 and create message queue
// 'plan' and 'auto' are enforced inside Claude itself (not emulated via
// canCallTool like acceptEdits/bypassPermissions), so switching to/from
// them must start a new process with the matching --permission-mode flag.
const messageQueue = new MessageQueue2<EnhancedMode>(mode => hashObject({
isPlan: mode.permissionMode === 'plan',
agentEnforcedMode: mode.permissionMode === 'plan' || mode.permissionMode === 'auto' ? mode.permissionMode : null,
model: mode.model,
effort: mode.effort,
fallbackModel: mode.fallbackModel,
+1 -1
View File
@@ -32,7 +32,7 @@ interface PermissionResponse {
receivedAt?: number;
}
const PLAN_EXIT_MODES: PermissionMode[] = ['default', 'acceptEdits', 'bypassPermissions'];
const PLAN_EXIT_MODES: PermissionMode[] = ['default', 'acceptEdits', 'auto', 'bypassPermissions'];
function isAskUserQuestionToolName(toolName: string): boolean {
return toolName === 'AskUserQuestion' || toolName === 'ask_user_question';
+1 -1
View File
@@ -72,7 +72,7 @@ describe('buildCliArgs', () => {
})
it('validates all known permission modes', () => {
for (const mode of ['default', 'acceptEdits', 'bypassPermissions', 'plan', 'ask', 'read-only', 'safe-yolo', 'yolo']) {
for (const mode of ['default', 'acceptEdits', 'auto', 'bypassPermissions', 'plan', 'ask', 'read-only', 'safe-yolo', 'yolo']) {
const args = buildCliArgs('claude', {
directory: '/tmp',
permissionMode: mode,
+22
View File
@@ -0,0 +1,22 @@
import { describe, expect, it } from 'bun:test'
import {
getPermissionModeLabel,
getPermissionModeTone,
isPermissionModeAllowedForFlavor
} from './modes'
describe('claude auto permission mode', () => {
it('is allowed for claude only', () => {
expect(isPermissionModeAllowedForFlavor('auto', 'claude')).toBe(true)
expect(isPermissionModeAllowedForFlavor('auto', 'codex')).toBe(false)
expect(isPermissionModeAllowedForFlavor('auto', 'gemini')).toBe(false)
expect(isPermissionModeAllowedForFlavor('auto', 'cursor')).toBe(false)
expect(isPermissionModeAllowedForFlavor('auto', 'opencode')).toBe(false)
expect(isPermissionModeAllowedForFlavor('auto', 'kimi')).toBe(false)
})
it('has a label and tone', () => {
expect(getPermissionModeLabel('auto')).toBe('Auto')
expect(getPermissionModeTone('auto')).toBe('warning')
})
})
+4 -1
View File
@@ -11,7 +11,7 @@ export const AGENT_FLAVORS = ['claude', 'codex', 'cursor', 'gemini', 'kimi', 'op
export type AgentFlavor = typeof AGENT_FLAVORS[number]
export const AgentFlavorSchema = z.enum(AGENT_FLAVORS)
export const CLAUDE_PERMISSION_MODES = ['default', 'acceptEdits', 'bypassPermissions', 'plan'] as const
export const CLAUDE_PERMISSION_MODES = ['default', 'acceptEdits', 'auto', 'bypassPermissions', 'plan'] as const
export type ClaudePermissionMode = typeof CLAUDE_PERMISSION_MODES[number]
export const CODEX_PERMISSION_MODES = ['default', 'read-only', 'safe-yolo', 'yolo'] as const
@@ -35,6 +35,7 @@ export type CursorPermissionMode = typeof CURSOR_PERMISSION_MODES[number]
export const PERMISSION_MODES = [
'default',
'acceptEdits',
'auto',
'bypassPermissions',
'plan',
'ask',
@@ -49,6 +50,7 @@ export type PermissionMode = typeof PERMISSION_MODES[number]
export const PERMISSION_MODE_LABELS: Record<PermissionMode, string> = {
default: 'Default',
acceptEdits: 'Accept Edits',
auto: 'Auto',
plan: 'Plan Mode',
ask: 'Ask Mode',
debug: 'Debug Mode',
@@ -63,6 +65,7 @@ export type PermissionModeTone = 'neutral' | 'info' | 'warning' | 'danger'
export const PERMISSION_MODE_TONES: Record<PermissionMode, PermissionModeTone> = {
default: 'neutral',
acceptEdits: 'warning',
auto: 'warning',
plan: 'info',
ask: 'info',
debug: 'info',
+1 -1
View File
@@ -47,7 +47,7 @@ See `src/router.tsx` for route definitions.
- Message thread with infinite scroll.
- Composer for sending messages.
- Permission mode toggle (default/acceptEdits/bypassPermissions/plan).
- Permission mode toggle (default/acceptEdits/auto/bypassPermissions/plan).
- Model selection (default/sonnet/sonnet[1m]/opus/opus[1m]).
- Session abort and mode switch controls.
- Context size display.
+2 -2
View File
@@ -521,8 +521,8 @@ export class ApiClient {
async approvePermission(
sessionId: string,
requestId: string,
modeOrOptions?: 'default' | 'acceptEdits' | 'bypassPermissions' | 'plan' | {
mode?: 'default' | 'acceptEdits' | 'bypassPermissions' | 'plan'
modeOrOptions?: 'default' | 'acceptEdits' | 'auto' | 'bypassPermissions' | 'plan' | {
mode?: 'default' | 'acceptEdits' | 'auto' | 'bypassPermissions' | 'plan'
allowTools?: string[]
decision?: 'approved' | 'approved_for_session' | 'denied' | 'abort'
answers?: Record<string, string[]> | Record<string, { answers: string[] }>