feat: replace YoloToggle with PermissionModeSelector for Claude sessions#529
feat: replace YoloToggle with PermissionModeSelector for Claude sessions#529ye4241 wants to merge 10 commits intotiann:mainfrom
Conversation
…de 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.
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
There was a problem hiding this comment.
Findings
-
[Major]
permissionModeis dropped by the spawn API before it reaches the runner. The web client now sendspermissionMode, buthub/src/web/routes/machines.tsstill omits it fromspawnBodySchemaand from theengine.spawnSession(...)call, so Zod strips the field andacceptEdits/plannever reachrpcGatewayorbuildCliArgs. Evidence:web/src/api/client.ts:422,hub/src/web/routes/machines.ts:7.
Suggested fix:import { PermissionModeSchema } from '@hapi/protocol/schemas' const spawnBodySchema = z.object({ directory: z.string().min(1), agent: z.enum(['claude', 'codex', 'cursor', 'gemini', 'opencode']).optional(), model: z.string().optional(), effort: z.string().optional(), modelReasoningEffort: z.string().optional(), yolo: z.boolean().optional(), permissionMode: PermissionModeSchema.optional(), sessionType: z.enum(['simple', 'worktree']).optional(), worktreeName: z.string().optional() }) const result = await engine.spawnSession( machineId, parsed.data.directory, parsed.data.agent, parsed.data.model, parsed.data.modelReasoningEffort, parsed.data.yolo, parsed.data.sessionType, parsed.data.worktreeName, undefined, parsed.data.effort, parsed.data.permissionMode )
-
[Major] Hidden Claude bypass selection still sends
yolo: truefor non-Claude agents.PermissionModeSelectorreturnsnullunlessagent === 'claude', buthandleCreatederivesyolofrom the storedpermissionModewithout checking the current agent. A user who previously savedbypassPermissionsfor Claude can switch to Codex/Gemini/Cursor/OpenCode and spawn with--yolodespite no visible control. Evidence:web/src/components/NewSession/index.tsx:292.
Suggested fix:const claudePermissionMode = agent === 'claude' ? permissionMode : undefined const result = await spawnSession({ machineId, directory: trimmedDirectory, agent, model: resolvedModel, effort: resolvedEffort, modelReasoningEffort: resolvedModelReasoningEffort, yolo: claudePermissionMode === 'bypassPermissions', permissionMode: claudePermissionMode, sessionType, worktreeName: sessionType === 'worktree' ? (worktreeName.trim() || undefined) : undefined })
Summary
- Review mode: initial
- The current diff does not yet deliver the new Claude permission modes end-to-end, and it introduces a hidden unsafe-mode carryover risk for non-Claude spawns.
- Coverage gap: no updated
NewSessionpreference/spawn tests for migration,permissionModepayload forwarding, or non-Claude yolo gating.
Testing
- Not run (automation)
HAPI Bot
| return await this.request<SpawnResponse>(`/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 }) |
There was a problem hiding this comment.
[MAJOR] permissionMode is added to the web request body here, but the hub spawn route still does not accept or forward it: hub/src/web/routes/machines.ts omits it from spawnBodySchema and from engine.spawnSession(...). Because Zod strips unknown keys, acceptEdits and plan never reach the runner, so the selector does not work for the new modes.
Suggested fix:
import { PermissionModeSchema } from '@hapi/protocol/schemas'
const spawnBodySchema = z.object({
directory: z.string().min(1),
agent: z.enum(['claude', 'codex', 'cursor', 'gemini', 'opencode']).optional(),
model: z.string().optional(),
effort: z.string().optional(),
modelReasoningEffort: z.string().optional(),
yolo: z.boolean().optional(),
permissionMode: PermissionModeSchema.optional(),
sessionType: z.enum(['simple', 'worktree']).optional(),
worktreeName: z.string().optional()
})
const result = await engine.spawnSession(
machineId,
parsed.data.directory,
parsed.data.agent,
parsed.data.model,
parsed.data.modelReasoningEffort,
parsed.data.yolo,
parsed.data.sessionType,
parsed.data.worktreeName,
undefined,
parsed.data.effort,
parsed.data.permissionMode
)| effort: resolvedEffort, | ||
| modelReasoningEffort: resolvedModelReasoningEffort, | ||
| yolo: yoloMode, | ||
| yolo: permissionMode === 'bypassPermissions', |
There was a problem hiding this comment.
[MAJOR] This still derives yolo from the saved Claude permissionMode even when the current agent is not Claude. Since PermissionModeSelector is hidden for non-Claude agents, a saved bypassPermissions value can silently spawn Codex/Gemini/Cursor/OpenCode with --yolo and no visible control.
Suggested fix:
const claudePermissionMode = agent === 'claude' ? permissionMode : undefined
const result = await spawnSession({
machineId,
directory: trimmedDirectory,
agent,
model: resolvedModel,
effort: resolvedEffort,
modelReasoningEffort: resolvedModelReasoningEffort,
yolo: claudePermissionMode === 'bypassPermissions',
permissionMode: claudePermissionMode,
sessionType,
worktreeName: sessionType === 'worktree' ? (worktreeName.trim() || undefined) : undefined
})There was a problem hiding this comment.
Pull request overview
Replaces the binary YOLO toggle in the New Session UI with a Claude-only Permission Mode selector so users can choose among all supported Claude permission modes, and attempts to thread that selection through to the spawn request.
Changes:
- Added
PermissionModeSelectordropdown (Claude-only) and wired it intoNewSession. - Persisted
permissionModein localStorage with migration from legacyyolo=true. - Added
permissionModeto the web spawn call path and introduced a new i18n label.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| web/src/components/NewSession/PermissionModeSelector.tsx | New Claude-only dropdown UI for selecting permission mode. |
| web/src/components/NewSession/index.tsx | Replaces yoloMode state with permissionMode and sends both yolo (compat) + permissionMode on spawn. |
| web/src/components/NewSession/preferences.ts | Adds localStorage persistence + migration from legacy YOLO toggle. |
| web/src/components/NewSession/types.ts | Exposes Claude permission mode option list from protocol constants/labels. |
| web/src/hooks/mutations/useSpawnSession.ts | Threads permissionMode through the mutation to the API client. |
| web/src/api/client.ts | Adds permissionMode to spawnSession request body. |
| web/src/lib/locales/en.ts | Adds newSession.permissionMode label. |
| web/src/lib/locales/zh-CN.ts | Adds newSession.permissionMode label. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| return await this.request<SpawnResponse>(`/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 }) | ||
| }) |
There was a problem hiding this comment.
The client now includes permissionMode in the spawn request body, but the server-side /api/machines/:id/spawn route currently does not read/forward permissionMode (it only passes yolo, effort, etc.). As a result, modes other than the legacy YOLO mapping will be ignored end-to-end. Either update the server route/schema to accept and pass permissionMode, or remove this field until the backend is wired up.
| effort?: string | ||
| modelReasoningEffort?: string | ||
| yolo?: boolean | ||
| permissionMode?: string |
There was a problem hiding this comment.
permissionMode is typed as string here, which loses the compile-time guarantees you already have elsewhere (e.g., PermissionMode / ClaudePermissionMode). Consider typing this as PermissionMode (or ClaudePermissionMode if it’s intentionally Claude-only) so invalid values can’t be threaded into the API call.
| permissionMode?: string | |
| permissionMode?: Parameters<ApiClient['spawnSession']>[10] |
| 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' | ||
| } |
There was a problem hiding this comment.
New loadPreferredPermissionMode/savePreferredPermissionMode logic (including the legacy yolo=true migration path) isn’t covered by the existing preferences.test.ts. Adding/adjusting tests would help prevent regressions, especially around validating stored values and the yolo→bypassPermissions migration behavior.
| effort: resolvedEffort, | ||
| modelReasoningEffort: resolvedModelReasoningEffort, | ||
| yolo: yoloMode, | ||
| yolo: permissionMode === 'bypassPermissions', |
There was a problem hiding this comment.
yolo is derived solely from permissionMode and is sent even when agent !== 'claude'. If a user previously selected bypassPermissions for a Claude session and then switches to another agent, this will still send yolo: true (with the selector hidden), which can lead to unexpected/dangerous spawns or even CLI arg incompatibilities for non-Claude agents. Gate yolo behind the Claude agent (e.g., only set it when agent === 'claude' and mode is bypassPermissions).
| yolo: permissionMode === 'bypassPermissions', | |
| yolo: agent === 'claude' && permissionMode === 'bypassPermissions' ? true : undefined, |
…ermission-mode-selector
There was a problem hiding this comment.
Findings
-
[Major]
permissionModeis still dropped before it reaches the runner. The web client now serializespermissionMode, buthub/src/web/routes/machines.tsstill omits it fromspawnBodySchemaand fromengine.spawnSession(...), so Zod strips the field andacceptEdits/plannever reachrpcGatewayorbuildCliArgs. Evidence:web/src/api/client.ts:422,hub/src/web/routes/machines.ts:7.
Suggested fix:import { PermissionModeSchema } from '@hapi/protocol/schemas' const spawnBodySchema = z.object({ directory: z.string().min(1), agent: z.enum(['claude', 'codex', 'cursor', 'gemini', 'opencode']).optional(), model: z.string().optional(), effort: z.string().optional(), modelReasoningEffort: z.string().optional(), yolo: z.boolean().optional(), permissionMode: PermissionModeSchema.optional(), sessionType: z.enum(['simple', 'worktree']).optional(), worktreeName: z.string().optional() }) const result = await engine.spawnSession( machineId, parsed.data.directory, parsed.data.agent, parsed.data.model, parsed.data.modelReasoningEffort, parsed.data.yolo, parsed.data.sessionType, parsed.data.worktreeName, undefined, parsed.data.effort, parsed.data.permissionMode )
-
[Major] Hidden Claude bypass state still sends
yolo: truefor non-Claude agents.PermissionModeSelectoris hidden unlessagent === 'claude', buthandleCreatederivesyolofrom the persisted ClaudepermissionModewithout checking the selected agent. A user who savedbypassPermissionsfor Claude can switch to Codex/Gemini/Cursor/OpenCode and spawn with unsafe mode despite no visible control. Evidence:web/src/components/NewSession/index.tsx:292.
Suggested fix:const claudePermissionMode = agent === 'claude' ? permissionMode : undefined const result = await spawnSession({ machineId, directory: trimmedDirectory, agent, model: resolvedModel, effort: resolvedEffort, modelReasoningEffort: resolvedModelReasoningEffort, yolo: claudePermissionMode === 'bypassPermissions', permissionMode: claudePermissionMode, sessionType, worktreeName: sessionType === 'worktree' ? (worktreeName.trim() || undefined) : undefined })
Summary
- Review mode: follow-up after new commits
- The follow-up commit adds Claude settings default-mode handling, but the end-to-end web spawn path still does not deliver the new permission modes, and the hidden non-Claude unsafe-mode carryover remains.
- Coverage gap: no updated
NewSessionpreference/spawn tests for migration,permissionModepayload forwarding, or non-Claude yolo gating.
Testing
- Not run (automation)
HAPI Bot
| return await this.request<SpawnResponse>(`/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 }) |
There was a problem hiding this comment.
[MAJOR] permissionMode is added to the web request body here, but the hub spawn route still does not accept or forward it: hub/src/web/routes/machines.ts omits it from spawnBodySchema and from engine.spawnSession(...). Because Zod strips unknown keys, acceptEdits and plan never reach the runner.
Suggested fix:
import { PermissionModeSchema } from '@hapi/protocol/schemas'
const spawnBodySchema = z.object({
directory: z.string().min(1),
agent: z.enum(['claude', 'codex', 'cursor', 'gemini', 'opencode']).optional(),
model: z.string().optional(),
effort: z.string().optional(),
modelReasoningEffort: z.string().optional(),
yolo: z.boolean().optional(),
permissionMode: PermissionModeSchema.optional(),
sessionType: z.enum(['simple', 'worktree']).optional(),
worktreeName: z.string().optional()
})
const result = await engine.spawnSession(
machineId,
parsed.data.directory,
parsed.data.agent,
parsed.data.model,
parsed.data.modelReasoningEffort,
parsed.data.yolo,
parsed.data.sessionType,
parsed.data.worktreeName,
undefined,
parsed.data.effort,
parsed.data.permissionMode
)| effort: resolvedEffort, | ||
| modelReasoningEffort: resolvedModelReasoningEffort, | ||
| yolo: yoloMode, | ||
| yolo: permissionMode === 'bypassPermissions', |
There was a problem hiding this comment.
[MAJOR] This still derives yolo from the saved Claude permissionMode even when the current agent is not Claude. Since PermissionModeSelector is hidden for non-Claude agents, a saved bypassPermissions value can silently spawn Codex/Gemini/Cursor/OpenCode with unsafe mode and no visible control.
Suggested fix:
const claudePermissionMode = agent === 'claude' ? permissionMode : undefined
const result = await spawnSession({
machineId,
directory: trimmedDirectory,
agent,
model: resolvedModel,
effort: resolvedEffort,
modelReasoningEffort: resolvedModelReasoningEffort,
yolo: claudePermissionMode === 'bypassPermissions',
permissionMode: claudePermissionMode,
sessionType,
worktreeName: sessionType === 'worktree' ? (worktreeName.trim() || undefined) : undefined
})- 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
There was a problem hiding this comment.
Findings
-
[Major] Hidden Claude bypass state still sets YOLO for non-Claude spawns —
PermissionModeSelectoris hidden unlessagent === 'claude', buthandleCreatestill derivesyolofrom the persisted ClaudepermissionModebefore checking the selected agent. A user who savedbypassPermissionsfor Claude can switch to Codex/Gemini/Cursor/OpenCode and spawn with unsafe mode even though no control is visible. Evidence:web/src/components/NewSession/index.tsx:292.
Suggested fix:const claudePermissionMode = agent === 'claude' ? permissionMode : undefined const result = await spawnSession({ machineId, directory: trimmedDirectory, agent, model: resolvedModel, effort: resolvedEffort, modelReasoningEffort: resolvedModelReasoningEffort, yolo: claudePermissionMode === 'bypassPermissions', permissionMode: claudePermissionMode, sessionType, worktreeName: sessionType === 'worktree' ? (worktreeName.trim() || undefined) : undefined })
-
[Minor]
permissionModebypasses runtime validation in the spawn API — the route accepts any string and casts it toPermissionMode, so invalid values silently reach the runner path, and globally valid but wrong-for-agent modes can be forwarded to CLI commands that reject them later. This should be rejected at the API boundary like the existing session permission-mode route. Evidence:hub/src/web/routes/machines.ts:15.
Suggested fix:import { isPermissionModeAllowedForFlavor } from '@hapi/protocol' import { PermissionModeSchema } from '@hapi/protocol/schemas' const spawnBodySchema = z.object({ directory: z.string().min(1), agent: z.enum(['claude', 'codex', 'cursor', 'gemini', 'opencode']).optional(), model: z.string().optional(), effort: z.string().optional(), modelReasoningEffort: z.string().optional(), yolo: z.boolean().optional(), permissionMode: PermissionModeSchema.optional(), sessionType: z.enum(['simple', 'worktree']).optional(), worktreeName: z.string().optional() }) const agent = parsed.data.agent ?? 'claude' if (parsed.data.permissionMode && !isPermissionModeAllowedForFlavor(parsed.data.permissionMode, agent)) { return c.json({ error: 'Invalid permissionMode' }, 400) }
Summary
- Review mode: follow-up after new commits
- The follow-up wires
permissionModethrough the hub, but the hidden non-Claude unsafe-mode carryover remains, and the new API field is cast instead of validated. - Coverage gap: no updated web/hub tests cover permission-mode spawn payloads, legacy yolo migration, or non-Claude yolo gating.
Testing
- Not run (automation)
HAPI Bot
| effort: resolvedEffort, | ||
| modelReasoningEffort: resolvedModelReasoningEffort, | ||
| yolo: yoloMode, | ||
| yolo: permissionMode === 'bypassPermissions', |
There was a problem hiding this comment.
[Major] Hidden Claude bypass state still sets YOLO for non-Claude spawns. PermissionModeSelector returns null for non-Claude agents, but this line still sends yolo: true whenever the persisted Claude mode is bypassPermissions. Gate both yolo and permissionMode on the selected agent.
Suggested fix:
const claudePermissionMode = agent === 'claude' ? permissionMode : undefined
const result = await spawnSession({
machineId,
directory: trimmedDirectory,
agent,
model: resolvedModel,
effort: resolvedEffort,
modelReasoningEffort: resolvedModelReasoningEffort,
yolo: claudePermissionMode === 'bypassPermissions',
permissionMode: claudePermissionMode,
sessionType,
worktreeName: sessionType === 'worktree' ? (worktreeName.trim() || undefined) : undefined
})| effort: z.string().optional(), | ||
| modelReasoningEffort: z.string().optional(), | ||
| yolo: z.boolean().optional(), | ||
| permissionMode: z.string().optional(), |
There was a problem hiding this comment.
[Minor] permissionMode should use the shared schema instead of z.string() plus a cast. As written, invalid values and modes that are valid globally but invalid for the requested agent can pass the API boundary and fail later in the runner/CLI path.
Suggested fix:
import { isPermissionModeAllowedForFlavor } from '@hapi/protocol'
import { PermissionModeSchema } from '@hapi/protocol/schemas'
const spawnBodySchema = z.object({
directory: z.string().min(1),
agent: z.enum(['claude', 'codex', 'cursor', 'gemini', 'opencode']).optional(),
model: z.string().optional(),
effort: z.string().optional(),
modelReasoningEffort: z.string().optional(),
yolo: z.boolean().optional(),
permissionMode: PermissionModeSchema.optional(),
sessionType: z.enum(['simple', 'worktree']).optional(),
worktreeName: z.string().optional()
})
const agent = parsed.data.agent ?? 'claude'
if (parsed.data.permissionMode && !isPermissionModeAllowedForFlavor(parsed.data.permissionMode, agent)) {
return c.json({ error: 'Invalid permissionMode' }, 400)
}- 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
There was a problem hiding this comment.
Findings
-
[Major] Hidden Claude bypass state still sets YOLO for non-Claude spawns —
PermissionModeSelectorreturnsnullunlessagent === 'claude', andpermissionModeis already suppressed for other agents, but the payload still sendsyolo: truewhenever the persisted Claude mode isbypassPermissions. A user can save bypass mode for Claude, switch to Codex/Gemini/Cursor/OpenCode, and spawn unsafe mode with no visible control. Evidence:web/src/components/NewSession/index.tsx:292.
Suggested fix:const claudePermissionMode = agent === 'claude' ? permissionMode : undefined const result = await spawnSession({ machineId, directory: trimmedDirectory, agent, model: resolvedModel, effort: resolvedEffort, modelReasoningEffort: resolvedModelReasoningEffort, yolo: claudePermissionMode === 'bypassPermissions', permissionMode: claudePermissionMode, sessionType, worktreeName: sessionType === 'worktree' ? (worktreeName.trim() || undefined) : undefined })
-
[Minor] Spawn API accepts permission modes that are invalid for the requested agent —
PermissionModeSchemaaccepts Claude-only values likebypassPermissions; withagent: 'codex', this reachesbuildCliArgs()as--permission-mode bypassPermissions, then the spawned CLI rejects it later instead of the API returning a 400 at the boundary. Evidence:hub/src/web/routes/machines.ts:67.
Suggested fix:import { isPermissionModeAllowedForFlavor } from '@hapi/protocol' const agent = parsed.data.agent ?? 'claude' if (parsed.data.permissionMode && !isPermissionModeAllowedForFlavor(parsed.data.permissionMode, agent)) { return c.json({ error: 'Invalid permissionMode' }, 400) } const result = await engine.spawnSession( machineId, parsed.data.directory, agent, parsed.data.model, parsed.data.modelReasoningEffort, parsed.data.yolo, parsed.data.sessionType, parsed.data.worktreeName, undefined, parsed.data.effort, parsed.data.permissionMode )
Summary
- Review mode: follow-up after new commits
- The follow-up fixed raw
permissionModeschema validation, but the hidden non-Claude YOLO carryover remains, and the route still does not validatepermissionModeagainst the requested agent flavor. - Coverage gap: no updated web/hub tests cover permission-mode spawn payloads, legacy yolo migration, or non-Claude yolo gating.
Testing
- Not run (automation)
HAPI Bot
There was a problem hiding this comment.
Findings
- [Minor] Spawn API accepts permission modes that are invalid for the selected agent —
PermissionModeSchemaonly proves the value is in the global union, but this route forwards it without checking the requested/default agent flavor.buildCliArgs()emits--permission-modefor any global mode, while the individual CLI commands reject modes outside their own flavor set, so a request such asagent: "codex", permissionMode: "bypassPermissions"fails later in the runner instead of returning a clean 400 at the API boundary. Evidence:hub/src/web/routes/machines.ts:67.
Suggested fix:import { isPermissionModeAllowedForFlavor } from '@hapi/protocol' 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, agent, parsed.data.model, parsed.data.modelReasoningEffort, parsed.data.yolo, parsed.data.sessionType, parsed.data.worktreeName, undefined, parsed.data.effort, parsed.data.permissionMode )
Summary
- Review mode: follow-up after new commits
- Latest commit fixed the hidden non-Claude
yolocarryover from the prior bot pass. - One boundary-validation issue remains on the new
permissionModespawn API field. - Coverage gap: no hub/web tests cover
permissionModespawn validation, legacy YOLO migration, or Claude-only payload gating.
Testing
- Not run (automation)
HAPI Bot
| undefined, | ||
| parsed.data.effort | ||
| parsed.data.effort, | ||
| parsed.data.permissionMode |
There was a problem hiding this comment.
[MINOR] This forwards any globally valid PermissionModeSchema value without checking whether it is valid for the selected/default agent. buildCliArgs() then emits --permission-mode for any global mode, while the individual CLI commands reject modes outside their own flavor set, so agent: "codex", permissionMode: "bypassPermissions" fails later in the runner instead of returning 400 here.
Suggested fix:
const agent = parsed.data.agent ?? 'claude'
if (parsed.data.permissionMode && !isPermissionModeAllowedForFlavor(parsed.data.permissionMode, agent)) {
return c.json({ error: 'Invalid permissionMode for agent' }, 400)
}|
The test failure in |
There was a problem hiding this comment.
Findings
- [Major] Non-Claude YOLO spawn option was removed — the old
YoloTogglewas rendered for every agent andyoloModewas forwarded tospawnSession, which lets Codex/Gemini/Cursor/OpenCode start with their supported--yolomode. The new code drops bothyoloandpermissionModewheneveragent !== 'claude', so those agents can no longer be started in YOLO from the web UI. Evidence:web/src/components/NewSession/index.tsx:285.
Suggested fix:const nonClaudeYolo = agent !== 'claude' && yoloMode const claudePermissionMode = agent === 'claude' ? permissionMode : undefined // render the Claude selector only for Claude, and keep a YOLO toggle for other agents {agent === 'claude' ? ( <PermissionModeSelector agent={agent} permissionMode={permissionMode} isDisabled={isFormDisabled} onPermissionModeChange={setPermissionMode} /> ) : ( <YoloToggle yoloMode={yoloMode} isDisabled={isFormDisabled} onToggle={setYoloMode} /> )} await spawnSession({ ..., yolo: claudePermissionMode === 'bypassPermissions' || nonClaudeYolo, permissionMode: claudePermissionMode, })
Questions
- None.
Summary
- Review mode: follow-up after new commits
- One regression remains in the new-session web flow: non-Claude agents lost the existing YOLO spawn control.
- Coverage gap: no web/hub tests cover agent-specific spawn payloads or the legacy YOLO migration path.
Testing
- Not run (automation)
HAPI Bot
| const resolvedModelReasoningEffort = agent === 'codex' && modelReasoningEffort !== 'default' | ||
| ? modelReasoningEffort | ||
| : undefined | ||
| const claudePermissionMode = agent === 'claude' ? permissionMode : undefined |
There was a problem hiding this comment.
[MAJOR] This now drops both yolo and permissionMode for every non-Claude agent, whereas the removed YoloToggle applied to all agents and let Codex/Gemini/Cursor/OpenCode start with their supported --yolo mode. That makes YOLO unavailable from the web new-session flow for those agents.
Suggested fix:
const nonClaudeYolo = agent !== 'claude' && yoloMode
const claudePermissionMode = agent === 'claude' ? permissionMode : undefined
await spawnSession({
...,
yolo: claudePermissionMode === 'bypassPermissions' || nonClaudeYolo,
permissionMode: claudePermissionMode,
})…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.
There was a problem hiding this comment.
Findings
- [Major] Non-Claude YOLO spawn option is no longer reachable — the removed
YoloToggleapplied to every agent andhandleCreateforwardedyolodirectly, but the new flow only setsyolowhen the Claude-onlypermissionModeisbypassPermissions. BecausePermissionModeSelectorreturnsnullfor non-Claude agents, Codex/Cursor/Gemini/OpenCode sessions can no longer be spawned in YOLO from the web UI even though their CLI commands still support it. Evidence:web/src/components/NewSession/index.tsx:293.
Suggested fix:const [nonClaudeYoloMode, setNonClaudeYoloMode] = useState(loadPreferredYoloMode) const claudePermissionMode = agent === 'claude' ? permissionMode : undefined const nonClaudeYolo = agent !== 'claude' && nonClaudeYoloMode await spawnSession({ ..., yolo: claudePermissionMode === 'bypassPermissions' || nonClaudeYolo, permissionMode: claudePermissionMode, })
Questions
- None.
Summary
- Review mode: follow-up after new commits
- The previous bot finding remains unresolved in the latest head: non-Claude agents lost the existing YOLO spawn control.
- Coverage gap: no web/hub test asserts agent-specific spawn payloads or the legacy YOLO preference migration.
Testing
- Not run (automation; review only, no PR code executed)
HAPI Bot
| effort: resolvedEffort, | ||
| modelReasoningEffort: resolvedModelReasoningEffort, | ||
| yolo: yoloMode, | ||
| yolo: claudePermissionMode === 'bypassPermissions', |
There was a problem hiding this comment.
[MAJOR] This now derives yolo only from the Claude permission selector. Since PermissionModeSelector is hidden for non-Claude agents, Codex/Cursor/Gemini/OpenCode users no longer have a web UI path to spawn YOLO sessions, even though the old YoloToggle applied to all agents and those commands still support YOLO.
Suggested fix:
const [nonClaudeYoloMode, setNonClaudeYoloMode] = useState(loadPreferredYoloMode)
const claudePermissionMode = agent === 'claude' ? permissionMode : undefined
const nonClaudeYolo = agent !== 'claude' && nonClaudeYoloMode
await spawnSession({
...,
yolo: claudePermissionMode === 'bypassPermissions' || nonClaudeYolo,
permissionMode: claudePermissionMode,
})There was a problem hiding this comment.
Findings
- [Major] Non-Claude YOLO spawn option is no longer reachable —
PermissionModeSelectorrenders only for Claude, but the spawn payload now setsyoloonly when the Claude permission mode isbypassPermissions. The removedYoloTogglewas available for all agents, so Codex/Cursor/Gemini/OpenCode sessions no longer have a web UI path to spawn with their supported YOLO mode. Evidence:web/src/components/NewSession/index.tsx:293.
Suggested fix:const [nonClaudeYoloMode, setNonClaudeYoloMode] = useState(loadPreferredYoloMode) const claudePermissionMode = agent === 'claude' ? permissionMode : undefined const nonClaudeYolo = agent !== 'claude' && nonClaudeYoloMode await spawnSession({ ..., yolo: claudePermissionMode === 'bypassPermissions' || nonClaudeYolo, permissionMode: claudePermissionMode, })
Questions
- None.
Summary
- Review mode: follow-up after new commits
- The previous bot finding remains unresolved in the latest head. The only change since that pass is
bun.lock, so the functional regression is unchanged. - Coverage gap: no web test asserts the per-agent spawn payload after replacing the global YOLO toggle.
Testing
- Not run (automation; review only, no PR code executed)
HAPI Bot
| effort: resolvedEffort, | ||
| modelReasoningEffort: resolvedModelReasoningEffort, | ||
| yolo: yoloMode, | ||
| yolo: claudePermissionMode === 'bypassPermissions', |
There was a problem hiding this comment.
[MAJOR] This derives yolo only from the Claude permission selector. Since PermissionModeSelector returns null for non-Claude agents, Codex/Cursor/Gemini/OpenCode users no longer have a web UI path to spawn YOLO sessions even though the old YoloToggle applied to all agents and those commands still support YOLO.
Suggested fix:
const [nonClaudeYoloMode, setNonClaudeYoloMode] = useState(loadPreferredYoloMode)
const claudePermissionMode = agent === 'claude' ? permissionMode : undefined
const nonClaudeYolo = agent !== 'claude' && nonClaudeYoloMode
await spawnSession({
...,
yolo: claudePermissionMode === 'bypassPermissions' || nonClaudeYolo,
permissionMode: claudePermissionMode,
})
Problem
The new session UI only offers a binary Yolo toggle, which maps to
bypassPermissions. Users have no way to select other Claude permission modes (acceptEdits,plan,default) when spawning a remote session.Solution
Replace
YoloTogglewith aPermissionModeSelectordropdown that exposes all four Claude permission modes using the existingCLAUDE_PERMISSION_MODESandPERMISSION_MODE_LABELSalready defined in@hapi/protocol.defaultacceptEditsbypassPermissionsplanChanges
PermissionModeSelector.tsx— new dropdown component (only shown for Claude agent, same pattern asClaudeEffortSelector)index.tsx— replaceyoloMode: booleanstate withpermissionMode: ClaudePermissionMode; deriveyolo: truewhenpermissionMode === 'bypassPermissions'for runner backwards compatibilitypreferences.ts— persist selection in localStorage; migrate users who hadyolo=truetobypassPermissionsuseSpawnSession.ts/client.ts— threadpermissionModethrough to the spawn API callen.ts/zh-CN.ts— addnewSession.permissionModei18n keyTest plan
permissionModein logsyolo=truein localStorage are migrated tobypassPermissions