From 546912afb2d46391365edcf56dc4fa04205e0fad Mon Sep 17 00:00:00 2001 From: ye4241 Date: Sun, 26 Apr 2026 20:02:48 +0800 Subject: [PATCH 1/9] fix: respect Claude's defaultMode from settings.json as permission mode default When hapi spawns a session without an explicit permissionMode, it was hardcoded to 'default', ignoring the user's `permissions.defaultMode` setting in ~/.claude/settings.json (e.g. 'auto'). Now reads Claude's settings.json via the existing readClaudeSettings() utility and uses its defaultMode as the fallback, keeping 'default' as the final fallback for users without the setting configured. --- cli/src/claude/runClaude.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cli/src/claude/runClaude.ts b/cli/src/claude/runClaude.ts index c7aae047b..29d5cc72a 100644 --- a/cli/src/claude/runClaude.ts +++ b/cli/src/claude/runClaude.ts @@ -20,6 +20,7 @@ import { formatMessageWithAttachments } from '@/utils/attachmentFormatter'; import { normalizeClaudeSessionModel } from './model'; import { normalizeClaudeSessionEffort } from './effort'; import { getInvokedCwd } from '@/utils/invokedCwd'; +import { readClaudeSettings } from '@/claude/utils/claudeSettings'; export interface StartOptions { model?: string @@ -151,7 +152,9 @@ export async function runClaude(options: StartOptions = {}): Promise { })); // Forward messages to the queue - let currentPermissionMode: PermissionMode = options.permissionMode ?? 'default'; + const claudeSettings = readClaudeSettings(); + const claudeDefaultMode = claudeSettings?.permissions?.defaultMode as PermissionMode | undefined; + let currentPermissionMode: PermissionMode = options.permissionMode ?? claudeDefaultMode ?? 'default'; let currentModel: SessionModel = initialModel; let currentEffort: SessionEffort = initialEffort; let currentFallbackModel: string | undefined = undefined; // Track current fallback model From 5ec4d78b83f451a38e89521df6d9d431e5497936 Mon Sep 17 00:00:00 2001 From: ye4241 Date: Sun, 26 Apr 2026 20:39:56 +0800 Subject: [PATCH 2/9] feat: replace YoloToggle with PermissionModeSelector for Claude sessions The previous UI only offered a binary yolo toggle (bypassPermissions). This replaces it with a proper dropdown selector exposing all four Claude permission modes: Default, Accept Edits, Bypass Permissions (yolo), and Plan Mode. Changes: - Add PermissionModeSelector component using existing CLAUDE_PERMISSION_MODES and PERMISSION_MODE_LABELS from @hapi/protocol - Remove YoloToggle from NewSession; derive yolo flag from permissionMode === 'bypassPermissions' for backwards compatibility with the runner - Thread permissionMode through useSpawnSession and ApiClient.spawnSession so the backend receives it directly - Persist selected mode in localStorage with migration from legacy yolo key - Add i18n keys for en and zh-CN locales --- web/src/api/client.ts | 5 +-- .../NewSession/PermissionModeSelector.tsx | 36 +++++++++++++++++++ web/src/components/NewSession/index.tsx | 24 +++++++------ web/src/components/NewSession/preferences.ts | 28 ++++++++++++++- web/src/components/NewSession/types.ts | 8 ++++- web/src/hooks/mutations/useSpawnSession.ts | 4 ++- web/src/lib/locales/en.ts | 1 + web/src/lib/locales/zh-CN.ts | 1 + 8 files changed, 91 insertions(+), 16 deletions(-) create mode 100644 web/src/components/NewSession/PermissionModeSelector.tsx diff --git a/web/src/api/client.ts b/web/src/api/client.ts index 19c577b54..379909cd8 100644 --- a/web/src/api/client.ts +++ b/web/src/api/client.ts @@ -414,11 +414,12 @@ export class ApiClient { yolo?: boolean, sessionType?: 'simple' | 'worktree', worktreeName?: string, - effort?: string + effort?: string, + permissionMode?: string ): Promise { return await this.request(`/api/machines/${encodeURIComponent(machineId)}/spawn`, { method: 'POST', - body: JSON.stringify({ directory, agent, model, modelReasoningEffort, yolo, sessionType, worktreeName, effort }) + body: JSON.stringify({ directory, agent, model, modelReasoningEffort, yolo, sessionType, worktreeName, effort, permissionMode }) }) } diff --git a/web/src/components/NewSession/PermissionModeSelector.tsx b/web/src/components/NewSession/PermissionModeSelector.tsx new file mode 100644 index 000000000..331c2772b --- /dev/null +++ b/web/src/components/NewSession/PermissionModeSelector.tsx @@ -0,0 +1,36 @@ +import type { AgentType, ClaudePermissionMode } from './types' +import { CLAUDE_PERMISSION_MODE_OPTIONS } from './types' +import { useTranslation } from '@/lib/use-translation' + +export function PermissionModeSelector(props: { + agent: AgentType + permissionMode: ClaudePermissionMode + isDisabled: boolean + onPermissionModeChange: (value: ClaudePermissionMode) => void +}) { + const { t } = useTranslation() + + if (props.agent !== 'claude') { + return null + } + + return ( +
+ + +
+ ) +} diff --git a/web/src/components/NewSession/index.tsx b/web/src/components/NewSession/index.tsx index 721596b8d..4b420dc3c 100644 --- a/web/src/components/NewSession/index.tsx +++ b/web/src/components/NewSession/index.tsx @@ -10,7 +10,7 @@ import { useActiveSuggestions, type Suggestion } from '@/hooks/useActiveSuggesti import { useDirectorySuggestions } from '@/hooks/useDirectorySuggestions' import { useRecentPaths } from '@/hooks/useRecentPaths' import { useTranslation } from '@/lib/use-translation' -import type { AgentType, ClaudeEffort, CodexReasoningEffort, SessionType } from './types' +import type { AgentType, ClaudeEffort, ClaudePermissionMode, CodexReasoningEffort, SessionType } from './types' import { ActionButtons } from './ActionButtons' import { AgentSelector } from './AgentSelector' import { DirectorySection } from './DirectorySection' @@ -18,14 +18,14 @@ import { MachineSelector } from './MachineSelector' import { ModelSelector } from './ModelSelector' import { ClaudeEffortSelector } from './ClaudeEffortSelector' import { ReasoningEffortSelector } from './ReasoningEffortSelector' +import { PermissionModeSelector } from './PermissionModeSelector' import { loadPreferredAgent, - loadPreferredYoloMode, + loadPreferredPermissionMode, savePreferredAgent, - savePreferredYoloMode, + savePreferredPermissionMode, } from './preferences' import { SessionTypeSelector } from './SessionTypeSelector' -import { YoloToggle } from './YoloToggle' import { formatRunnerSpawnError } from '../../utils/formatRunnerSpawnError' export function NewSession(props: { @@ -53,7 +53,7 @@ export function NewSession(props: { const [model, setModel] = useState('auto') const [effort, setEffort] = useState('auto') const [modelReasoningEffort, setModelReasoningEffort] = useState('default') - const [yoloMode, setYoloMode] = useState(loadPreferredYoloMode) + const [permissionMode, setPermissionMode] = useState(loadPreferredPermissionMode) const [sessionType, setSessionType] = useState('simple') const [worktreeName, setWorktreeName] = useState('') const [directoryCreationConfirmed, setDirectoryCreationConfirmed] = useState(false) @@ -76,8 +76,8 @@ export function NewSession(props: { }, [agent]) useEffect(() => { - savePreferredYoloMode(yoloMode) - }, [yoloMode]) + savePreferredPermissionMode(permissionMode) + }, [permissionMode]) useEffect(() => { if (props.machines.length === 0) return @@ -289,7 +289,8 @@ export function NewSession(props: { model: resolvedModel, effort: resolvedEffort, modelReasoningEffort: resolvedModelReasoningEffort, - yolo: yoloMode, + yolo: permissionMode === 'bypassPermissions', + permissionMode: agent === 'claude' ? permissionMode : undefined, sessionType, worktreeName: sessionType === 'worktree' ? (worktreeName.trim() || undefined) : undefined }) @@ -378,10 +379,11 @@ export function NewSession(props: { isDisabled={isFormDisabled} onChange={setModelReasoningEffort} /> - {(error ?? spawnError) ? ( diff --git a/web/src/components/NewSession/preferences.ts b/web/src/components/NewSession/preferences.ts index 84ac458f6..6ab187457 100644 --- a/web/src/components/NewSession/preferences.ts +++ b/web/src/components/NewSession/preferences.ts @@ -1,7 +1,9 @@ -import type { AgentType } from './types' +import type { AgentType, ClaudePermissionMode } from './types' +import { CLAUDE_PERMISSION_MODES } from '@hapi/protocol' const AGENT_STORAGE_KEY = 'hapi:newSession:agent' const YOLO_STORAGE_KEY = 'hapi:newSession:yolo' +const PERMISSION_MODE_STORAGE_KEY = 'hapi:newSession:permissionMode' const VALID_AGENTS: AgentType[] = ['claude', 'codex', 'cursor', 'gemini', 'opencode'] @@ -40,3 +42,27 @@ export function savePreferredYoloMode(enabled: boolean): void { // Ignore storage errors } } + +export function loadPreferredPermissionMode(): ClaudePermissionMode { + try { + const stored = localStorage.getItem(PERMISSION_MODE_STORAGE_KEY) + if (stored && (CLAUDE_PERMISSION_MODES as readonly string[]).includes(stored)) { + return stored as ClaudePermissionMode + } + // Migrate from legacy yolo toggle + if (localStorage.getItem(YOLO_STORAGE_KEY) === 'true') { + return 'bypassPermissions' + } + } catch { + // Ignore storage errors + } + return 'default' +} + +export function savePreferredPermissionMode(mode: ClaudePermissionMode): void { + try { + localStorage.setItem(PERMISSION_MODE_STORAGE_KEY, mode) + } catch { + // Ignore storage errors + } +} diff --git a/web/src/components/NewSession/types.ts b/web/src/components/NewSession/types.ts index a950a6ff0..464c56100 100644 --- a/web/src/components/NewSession/types.ts +++ b/web/src/components/NewSession/types.ts @@ -1,4 +1,7 @@ -import { GEMINI_MODEL_PRESETS, GEMINI_MODEL_LABELS } from '@hapi/protocol' +import { GEMINI_MODEL_PRESETS, GEMINI_MODEL_LABELS, CLAUDE_PERMISSION_MODES, PERMISSION_MODE_LABELS } from '@hapi/protocol' +import type { ClaudePermissionMode } from '@hapi/protocol' + +export type { ClaudePermissionMode } export type AgentType = 'claude' | 'codex' | 'cursor' | 'gemini' | 'opencode' export type SessionType = 'simple' | 'worktree' @@ -38,3 +41,6 @@ export const CLAUDE_EFFORT_OPTIONS: { value: ClaudeEffort; label: string }[] = [ { value: 'high', label: 'High' }, { value: 'max', label: 'Max' }, ] + +export const CLAUDE_PERMISSION_MODE_OPTIONS: { value: ClaudePermissionMode; label: string }[] = + CLAUDE_PERMISSION_MODES.map((mode) => ({ value: mode, label: PERMISSION_MODE_LABELS[mode] })) diff --git a/web/src/hooks/mutations/useSpawnSession.ts b/web/src/hooks/mutations/useSpawnSession.ts index 37f69f61a..d2f1329f9 100644 --- a/web/src/hooks/mutations/useSpawnSession.ts +++ b/web/src/hooks/mutations/useSpawnSession.ts @@ -11,6 +11,7 @@ type SpawnInput = { effort?: string modelReasoningEffort?: string yolo?: boolean + permissionMode?: string sessionType?: 'simple' | 'worktree' worktreeName?: string } @@ -36,7 +37,8 @@ export function useSpawnSession(api: ApiClient | null): { input.yolo, input.sessionType, input.worktreeName, - input.effort + input.effort, + input.permissionMode ) }, onSuccess: () => { diff --git a/web/src/lib/locales/en.ts b/web/src/lib/locales/en.ts index 1596adbd3..4b492dd48 100644 --- a/web/src/lib/locales/en.ts +++ b/web/src/lib/locales/en.ts @@ -112,6 +112,7 @@ export default { 'newSession.model.optional': 'optional', 'newSession.model.loadFailed': 'Failed to load Codex models', 'newSession.reasoningEffort': 'Reasoning effort', + 'newSession.permissionMode': 'Permission Mode', 'newSession.yolo': 'YOLO mode', 'newSession.yolo.title': 'Bypass approvals and sandbox', 'newSession.yolo.desc': 'Uses dangerous agent flags when spawning.', diff --git a/web/src/lib/locales/zh-CN.ts b/web/src/lib/locales/zh-CN.ts index 8539a00d2..a40b75d22 100644 --- a/web/src/lib/locales/zh-CN.ts +++ b/web/src/lib/locales/zh-CN.ts @@ -114,6 +114,7 @@ export default { 'newSession.model.optional': '可选', 'newSession.model.loadFailed': '加载 Codex 模型失败', 'newSession.reasoningEffort': '推理强度', + 'newSession.permissionMode': '权限模式', 'newSession.yolo': 'YOLO 模式', 'newSession.yolo.title': '跳过审批和沙箱', 'newSession.yolo.desc': '启动时使用危险的代理标志。', From bc150248b1137cdcb62bb4602cd6fbec23da78e5 Mon Sep 17 00:00:00 2001 From: ye4241 Date: Sun, 26 Apr 2026 21:13:36 +0800 Subject: [PATCH 3/9] fix: address review feedback for permission mode feature - Validate claudeDefaultMode from Claude settings with PermissionModeSchema instead of unsafe type cast - Fix localStorage migration to persist bypassPermissions to new key - Add permissionMode field to hub spawn endpoint schema and pass it through to syncEngine.spawnSession - Delete dead YoloToggle.tsx component - Remove unused newSession.yolo i18n keys from en.ts and zh-CN.ts --- cli/src/claude/runClaude.ts | 5 ++- hub/src/web/routes/machines.ts | 5 ++- web/src/components/NewSession/YoloToggle.tsx | 38 -------------------- web/src/components/NewSession/preferences.ts | 1 + web/src/lib/locales/en.ts | 3 -- web/src/lib/locales/zh-CN.ts | 3 -- 6 files changed, 9 insertions(+), 46 deletions(-) delete mode 100644 web/src/components/NewSession/YoloToggle.tsx diff --git a/cli/src/claude/runClaude.ts b/cli/src/claude/runClaude.ts index 29d5cc72a..1b1297c27 100644 --- a/cli/src/claude/runClaude.ts +++ b/cli/src/claude/runClaude.ts @@ -153,7 +153,10 @@ export async function runClaude(options: StartOptions = {}): Promise { // Forward messages to the queue const claudeSettings = readClaudeSettings(); - const claudeDefaultMode = claudeSettings?.permissions?.defaultMode as PermissionMode | undefined; + const rawDefaultMode = claudeSettings?.permissions?.defaultMode; + const claudeDefaultMode = PermissionModeSchema.safeParse(rawDefaultMode).success + ? rawDefaultMode as PermissionMode + : undefined; let currentPermissionMode: PermissionMode = options.permissionMode ?? claudeDefaultMode ?? 'default'; let currentModel: SessionModel = initialModel; let currentEffort: SessionEffort = initialEffort; diff --git a/hub/src/web/routes/machines.ts b/hub/src/web/routes/machines.ts index cf4a46055..ceddd31a0 100644 --- a/hub/src/web/routes/machines.ts +++ b/hub/src/web/routes/machines.ts @@ -1,5 +1,6 @@ import { Hono } from 'hono' import { z } from 'zod' +import type { PermissionMode } from '@hapi/protocol' import type { SyncEngine } from '../../sync/syncEngine' import type { WebAppEnv } from '../middleware/auth' import { requireMachine } from './guards' @@ -11,6 +12,7 @@ const spawnBodySchema = z.object({ effort: z.string().optional(), modelReasoningEffort: z.string().optional(), yolo: z.boolean().optional(), + permissionMode: z.string().optional(), sessionType: z.enum(['simple', 'worktree']).optional(), worktreeName: z.string().optional() }) @@ -61,7 +63,8 @@ export function createMachinesRoutes(getSyncEngine: () => SyncEngine | null): Ho parsed.data.sessionType, parsed.data.worktreeName, undefined, - parsed.data.effort + parsed.data.effort, + parsed.data.permissionMode as PermissionMode | undefined ) return c.json(result) }) diff --git a/web/src/components/NewSession/YoloToggle.tsx b/web/src/components/NewSession/YoloToggle.tsx deleted file mode 100644 index f0d0d25f5..000000000 --- a/web/src/components/NewSession/YoloToggle.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { useTranslation } from '@/lib/use-translation' - -export function YoloToggle(props: { - yoloMode: boolean - isDisabled: boolean - onToggle: (value: boolean) => void -}) { - const { t } = useTranslation() - - return ( -
- -
-
- - {t('newSession.yolo.title')} - - - {t('newSession.yolo.desc')} - -
- -
-
- ) -} diff --git a/web/src/components/NewSession/preferences.ts b/web/src/components/NewSession/preferences.ts index 6ab187457..e00fc6b14 100644 --- a/web/src/components/NewSession/preferences.ts +++ b/web/src/components/NewSession/preferences.ts @@ -51,6 +51,7 @@ export function loadPreferredPermissionMode(): ClaudePermissionMode { } // Migrate from legacy yolo toggle if (localStorage.getItem(YOLO_STORAGE_KEY) === 'true') { + savePreferredPermissionMode('bypassPermissions') return 'bypassPermissions' } } catch { diff --git a/web/src/lib/locales/en.ts b/web/src/lib/locales/en.ts index 4b492dd48..d4eaec881 100644 --- a/web/src/lib/locales/en.ts +++ b/web/src/lib/locales/en.ts @@ -113,9 +113,6 @@ export default { 'newSession.model.loadFailed': 'Failed to load Codex models', 'newSession.reasoningEffort': 'Reasoning effort', 'newSession.permissionMode': 'Permission Mode', - 'newSession.yolo': 'YOLO mode', - 'newSession.yolo.title': 'Bypass approvals and sandbox', - 'newSession.yolo.desc': 'Uses dangerous agent flags when spawning.', 'newSession.create': 'Create', 'newSession.creating': 'Creating…', diff --git a/web/src/lib/locales/zh-CN.ts b/web/src/lib/locales/zh-CN.ts index a40b75d22..429c7b96b 100644 --- a/web/src/lib/locales/zh-CN.ts +++ b/web/src/lib/locales/zh-CN.ts @@ -115,9 +115,6 @@ export default { 'newSession.model.loadFailed': '加载 Codex 模型失败', 'newSession.reasoningEffort': '推理强度', 'newSession.permissionMode': '权限模式', - 'newSession.yolo': 'YOLO 模式', - 'newSession.yolo.title': '跳过审批和沙箱', - 'newSession.yolo.desc': '启动时使用危险的代理标志。', 'newSession.create': '创建', 'newSession.creating': '创建中…', From 8846bc5fe77ad0dbea66f700d6caff82c7cf3be1 Mon Sep 17 00:00:00 2001 From: ye4241 Date: Sun, 26 Apr 2026 21:17:31 +0800 Subject: [PATCH 4/9] refactor: improve permission mode validation robustness - runClaude.ts: use PermissionModeSchema.safeParse().data directly to eliminate intermediate variable and unsafe cast; add isPermissionModeAllowedForFlavor check so cross-agent modes (e.g. codex yolo) in ~/.claude/settings.json are silently ignored - machines.ts: replace z.string().optional() + type cast with PermissionModeSchema.optional() from @hapi/protocol/schemas, matching the pattern used in sessions.ts --- cli/src/claude/runClaude.ts | 6 +++--- hub/src/web/routes/machines.ts | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cli/src/claude/runClaude.ts b/cli/src/claude/runClaude.ts index 1b1297c27..142bd78c0 100644 --- a/cli/src/claude/runClaude.ts +++ b/cli/src/claude/runClaude.ts @@ -153,9 +153,9 @@ export async function runClaude(options: StartOptions = {}): Promise { // Forward messages to the queue const claudeSettings = readClaudeSettings(); - const rawDefaultMode = claudeSettings?.permissions?.defaultMode; - const claudeDefaultMode = PermissionModeSchema.safeParse(rawDefaultMode).success - ? rawDefaultMode as PermissionMode + const parsedDefaultMode = PermissionModeSchema.safeParse(claudeSettings?.permissions?.defaultMode); + const claudeDefaultMode = parsedDefaultMode.success && isPermissionModeAllowedForFlavor(parsedDefaultMode.data, 'claude') + ? parsedDefaultMode.data : undefined; let currentPermissionMode: PermissionMode = options.permissionMode ?? claudeDefaultMode ?? 'default'; let currentModel: SessionModel = initialModel; diff --git a/hub/src/web/routes/machines.ts b/hub/src/web/routes/machines.ts index ceddd31a0..e7dbdfa72 100644 --- a/hub/src/web/routes/machines.ts +++ b/hub/src/web/routes/machines.ts @@ -1,6 +1,6 @@ import { Hono } from 'hono' import { z } from 'zod' -import type { PermissionMode } from '@hapi/protocol' +import { PermissionModeSchema } from '@hapi/protocol/schemas' import type { SyncEngine } from '../../sync/syncEngine' import type { WebAppEnv } from '../middleware/auth' import { requireMachine } from './guards' @@ -12,7 +12,7 @@ const spawnBodySchema = z.object({ effort: z.string().optional(), modelReasoningEffort: z.string().optional(), yolo: z.boolean().optional(), - permissionMode: z.string().optional(), + permissionMode: PermissionModeSchema.optional(), sessionType: z.enum(['simple', 'worktree']).optional(), worktreeName: z.string().optional() }) @@ -64,7 +64,7 @@ export function createMachinesRoutes(getSyncEngine: () => SyncEngine | null): Ho parsed.data.worktreeName, undefined, parsed.data.effort, - parsed.data.permissionMode as PermissionMode | undefined + parsed.data.permissionMode ) return c.json(result) }) From 43fb6d84c60e7b5be4735555f84ea3ac3aab1e0d Mon Sep 17 00:00:00 2001 From: ye4241 Date: Sun, 26 Apr 2026 21:22:11 +0800 Subject: [PATCH 5/9] fix: gate yolo flag on claude agent to prevent unsafe spawns for other agents --- web/src/components/NewSession/index.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/web/src/components/NewSession/index.tsx b/web/src/components/NewSession/index.tsx index 4b420dc3c..268b82af1 100644 --- a/web/src/components/NewSession/index.tsx +++ b/web/src/components/NewSession/index.tsx @@ -282,6 +282,7 @@ export function NewSession(props: { const resolvedModelReasoningEffort = agent === 'codex' && modelReasoningEffort !== 'default' ? modelReasoningEffort : undefined + const claudePermissionMode = agent === 'claude' ? permissionMode : undefined const result = await spawnSession({ machineId, directory: trimmedDirectory, @@ -289,8 +290,8 @@ export function NewSession(props: { model: resolvedModel, effort: resolvedEffort, modelReasoningEffort: resolvedModelReasoningEffort, - yolo: permissionMode === 'bypassPermissions', - permissionMode: agent === 'claude' ? permissionMode : undefined, + yolo: claudePermissionMode === 'bypassPermissions', + permissionMode: claudePermissionMode, sessionType, worktreeName: sessionType === 'worktree' ? (worktreeName.trim() || undefined) : undefined }) From 22f8a7863d0d2d18e12647359e222632aa56388d Mon Sep 17 00:00:00 2001 From: ye4241 Date: Sun, 26 Apr 2026 21:23:18 +0800 Subject: [PATCH 6/9] fix: add type assertion after Claude flavor validation to satisfy tsc --- cli/src/claude/runClaude.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/src/claude/runClaude.ts b/cli/src/claude/runClaude.ts index 142bd78c0..a194158d8 100644 --- a/cli/src/claude/runClaude.ts +++ b/cli/src/claude/runClaude.ts @@ -154,8 +154,8 @@ export async function runClaude(options: StartOptions = {}): Promise { // Forward messages to the queue const claudeSettings = readClaudeSettings(); const parsedDefaultMode = PermissionModeSchema.safeParse(claudeSettings?.permissions?.defaultMode); - const claudeDefaultMode = parsedDefaultMode.success && isPermissionModeAllowedForFlavor(parsedDefaultMode.data, 'claude') - ? parsedDefaultMode.data + const claudeDefaultMode: PermissionMode | undefined = parsedDefaultMode.success && isPermissionModeAllowedForFlavor(parsedDefaultMode.data, 'claude') + ? parsedDefaultMode.data as PermissionMode : undefined; let currentPermissionMode: PermissionMode = options.permissionMode ?? claudeDefaultMode ?? 'default'; let currentModel: SessionModel = initialModel; From 482bc97a634f1a4bda0cd1a5317792140392a97a Mon Sep 17 00:00:00 2001 From: ye4241 Date: Sun, 26 Apr 2026 21:28:51 +0800 Subject: [PATCH 7/9] fix: validate permissionMode against agent flavor in spawn endpoint --- hub/src/web/routes/machines.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/hub/src/web/routes/machines.ts b/hub/src/web/routes/machines.ts index e7dbdfa72..ea84cddd4 100644 --- a/hub/src/web/routes/machines.ts +++ b/hub/src/web/routes/machines.ts @@ -1,5 +1,6 @@ import { Hono } from 'hono' import { z } from 'zod' +import { isPermissionModeAllowedForFlavor } from '@hapi/protocol' import { PermissionModeSchema } from '@hapi/protocol/schemas' import type { SyncEngine } from '../../sync/syncEngine' import type { WebAppEnv } from '../middleware/auth' @@ -53,10 +54,15 @@ export function createMachinesRoutes(getSyncEngine: () => SyncEngine | null): Ho return c.json({ error: 'Invalid body' }, 400) } + const agent = parsed.data.agent ?? 'claude' + if (parsed.data.permissionMode && !isPermissionModeAllowedForFlavor(parsed.data.permissionMode, agent)) { + return c.json({ error: 'Invalid permissionMode for agent' }, 400) + } + const result = await engine.spawnSession( machineId, parsed.data.directory, - parsed.data.agent, + agent, parsed.data.model, parsed.data.modelReasoningEffort, parsed.data.yolo, From c1dc12e9914206056225d86e6b3fc70fc259e686 Mon Sep 17 00:00:00 2001 From: ye4241 Date: Mon, 27 Apr 2026 10:43:49 +0800 Subject: [PATCH 8/9] feat: add auto and dontAsk to CLAUDE_PERMISSION_MODES and PERMISSION_MODES Claude CLI supports --permission-mode auto and dontAsk but hapi's protocol did not include them, causing auto mode from ~/.claude/settings.json to be silently dropped and fall back to default. --- shared/src/modes.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/shared/src/modes.ts b/shared/src/modes.ts index d59320a9d..b12f20ea9 100644 --- a/shared/src/modes.ts +++ b/shared/src/modes.ts @@ -5,7 +5,7 @@ */ export const AGENT_MESSAGE_PAYLOAD_TYPE = 'codex' as const -export const CLAUDE_PERMISSION_MODES = ['default', 'acceptEdits', 'bypassPermissions', 'plan'] as const +export const CLAUDE_PERMISSION_MODES = ['default', 'acceptEdits', 'bypassPermissions', 'plan', 'auto', 'dontAsk'] as const export type ClaudePermissionMode = typeof CLAUDE_PERMISSION_MODES[number] export const CODEX_PERMISSION_MODES = ['default', 'read-only', 'safe-yolo', 'yolo'] as const @@ -28,6 +28,8 @@ export const PERMISSION_MODES = [ 'acceptEdits', 'bypassPermissions', 'plan', + 'auto', + 'dontAsk', 'ask', 'read-only', 'safe-yolo', @@ -41,8 +43,10 @@ export const PERMISSION_MODE_LABELS: Record = { default: 'Default', acceptEdits: 'Accept Edits', plan: 'Plan Mode', + auto: 'Auto', + dontAsk: "Don't Ask", ask: 'Ask Mode', - bypassPermissions: 'Yolo', + bypassPermissions: 'Bypass Permissions', 'read-only': 'Read Only', 'safe-yolo': 'Safe Yolo', yolo: 'Yolo' @@ -54,6 +58,8 @@ export const PERMISSION_MODE_TONES: Record = default: 'neutral', acceptEdits: 'warning', plan: 'info', + auto: 'neutral', + dontAsk: 'danger', ask: 'info', bypassPermissions: 'danger', 'read-only': 'warning', From 8cdaf73766f1a245cc9209297d0c4fce5768216b Mon Sep 17 00:00:00 2001 From: Penn Date: Mon, 27 Apr 2026 11:03:48 +0800 Subject: [PATCH 9/9] chore: update bun.lock --- bun.lock | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bun.lock b/bun.lock index b00aeeb88..cfa5096d1 100644 --- a/bun.lock +++ b/bun.lock @@ -1062,6 +1062,8 @@ "@twsxtd/hapi-linux-x64": ["@twsxtd/hapi-linux-x64@0.17.1", "", { "os": "linux", "cpu": "x64", "bin": { "hapi": "bin/hapi" } }, "sha512-W4Wzm5KPMvXhUkx8q5Yw4cjx3g4Xhg2I7nbn8CYaMXyhyZhsetN1zIY6P5ciQMcwuL3AXbJvvOgVOodxudw0CA=="], + "@twsxtd/hapi-win32-x64": ["@twsxtd/hapi-win32-x64@0.17.1", "", { "os": "win32", "cpu": "x64", "bin": { "hapi": "bin/hapi.exe" } }, "sha512-FhSkaVbnYOSiqs/4SVbL5RD9RWdV0KES014ovA8jf0qDSpcjMMqO3SygSnn6Rj05u84Na61tzULeCnYqcwkc/A=="], + "@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="], "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],