fix(exit-plan): harden permission metadata and UI for exit-plan-mode#413
fix(exit-plan): harden permission metadata and UI for exit-plan-mode#413xyzhang626 wants to merge 8 commits intotiann:mainfrom
Conversation
There was a problem hiding this comment.
Findings
- [Major]
clear_contextstill carries over prior "allow for this session" grants into the restarted Claude session, so the fresh-context path can silently auto-approve later tools without asking again. The new branch only clearsclaudeSessionId, but the in-memory allowlists are still consulted byhandleToolCall, evidencecli/src/claude/utils/permissionHandler.ts:366andcli/src/claude/utils/permissionHandler.ts:416.
Summary
Review mode: initial
- 1 major issue found in
cli/src/claude/utils/permissionHandler.ts.
Testing
- Not run (automation environment does not have
bun)
HAPI Bot
| this.permissionMode = permissionMode; | ||
| this.session.setPermissionMode(permissionMode); | ||
|
|
||
| if (implementationMode === 'clear_context') { |
There was a problem hiding this comment.
[MAJOR] clear_context only clears Claude's persisted session id here, but the handler still keeps the previous session-scoped allowlists in memory (allowedTools, allowedBashLiterals, allowedBashPrefixes). handleToolCall() keeps consulting those sets later in the restarted session, so the new "fresh context" path can silently inherit old allow for this session approvals.
Suggested fix:
if (implementationMode === 'clear_context') {
this.allowedTools.clear()
this.allowedBashLiterals.clear()
this.allowedBashPrefixes.clear()
this.session.clearSessionId()
}In clear_context mode, only the persisted session ID was cleared but the in-memory allowedTools, allowedBashLiterals, and allowedBashPrefixes sets were preserved. This let stale "allow for this session" approvals carry over into the fresh-context restart. Clear all three sets alongside clearSessionId() so the new session starts with a clean permission slate. via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run>
There was a problem hiding this comment.
Findings
- [Major] Exit-plan approvals from the web UI can no longer carry a non-default post-plan permission mode. The new footer only submits
implementationMode, while the hub and CLI still accept and applymode, so approving a plan from the web now always restarts indefaultinstead of allowingacceptEditsorbypassPermissions. Evidenceweb/src/components/ToolCard/ExitPlanModeFooter.tsx:100,web/src/components/ToolCard/ToolCard.tsx:473,hub/src/web/routes/permissions.ts:69,cli/src/claude/utils/permissionHandler.ts:354.
Suggested fix:const [selectedPermissionMode, setSelectedPermissionMode] = useState<'default' | 'acceptEdits' | 'bypassPermissions'>('default') await run(() => props.api.approvePermission(props.sessionId, permission.id, { mode: selectedPermissionMode, implementationMode: selectedMode }), 'success')
Summary
Review mode: follow-up after new commits
- 1 major issue found in the exit-plan web approval flow.
- Existing tests cover
implementationMode, but not selecting or preserving the post-plan permission mode.
Testing
- Not run (automation environment does not have
bun)
HAPI Bot
| } | ||
|
|
||
| setLoading('approve') | ||
| await run(() => props.api.approvePermission(props.sessionId, permission.id, { |
There was a problem hiding this comment.
[MAJOR] This new footer only posts implementationMode, so the web UI has lost the ability to send the post-plan permission mode (acceptEdits / bypassPermissions). The backend and CLI still accept and apply mode, which means every exit-plan approval from the web now restarts in default instead of the user-selected mode. Evidence: hub/src/web/routes/permissions.ts:69, cli/src/claude/utils/permissionHandler.ts:354.
Suggested fix:
const [selectedPermissionMode, setSelectedPermissionMode] =
useState<'default' | 'acceptEdits' | 'bypassPermissions'>('default')
await run(() => props.api.approvePermission(props.sessionId, permission.id, {
mode: selectedPermissionMode,
implementationMode: selectedMode
}), 'success')The footer only sent implementationMode, so every exit-plan approval from the web restarted in default permission mode regardless of user intent. Derive mode from the implementation choice: - keep_context → default (conservative, approve tools individually) - clear_context → acceptEdits (trust the plan, auto-approve edits) via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run>
There was a problem hiding this comment.
Findings
- [Major]
web/src/components/ToolCard/ExitPlanModeFooter.tsx:100still couples the post-plan permission mode toimplementationMode. Choosingclear_contextnow always submitsmode: 'acceptEdits', so a user who only asked for a fresh Claude session will restart with all edit tools auto-approved (cli/src/claude/utils/permissionHandler.ts:354,cli/src/claude/utils/permissionHandler.ts:452). It also still leaves no way to choosebypassPermissionsoracceptEditswithkeep_contextfrom the web UI.
Summary
Review mode: follow-up after new commits
- 1 major issue remains in the exit-plan approval flow.
- Residual test gap:
web/src/components/ToolCard/ExitPlanModeFooter.test.tsx:56only locks in the hardcoded mapping, so it would not catch permission mode and implementation mode drifting apart again.
Testing
- Not run (security policy: PR code not executed in review automation)
HAPI Bot
|
|
||
| setLoading('approve') | ||
| await run(() => props.api.approvePermission(props.sessionId, permission.id, { | ||
| mode: selectedMode === 'clear_context' ? 'acceptEdits' : 'default', |
There was a problem hiding this comment.
[MAJOR] implementationMode and the post-plan permission level are still being treated as the same choice here. With this mapping, picking clear_context silently upgrades the next Claude session to acceptEdits, which means edit tools will auto-allow after the restart (cli/src/claude/utils/permissionHandler.ts:452). It also still leaves no path to bypassPermissions, or to acceptEdits while keeping context.
Suggested fix:
const [selectedPermissionMode, setSelectedPermissionMode] =
useState<'default' | 'acceptEdits' | 'bypassPermissions'>('default')
await run(() => props.api.approvePermission(props.sessionId, permission.id, {
mode: selectedPermissionMode,
implementationMode: selectedMode
}), 'success')Let users choose the post-plan permission mode (Normal / Accept edits / YOLO) alongside the implementation mode, instead of hardcoding it. via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run>
There was a problem hiding this comment.
Findings
- [Major]
web/src/components/ToolCard/ExitPlanModeFooter.test.tsx:56still asserts the old coupling betweenimplementationModeand permission mode, but the component now sends the independently selected permission mode (web/src/components/ToolCard/ExitPlanModeFooter.tsx:72,web/src/components/ToolCard/ExitPlanModeFooter.tsx:107). The first test now expectsclear_context -> acceptEdits, so it no longer matches the implementation and leaves the new selector behavior under-covered.
Suggested fix:fireEvent.click(screen.getByText('Clear context')) fireEvent.click(screen.getByText('Accept edits')) fireEvent.click(screen.getByRole('button', { name: 'Start implementation' })) expect(api.approvePermission).toHaveBeenCalledWith('session-1', 'request-1', { mode: 'acceptEdits', implementationMode: 'clear_context' })
Summary
Review mode: follow-up after new commits
- 1 major issue remains: the updated footer logic and its new web test file disagree, so the PR still carries a stale assertion and misses direct coverage for the new permission-mode selector.
Testing
- Not run (automation; PR code not executed during review)
HAPI Bot
| Object.defineProperty(window, 'localStorage', { value: localStorageMock, configurable: true }) | ||
| }) | ||
|
|
||
| it('submits the selected implementationMode with correct permission mode', async () => { |
There was a problem hiding this comment.
[MAJOR] This test still encodes the old clear_context -> acceptEdits mapping, but the component now posts whatever selectedPermissionMode the user picked (web/src/components/ToolCard/ExitPlanModeFooter.tsx:72, web/src/components/ToolCard/ExitPlanModeFooter.tsx:107). As written, the first assertion no longer matches the implementation, and the new permission-mode selector is left without direct coverage.
Suggested fix:
fireEvent.click(screen.getByText('Clear context'))
fireEvent.click(screen.getByText('Accept edits'))
fireEvent.click(screen.getByRole('button', { name: 'Start implementation' }))
expect(api.approvePermission).toHaveBeenCalledWith('session-1', 'request-1', {
mode: 'acceptEdits',
implementationMode: 'clear_context'
})… mode selector Replace stale tests that assumed clear_context -> acceptEdits coupling. Add coverage for all combinations: default (no selection), acceptEdits with clear_context, and bypassPermissions with keep_context. via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run>
There was a problem hiding this comment.
Findings
- [Major]
clear_contextrestart still replays stale queued prompts — the new branch clears Claude's session id, but it enqueues the restart message withunshiftIsolate, which explicitly preserves the rest of the queue. Any user messages queued before approval will be replayed into the fresh session immediately after the restart prompt, so old-context instructions can still leak through. Evidencecli/src/claude/utils/permissionHandler.ts:366,cli/src/claude/utils/permissionHandler.ts:378,cli/src/utils/MessageQueue2.ts:181.
Suggested fix:if (implementationMode === 'clear_context') { this.session.queue.pushIsolateAndClear(restartPrompt, restartMode) } else { this.session.queue.unshiftIsolate(restartPrompt, restartMode) }
Summary
Review mode: follow-up after new commits
- 1 major issue remains: the new
clear_contextpath clears Claude's session id, but it still preserves already-queued prompts, so stale pre-approval messages can bleed into the fresh-context restart.
Testing
- Not run (automation; PR code not executed during review)
HAPI Bot
| implementationMode, | ||
| permissionMode | ||
| }); | ||
| this.session.queue.unshiftIsolate(restartPrompt, restartMode); |
There was a problem hiding this comment.
[MAJOR] clear_context is supposed to start from a fresh Claude session, but this branch still uses unshiftIsolate(), and that helper explicitly preserves the rest of the queue (cli/src/utils/MessageQueue2.ts:181). If the user sent anything while the plan approval was pending, those prompts will be replayed immediately after the restart prompt in the new session, so stale old-context instructions survive the restart.
Suggested fix:
if (implementationMode === 'clear_context') {
this.session.queue.pushIsolateAndClear(restartPrompt, restartMode)
} else {
this.session.queue.unshiftIsolate(restartPrompt, restartMode)
}
Summary
PLAN_FAKE_RESTARTwith full mode snapshot (permissionMode,model,effort,appendSystemPrompt) so agent continues correctly after plan approvalimplementationModesupport (keep_context/clear_context): when user approves with "clear context", clear session and requeue the approved plan for a fresh-context restartdecisionfield to permission responses, normalize invalid post-plan modes (e.g.plan→default), and propagateimplementationModethrough the full stack (CLI → Hub → Web)ExitPlanModeFootercomponent with mode selector and permission controls; polish tool card rendering for plan approval flowPermissionHandler,Session.clearSessionId,toolResultPermissions,reducerTimeline,normalizeAgent,ExitPlanModeFooter,ToolCard, andExitPlanModeViewTest plan
permissionHandler.test.ts: exit_plan_mode defaults tokeep_context,clear_contextclears session, invalid modes normalize todefaultsession.test.ts:clearSessionIdresets session IDtoolResultPermissions.test.ts: permission metadata extraction from tool resultsreducerTimeline.test.ts:implementationModepreserved through reducernormalizeAgent.test.ts:parentUUIDpropagationExitPlanModeFooter.test.tsx: mode selector UI and approval flowToolCard.test.tsx: exit-plan-mode card renderingExitPlanModeView.test.tsx: view rendering with implementation modepermissions.test.ts(hub): permission route metadata handlingsessionModel.test.ts(hub): agent state normalization🤖 Generated with Claude Code
via HAPI