fix(chat): keep a chat's permission mode when it is forked or recovered
Links
- Companion to #9354 (records the mode on the pane). This PR covers the paths #9354 does not.
- Investigation: https://app.superset.sh/page/pr-9354-vs-run-this-command-in-full-access-3y2y6b
Summary
- Fork: a chat that is forked ("Branch" in a chat, or "Fork into chat" from a terminal) now starts in the mode its source was running in, not in Ask for approval.
- Recovery: a chat that is recovered after a host or app restart, an account switch, or a return from the terminal now resumes in the mode the host journal recorded for it, when the client sends no mode.
- Together with #9354, a user in Full access no longer sees "Run this command?" on ordinary commands after any of these paths.
Why / Context
A user who runs in Full access reported that the chat still asks "Run this command?". That heading is the generic approval card for an execute tool (ApprovalRow.tsx:92-93). In true Full access, Claude Code asks only for a few dangerous commands, for example removing the working directory. Most of the asks come from chats that silently fall back to default (Ask for approval).
The adapter on purpose does not apply the harness default on a resume (acpAdapter.ts startModeId). That is correct for an unknown session. But these paths resumed sessions whose mode is known and did not pass it:
forkSessioncreated the branch withresumeand nomodeId.rewindSessionalready passes it.- "Fork into chat" from a terminal loads the CLI session into a hidden chat with no mode, so the source of the fork was always
default. - Recovery sends
replacesSessionIdbut no mode for any pane that never recorded one (every pane before #9354, and every client that does not record it).
How It Works
commands.forkSession: passlive.state.modeIdwhen the branch uses the same harness.commands.createSession: whenmodeIdis absent andresume.replacesSessionIdpoints to a session of the same harness and scope, usesnapshot.session.modeIdfrom the replay the host already builds to carry a failed turn. A mode the client sends still wins.useSurfaceFork: the hidden loader chat starts in the mode a new chat for that agent gets (readSavedChatMode(presetId) ?? acpStartModes(presetId).defaultModeId). This grants no more access than opening a new chat for that agent.savedChatModemoves fromChatPane/utilstousePaneRegistry/utils, because a hook outsideChatPanenow reads it (AGENTS.md co-location rule).
No tRPC input or output changes. Released desktop builds get the host-side fixes (Branch, recovery) without an update. "Fork into chat" from a terminal needs this desktop build.
Manual QA
End-to-end with the real Claude Code CLI 2.1.311 through claude-agent-acp 0.85.0 (haiku), in a cloud sandbox with proxy credentials. The script declined every approval:
| Path | Before | After |
|---|---|---|
Fork: load the CLI session, fork(), start the branch, then touch fork-*.txt | branch in default, "Run this command?" | branch in bypassPermissions, file created |
Recovery: real ChatRuntime, new chat, touch, closeSession, createSession with replacesSessionId and no mode, then touch | replacement in default, "Run this command?" (touch base-2.txt) | replacement in bypassPermissions, file created |
Desktop dev app (bun run dev, driven with real input over CDP). A Claude chat with Chat on, with the same steps on main and on this branch:
| Path | main | This PR |
|---|---|---|
| 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 |
| Restart with #9354 and this PR applied together | Full access, no prompt | |
A chat that already fell back to Ask on main | Stays in Ask, as expected |
No render-loop errors in the app log. Screenshots: https://app.superset.sh/page/pr-9354-vs-run-this-command-in-full-access-3y2y6b
Merge note: this PR and #9354 both touch the import block of ChatPane.tsx. Whichever merges second gets a one-line conflict: keep sessionInfoPaneData from ./utils/sessionInfoPaneData, and saveChatMode from ../../utils/savedChatMode.
Testing
bun testinpackages/chat-runtime: 453 pass, 0 fail- New:
forkSession starts the branch in the source's mode - New:
a 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) - Both new tests fail without the change.
- New:
bun test src/renderer/routes/_authenticated/_dashboard/v2-workspaceinapps/desktop: 1187 pass, 0 failbun run typecheckinpackages/chat-runtimeandapps/desktop: 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. Picking Full access once fixes that chat.
- 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.
Risks / Rollback
- Risk: a recovered chat can come back in Full access where it previously came back in Ask. That happens only when the host journal recorded Full access for that same chat, so it is the mode the user was already running in.
- Rollback: revert the two commits. No data or schema changes.
Summary by cubic
Keeps a chat's permission mode when it is forked or recovered, fixing users in Full access seeing "Run this command?" on ordinary commands after branching, restarting, switching accounts, or returning from a terminal.
Host-side changes now pass the source chat's mode when forking a branch, and resume a recovered chat in the mode its journal records when the client sends none. Forking from a terminal starts the hidden chat in the mode a new chat for that agent would get.
- Forked branches inherit the source's mode only when both use the same harness; an explicit mode still wins on recovery.
- The Branch and recovery fixes apply to released desktop builds without an update; terminal-to-chat forking requires this build.
Summary by CodeRabbit
- Bug Fixes
- Chat forks now retain the source chat’s mode when using the same chat type. Replacing a chat preserves its mode by default, while an explicitly selected mode takes precedence.
- Hidden chat sessions used for forks now use the saved mode when available, falling back to the preset’s default.
- Forks across different chat types do not carry over the source mode.