fix(chat): keep a chat's permission mode across resume, fork and recovery
Links
- Investigation and screenshots: https://app.superset.sh/page/pr-9354-vs-run-this-command-in-full-access-3y2y6b
- Folds in #9358, which is closed.
Summary
- A chat in Full access (the default) no longer drops to Ask for approval when it is resumed, branched, or forked from a terminal. In that state, every command that changes something showed "Run this command?". That is the prompt reported in Slack by a user who always runs with skip permissions.
- Pane: the pane now records the mode the session reports, so a resume asks for it again.
- Host: forks carry the source's mode, and a recovered chat falls back to its own journaled mode when the client sends none.
Why / Context
"Run this command?" is the generic approval card for an execute tool (ApprovalRow.tsx). It appears whenever the agent asks, and in Full access Claude Code asks only for a few dangerous commands. Most asks came from chats that silently restarted in default.
The adapters deliberately do not apply the harness default on a resume with no mode (acpAdapter.ts startModeId; native Claude claudeMode(undefined) → default). That is right for an unknown session. But these paths resumed sessions whose mode was known, and did not pass it:
chatModeIdwas written to the pane only when the user picked a mode, so recovery (host or app restart, account switch, return from the terminal) sent no mode.forkSessioncreated the branch withresumeand nomodeId.rewindSessionalready passed it.- "Fork into chat" from a terminal loads the CLI session into a hidden chat with no mode, so the fork's source was always
default.
How It Works
- Pane records the mode:
AcpChatPanepassesstate.modeIdthroughonSessionInfo, andChatPanestores it aschatModeId(sessionInfoFromState,sessionInfoPaneData). - Host recovery: in
commands.createSession, whenmodeIdis absent andresume.replacesSessionIdpoints to a session of the same harness and scope, the host uses that session's journaledsnapshot.session.modeId. It takes it from the replay it already builds to carry a failed turn. This also fixes panes created before this change, and any other client. A mode the client sends still wins. - Fork:
commands.forkSessionpasseslive.state.modeIdwhen the branch uses the same harness. - Fork into chat: in
useSurfaceFork, the hidden loader chat starts in the mode a new chat for that agent gets (readSavedChatMode(presetId) ?? acpStartModes(presetId).defaultModeId). That is no more access than opening a new chat. - New chats never start in Plan: "Start a new chat" (recovery and failure cards) starts in the pane's mode, but not in Plan, and falls back to the saved per-agent mode (
freshStartModenext toNEVER_STARTING_MODES). A resume still keeps the exact mode. - Mobile: resuming an ended chat from the new-session sheet now passes
replacesSessionId, so the host restores the chat's journaled mode instead of starting it in Ask. - Folder move:
savedChatModemoves tousePaneRegistry/utils, since a hook outsideChatPanenow reads it. The persisted-key registry is updated to match.
No tRPC input or output changes. Released desktop builds get the host-side parts (recovery, Branch) without an update.
Manual QA
Done in the desktop dev app (bun run dev, driven with real input over CDP): a Claude chat with Chat on, the same steps on main and on this branch, each followed by a touch command.
| Path | main | This PR |
|---|---|---|
| New chat | Full access, no prompt | same |
| Quit and relaunch the app (pane recovers) | Ask for approval, "Run this command?" | Full access, no prompt. This held even for a chat created on main that never stored a mode. |
| Reply menu → Branch in this workspace | Ask for approval, "Run this command?" | Full access, no prompt |
CLI mode → Fork into chat, from claude --dangerously-skip-permissions | Ask for approval, "Run this command?" | Full access, no prompt |
A chat that already fell back to Ask on main | Stays in Ask (its journal says Ask) | |
| Chat in Plan, its session continued in a terminal, app restarted → "Start a new chat" | New chat starts in Plan | New chat starts in Full access, touch runs with no prompt |
Mobile was checked at the host, not in the app: the exact call sequence NewSessionSheet.resume() makes (close, then createSession with resume) ran against a real ChatRuntime with the real Claude CLI. Without replacesSessionId the chat came back in default and asked to approve touch. With it, bypassPermissions, and the file was created. Not run on an iOS simulator.
No render-loop errors in the app log. The same paths were also checked against the real Claude CLI through claude-agent-acp and a real ChatRuntime, outside the app.
Testing
bun testinpackages/chat-runtime: 453 pass, 0 fail. New tests, each of which fails without its change:forkSession starts the branch in the source's modea replacement that asks for no mode resumes in the mode the chat it replaces was in(also covers: an explicit mode wins, and a different harness does not inherit)
bun testinapps/desktop: 9385 pass, 0 fail, includingsessionInfoFromState,sessionInfoPaneDataandfreshStartModebun testinapps/mobile: 256 pass, 0 fail.apps/mobiletypecheck has 187 errors before and after this change (pre-existing, none on the changed lines)bun run typecheckin both packages: cleanbunx biome checkon the changed directories: clean
Known Limitations
- A chat opened from history (native agent sessions, with no host journal) still resumes in Ask for approval. Its mode is truly unknown, so the no-escalation rule applies.
- A chat that already fell back to
defaultbefore this ships hasdefaultin its journal, so it stays in Ask until the user picks Full access once. - AskUserQuestion, plan approval, and Claude Code's own safety checks (for example, deleting the working directory) still ask in Full access. That is expected.
Risks / Rollback
- Risk: a recovered chat can come back in Full access where it used to come back in Ask. This happens only when that same chat last ran in Full access, either as recorded on the pane or in the host journal.
- Rollback: revert the PR. No data or schema changes.
Summary by CodeRabbit
- Improvements
- Chat sessions retain their selected mode alongside session details. Titles and session identifiers update only when new information is available, avoiding unnecessary changes.
- Forked chats and replacement sessions preserve the source mode when applicable, while explicit mode selections take precedence. New chats also use the saved mode when the pane’s mode is unset or unsuitable for starting a session.
- Resuming an ended chat now carries forward the prior session identifiers, keeping the resume flow connected to the replaced chat.