fix(chat): keep a chat's permission mode when it is forked or recovered

Closed
HA
2d ago
+135-7

Links

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:

  • forkSession created the branch with resume and no modeId. rewindSession already 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 replacesSessionId but 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: pass live.state.modeId when the branch uses the same harness.
  • commands.createSession: when modeId is absent and resume.replacesSessionId points to a session of the same harness and scope, use snapshot.session.modeId from 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.
  • savedChatMode moves from ChatPane/utils to usePaneRegistry/utils, because a hook outside ChatPane now 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:

PathBeforeAfter
Fork: load the CLI session, fork(), start the branch, then touch fork-*.txtbranch in default, "Run this command?"branch in bypassPermissions, file created
Recovery: real ChatRuntime, new chat, touch, closeSession, createSession with replacesSessionId and no mode, then touchreplacement 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:

PathmainThis 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 workspaceAsk for approval, "Run this command?"Full access, no prompt
CLI mode → Fork into chat, from claude --dangerously-skip-permissionsAsk for approval, "Run this command?"Full access, no prompt
Restart with #9354 and this PR applied togetherFull access, no prompt
A chat that already fell back to Ask on mainStays 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 test in packages/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.
  • bun test src/renderer/routes/_authenticated/_dashboard/v2-workspace in apps/desktop: 1187 pass, 0 fail
  • bun run typecheck in packages/chat-runtime and apps/desktop: clean
  • bunx biome check on 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 default before this ships has default in 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.