Skip to content

Repair persisted session restore snapshots - #6692

Closed
lawrencecchen wants to merge 9 commits into
mainfrom
task-session-restore-snapshot-repair
Closed

lawrencecchen wants to merge 9 commits into
mainfrom
task-session-restore-snapshot-repair

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Jun 23, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Repairs decoded app session snapshots before restore uses them.
  • Drops poisoned agent-hook bindings shaped like shell-wrapper bash resume ... commands.
  • Strips untrusted persisted agent launch captures and recovers the working directory from the terminal, panel, or workspace snapshot.
  • Supersedes Drop poisoned shell-wrapper restore commands #6012 with a targeted branch on latest main.

Testing

  • swift test --package-path Packages/macOS/CmuxWorkspaces passed, 91 Swift Testing tests.
  • ./scripts/reload-cloud.sh --tag srfix passed, installed cmux DEV srfix.app locally.
  • Focused app-host xcodebuild test was not run locally because the repo guard blocks local cmux app-host tests by default to avoid stealing focus and socket pollution.

Notes

  • Two-commit regression structure: first commit adds failing persistence tests, second commit adds the repair.

View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


Note

Medium Risk
Changes session restore and resume command generation from persisted state; mistakes could drop valid bindings or launch captures, but behavior is guarded by tests and read-only inspection paths.

Overview
Adds an on-load repair pass for app session snapshots: SessionSnapshotRepository accepts repairLoadedSnapshot, runs it after decode, and persists cleaned JSON on normal load / startup paths while loadOutcome stays read-only.

SessionSnapshotRepairer walks terminals and drops poisoned agent-hook resume bindings (shell resume wrappers for built-in kinds, via trustedForSessionRestore) and untrusted agent launch captures, then recovers workingDirectory from terminal/panel/workspace when a bad capture is stripped.

Launch-capture trust is centralized on SessionRestorableAgentSnapshot: resume/fork use trusted captures only; nil-launcher paths validate via nativeProcessDescribesKind (new Hermes and acli → rovodev aliases) and reject shell-wrapper argv. Restore wiring uses the same trust in Workspace and AppDelegate’s snapshot store.

Regression tests cover repository repair persistence, binding poisoning, and resume-command behavior.

Reviewed by Cursor Bugbot for commit 90ab037. Bugbot is set up for automated code reviews on this repo. Configure here.


Summary by cubic

Repairs persisted session snapshots on load and writes back the cleaned version when using store load paths. Inspection loads remain read‑only. Blocks poisoned agent-hook resumes, tightens launch-capture trust (incl. nil-launcher Hermes and acli→rovodev), and restores the correct working directory.

  • Bug Fixes
    • Add repair hook in SessionSnapshotRepository wired to AppSessionSnapshot.repairLoadedSessionSnapshot via SessionSnapshotRepairer; persist cleaned snapshots on load/startup while keeping loadOutcome read‑only.
    • Tighten launch-capture trust: validate via launcher or native process; reject shell‑wrapper argv; recognize nil‑launcher Hermes and acli→rovodev.
    • Drop poisoned shell agent-hook resume bindings, including registry-owned built‑ins; keep valid custom agent‑named wrappers.
    • Recover and normalize agent workingDirectory from terminal/panel/workspace; apply trustedForSessionRestore in Workspace; add Swift Testing regressions.

Written for commit 90ab037. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Bug Fixes
    • Session snapshots are now validated and automatically repaired when loading saved sessions
    • Session restoration now filters out corrupted or untrusted configuration to prevent resume issues
    • Improved robustness of saved session recovery and restoration

@vercel

vercel Bot commented Jun 23, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Canceled Canceled Jun 23, 2026 3:53pm

@coderabbitai

coderabbitai Bot commented Jun 23, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a session snapshot repair pipeline that detects and removes shell-wrapper-poisoned agent-hook resume bindings on snapshot load. Introduces trust-filtering properties and a repair traversal (SessionSnapshotRepairer) that normalizes working directories and strips untrusted bindings, then persists the repaired snapshot via an injectable callback added to SessionSnapshotRepository.

Changes

Session Snapshot Repair Pipeline

Layer / File(s) Summary
Resume binding trust detection and CWD guard
Sources/SessionPersistence.swift
Adds trustedForSessionRestore and isPoisonedAgentHookShellWrapperResume to identify shell-wrapper-poisoned agent-hook bindings via command tokenization. Adds SurfaceResumeCommandCanonicalizer.commandStartIndexAfterCwdGuard to derive effective command start position past cd / {...} && guards.
Agent snapshot trust validation and repair
Sources/RestorableAgentSession.swift
Moves trustedLaunchCommand(_:kind:) into SessionRestorableAgentSnapshot, adds trustedLaunchCommandForSessionRestore, repairedForSessionRestore(fallbackWorkingDirectory:), and normalizedWorkingDirectory. Updates resumeCommand and resumeStartupCommand to use the trusted launch command. Applies the trusted command during index loading.
Workspace session-restore binding gate
Sources/Workspace.swift
resumeBindingForSessionRestore now unwraps binding?.trustedForSessionRestore before agent-hook matching, so untrusted bindings yield nil at the restore site.
Snapshot repair traversal and entry point
Sources/SessionPersistence.swift
Adds AppSessionSnapshot.repairLoadedSessionSnapshot and SessionSnapshotRepairer, which traverse windows → workspaces → panels to replace untrusted resume bindings and call repairedForSessionRestore on agent instances, tracking whether any repair occurred.
SessionSnapshotRepository injectable repair callback
Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Session/SessionSnapshotRepository.swift
Adds a stored repairLoadedSnapshot callback, updates the public initializer to accept it (default no-op), and invokes it in loadOutcome(fileURL:), persisting the repaired snapshot back to disk when didRepair is true.
AppDelegate wiring and tests
Sources/AppDelegate.swift, cmuxTests/SessionPersistenceTests.swift
Wires AppSessionSnapshot.repairLoadedSessionSnapshot into the SessionSnapshotRepository initializer. Tests add a makeSnapshot(workspaceSnapshot:) fixture builder and assert that poisoned shell-wrapper resume bindings are removed both in memory and in the persisted JSON after a save/load cycle.

Sequence Diagram(s)

sequenceDiagram
  participant AppDelegate
  participant SessionSnapshotRepository
  participant AppSessionSnapshot
  participant SessionSnapshotRepairer
  participant FileManager

  AppDelegate->>SessionSnapshotRepository: init(repairLoadedSnapshot: AppSessionSnapshot.repairLoadedSessionSnapshot)
  SessionSnapshotRepository->>FileManager: read JSON from fileURL
  FileManager-->>SessionSnapshotRepository: raw data
  SessionSnapshotRepository->>SessionSnapshotRepository: JSON decode → SnapshotValue
  SessionSnapshotRepository->>AppSessionSnapshot: repairLoadedSessionSnapshot(snapshot)
  AppSessionSnapshot->>SessionSnapshotRepairer: repair(snapshot)
  SessionSnapshotRepairer->>SessionSnapshotRepairer: traverse windows → workspaces → panels
  SessionSnapshotRepairer->>SessionSnapshotRepairer: trustedForSessionRestore (drop poisoned bindings)
  SessionSnapshotRepairer->>SessionSnapshotRepairer: repairedForSessionRestore (normalize CWD)
  SessionSnapshotRepairer-->>AppSessionSnapshot: (repairedSnapshot, didRepair: true)
  AppSessionSnapshot-->>SessionSnapshotRepository: (repairedSnapshot, didRepair: true)
  SessionSnapshotRepository->>FileManager: save(repairedSnapshot, to: fileURL)
  SessionSnapshotRepository-->>AppDelegate: .loaded(repairedSnapshot)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • manaflow-ai/cmux#5937: Introduces AgentLaunchCaptureTrust to strip shell-wrapper and cross-agent launch captures in RestorableAgentSessionIndex.load, which this PR directly extends with snapshot-level trust validation and repair.
  • manaflow-ai/cmux#6030: Introduces the SessionSnapshotRepository/AppDelegate snapshot-persistence wiring that this PR extends with the injectable repairLoadedSnapshot callback and persist-on-repair behavior.
  • manaflow-ai/cmux#6582: Adds a repair hook for stale resume executable paths via SurfaceResumeCommandCanonicalizer, which shares the same repair-pipeline entry point (AppSessionSnapshot.repairLoadedSessionSnapshot) introduced in this PR.

Poem

🐇 Hop, hop — a snapshot loaded with a shell-wrapper lie,
But now the repairer sniffs each command on the fly.
Poisoned resume? Nil it goes, CWD set right,
The JSON rewrites itself before morning's light.
Trust is earned token by token, dear terminal friend,
No sneaky bash --resume shall corrupt my blend! 🥕


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (6 errors, 1 warning)

Check name Status Explanation Resolution
Cmux Swift Actor Isolation ❌ Error The PR introduces a caseless enum SessionSnapshotRepairer used purely as a static-function namespace (Sources/SessionPersistence.swift), violating the coding guideline that disallows this pattern... Make SessionSnapshotRepairer a private struct with instance methods and didRepair property, as suggested in the review comment, following the no-ambient-global-state guidance in swift-actor-isolation.md.
Cmux Cache Substitution Correctness ❌ Error PR uses persisted cached values (launchCommand, workingDirectory) in snapshot restore paths without handling stale cache scenarios. Three unresolved review comments identify: (1) stale launchComman... Apply the three proposed fixes from review comments: (1) use trustedLaunchCommandForSessionRestore for cwd selection, (2) sanitize launchCommand before Claude derivation, (3) preserve .ignore cwd policy by setting workingDirectory to nil...
Cmux Swift File And Package Boundaries ❌ Error The PR violates the .github/review-bot-rules/no-ambient-global-state.md pattern via a caseless SessionSnapshotRepairer enum used purely as a static function namespace with mutable didRepair s... Refactor SessionSnapshotRepairer from a caseless enum with static funcs to a private struct with mutable didRepair instance property, eliminating the mutable state parameter threading pattern noted in review.
Cmux Full Internationalization ❌ Error PR added 5 new user-facing localized strings (surfaceResumeApproval.runPrompt.title/message/run/skip, surfaceResumeApproval.cwd.none) to Workspace.swift with translations in only 2 of 20 supported... Add translations for all 18 missing locales (ar, bs, da, de, es, fr, it, km, ko, nb, pl, pt-BR, ru, th, tr, uk, zh-Hans, zh-Hant) to the 5 new string catalog entries.
Cmux Architecture Rethink ❌ Error PR violates .github/review-bot-rules/swift-architectural-rethink.md: SessionSnapshotRepairer uses caseless enum as static-function namespace threading mutable didRepair state via inout paramete... Refactor SessionSnapshotRepairer from enum to private struct with instance didRepair property, eliminating mutable-state threading through static helpers as required by no-ambient-global-state.md guidelines.
Cmux No Ambient Global State ❌ Error SessionSnapshotRepairer (Sources/SessionPersistence.swift:1958) is a caseless enum used purely as a static-function namespace, violating .github/review-bot-rules/no-ambient-global-state.md. Refactor SessionSnapshotRepairer from a caseless enum to a private instance struct with a mutable didRepair property, as suggested in review comments.
Docstring Coverage ⚠️ Warning Docstring coverage is 5.26% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (18 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Repair persisted session restore snapshots' directly and concisely describes the main change: implementing repair logic for session snapshots during the restore process.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Swift Blocking Runtime ✅ Passed PR introduces no blocking synchronization primitives: all new production code (SessionSnapshotRepository.swift, AppDelegate.swift, RestorableAgentSession.swift, SessionPersistence.swift, Workspace....
Cmux Expensive Synchronous Load ✅ Passed The repair callback performs only in-memory string parsing and object transformations (tokenization, normalization) with no file I/O, syscalls, or directory scans. While called during app startup,...
Cmux No Hacky Sleeps ✅ Passed All changes are Swift code; rule explicitly excludes Swift timing (covered by separate check). No non-Swift runtime code modified.
Cmux Algorithmic Complexity ✅ Passed All code changes comply with algorithmic complexity rules. Nested iterations are bounded by snapshot size, called once per session load (not hot paths), and use O(1) dict lookups. String tokenizati...
Cmux Swift Concurrency ✅ Passed PR introduces no legacy async patterns. New @escaping @Sendable`` callback is synchronous repair logic, not a completion-handler API. Task.detached calls in RestorableAgentSession are immediately a...
Cmux Swift @Concurrent ✅ Passed PR introduces only synchronous repair helpers with proper @Sendable annotations; no new async functions, no improper @concurrent usage, and no concurrent isolation violations detected.
Cmux Swiftpm Lockfiles ✅ Passed PR makes only Swift source code changes for session snapshot repair; no SwiftPM Package.swift dependencies, Xcode project package references, or .gitignore SwiftPM exclusions were modified.
Cmux Swift Logging ✅ Passed The PR adds no logging violations: new repair/validation code in SessionPersistence.swift, RestorableAgentSession.swift, and SessionSnapshotRepository.swift contains zero print/debugPrint/dump/NSLo...
Cmux User-Facing Error Privacy ✅ Passed PR makes internal snapshot repair changes without exposing user-facing errors, alerts, or localized messages; repair silently removes poisoned bindings and recovers working directories without UI i...
Cmux Swiftui State Layout ✅ Passed PR makes only data model and persistence changes. No new SwiftUI state patterns (ObservableObject, @Published, @StateObject, @EnvironmentObject, @Observable, GeometryReader) added. Check does not a...
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR does not introduce or materially change standalone cmux-owned windows. Changes are focused on session snapshot repair logic, trusted launch command validation, and restore binding resolution—no...
Cmux Source Artifacts ✅ Passed All 6 changed files are legitimate hand-written Swift source code and test files with no generated code markers, hidden artifact directories, or build output that would violate source-control-artif...
Cmux No Test Or Debug Seam In Production Source ✅ Passed No test or debug seams were added to production source files. All new members (trustedLaunchCommandForSessionRestore, repairedForSessionRestore, trustedForSessionRestore) are runtime repair functio...
Cmux Hot Path Allocating Formatting ✅ Passed No allocating formatters added to hot paths. The repair logic contains no String(format:) or formatter allocations and runs once per snapshot load (cold path).
Description check ✅ Passed The PR description includes Summary, Testing, and Notes sections covering what changed, why, and testing approach. However, it omits the Demo Video section and most checklist items.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch task-session-restore-snapshot-repair

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Jun 23, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR repairs persisted session snapshots on load by introducing a repairLoadedSnapshot hook in SessionSnapshotRepository that cleans poisoned state and writes the result back. It tightens trustedLaunchCommand to validate nil-launcher captures via nativeProcessDescribesKind (adding Hermes alias and acli→rovodev subcommand mapping), drops poisoned agent-hook resume bindings shaped like bash resume ... for built-in agent kinds, and recovers working directories from terminal/panel/workspace context.

  • SessionSnapshotRepository gains a repairLoadedSnapshot: @Sendable parameter; load() and loadStartupSnapshot() persist the repaired snapshot when the callback reports a change, while the public loadOutcome(fileURL:) remains a read-only inspection path.
  • SessionRestorableAgentSnapshot.trustedLaunchCommand is refactored from RestorableAgentSessionIndex (private, launcher-only check) to a public-on-type method that also validates nil-launcher captures against native process metadata, and repairedForSessionRestore recovers workingDirectory from the terminal/panel/workspace fallback when the launch command is stripped.
  • SurfaceResumeBindingSnapshot.isPoisonedAgentHookShellWrapperResume detects agent-hook bindings whose tokenised command begins with a shell executable followed by resume/--resume, excluding custom agents whose kind happens to share a shell name.

Confidence Score: 5/5

Safe to merge — the repair runs only on load, writes back only when it detects a change, and all three code paths (poisoned resume binding, wrong-fork launch capture, CWD recovery) are covered by focused Swift Testing regressions that passed.

The logic changes are tightly scoped: the trust check for nil-launcher captures adds nativeProcessDescribesKind without weakening the existing launcher check, the isPoisonedAgentHookShellWrapperResume guard is carefully bounded (only agent-hook source, only built-in kinds, two-token resume/--resume suffix), and the repairedForSessionRestore CWD repair condition correctly distinguishes agent-owned working directories from those inherited from the stripped launch command. The public loadOutcome method remains read-only so existing crash-diagnostic callers are unaffected, and the write-back is idempotent once a session snapshot has been cleaned.

No files require special attention. The SessionSnapshotRepairer caseless-enum shape was raised in a previous review thread and is the only open style concern.

Important Files Changed

Filename Overview
Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/AgentLaunchCaptureTrust.swift Adds hermes-agent to the native-process alias table and an acli+rovodev-subcommand path inside nativeProcessDescriptors. Logic and table entry are correct.
Sources/SessionPersistence.swift Adds trustedForSessionRestore/isPoisonedAgentHookShellWrapperResume to SurfaceResumeBindingSnapshot, commandStartIndexAfterCwdGuard helper, and the AppSessionSnapshot.repairLoadedSessionSnapshot static wrapper. Bounds checking is correct.
Sources/RestorableAgentSession.swift Moves and expands trustedLaunchCommand to SessionRestorableAgentSnapshot with launcher or native-process check for nil-launcher captures; adds repairedForSessionRestore with correct CWD recovery logic.
Sources/SessionSnapshotRepairer.swift New caseless enum used as a static-only namespace (already raised in previous review thread); the repair walk across windows/workspaces/panels/terminals is correct and properly threads didRepair.
Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Session/SessionSnapshotRepository.swift Adds repairLoadedSnapshot hook with write-back gated by persistRepair; public loadOutcome stays read-only. Default closure is identity so existing callers are unaffected.
Sources/Workspace.swift Applies trustedForSessionRestore at the top of resumeBindingForSessionRestore, correctly returning nil for poisoned bindings. Preserves all previously-passing cases.
cmuxTests/SessionRestoreSnapshotRepairTests.swift New Swift Testing suite covering all repair paths: poisoned binding dropped/persisted, custom-shell-named binding preserved, wrong-fork capture stripped with CWD recovery, Hermes and RovoDev nil-launcher captures preserved.

Reviews (6): Last reviewed commit: "Keep session load outcome read-only" | Re-trigger Greptile

Comment thread Sources/SessionPersistence.swift Outdated
Comment on lines +1958 to +2034
enum SessionSnapshotRepairer {
static func repair(_ snapshot: AppSessionSnapshot) -> (snapshot: AppSessionSnapshot, didRepair: Bool) {
var didRepair = false
var repaired = snapshot
repaired.windows = repaired.windows.map { window in
repair(window, didRepair: &didRepair)
}
return (snapshot: repaired, didRepair: didRepair)
}

private static func repair(
_ window: SessionWindowSnapshot,
didRepair: inout Bool
) -> SessionWindowSnapshot {
var repaired = window
repaired.tabManager.workspaces = repaired.tabManager.workspaces.map { workspace in
repair(workspace, didRepair: &didRepair)
}
return repaired
}

private static func repair(
_ workspace: SessionWorkspaceSnapshot,
didRepair: inout Bool
) -> SessionWorkspaceSnapshot {
var repaired = workspace
repaired.panels = repaired.panels.map { panel in
repair(panel, workspaceDirectory: workspace.currentDirectory, didRepair: &didRepair)
}
return repaired
}

private static func repair(
_ panel: SessionPanelSnapshot,
workspaceDirectory: String,
didRepair: inout Bool
) -> SessionPanelSnapshot {
guard var terminal = panel.terminal else { return panel }
let fallbackWorkingDirectory = firstNormalizedDirectory(
terminal.workingDirectory,
panel.directory,
workspaceDirectory
)

if let resumeBinding = terminal.resumeBinding {
let trustedBinding = resumeBinding.trustedForSessionRestore
if trustedBinding == nil {
didRepair = true
}
terminal.resumeBinding = trustedBinding
}

if let agent = terminal.agent {
let repairedAgent = agent.repairedForSessionRestore(
fallbackWorkingDirectory: fallbackWorkingDirectory
)
if agent.launchCommand != repairedAgent.launchCommand
|| agent.workingDirectory != repairedAgent.workingDirectory {
didRepair = true
}
terminal.agent = repairedAgent
}

var repaired = panel
repaired.terminal = terminal
return repaired
}

private static func firstNormalizedDirectory(_ candidates: String?...) -> String? {
for candidate in candidates {
if let normalized = SurfaceResumeCommandCanonicalizer.normalizedCWD(candidate) {
return normalized
}
}
return nil
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Caseless enum used as static-only namespace

SessionSnapshotRepairer is a caseless enum whose entire public and private surface is static funcs — the canonical shape flagged by cmux-no-ambient-global-state. The same file already exposes the natural seam: AppSessionSnapshot.repairLoadedSessionSnapshot is where callers land, and all the recursive helpers operate exclusively on AppSessionSnapshot and its sub-snapshots. Placing the private static helpers directly inside a private extension AppSessionSnapshot block (alongside the already-existing repairLoadedSessionSnapshot) removes the separate namespace type and keeps all repair logic co-located with the type that owns the data.

Rule Used: Flag new ambient global state in production Swift:... (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Sources/RestorableAgentSession.swift`:
- Around line 792-808: In the repairedForSessionRestore function, add a check to
preserve the `.ignore` cwd semantics by preventing any workingDirectory
backfilling when registration?.cwd is set to .ignore. Before the existing
conditional blocks that assign fallbackWorkingDirectory or
trustedLaunchCommand?.workingDirectory to repaired.workingDirectory, add a guard
condition that checks if registration?.cwd == .ignore and returns early or skips
the workingDirectory assignment entirely. This ensures that when cwd is
explicitly set to ignore, the workingDirectory remains nil throughout the repair
process.
- Around line 1154-1160: The launchCommand sanitization using
trustedLaunchCommand is currently happening after resolvedClaudeWorkflowRecord
has already been invoked, which allows untrusted launch captures to influence
the resolution process before they are stripped. Move the sanitization of
effectiveRecord.launchCommand using
SessionRestorableAgentSnapshot.trustedLaunchCommand before the
resolvedClaudeWorkflowRecord call is made, ensuring the record is sanitized
before any derivation of transcript or session candidates can occur from the
potentially poisoned launch cwd or environment.

In `@Sources/SessionPersistence.swift`:
- Around line 1958-1965: Replace the caseless enum `SessionSnapshotRepairer`
with a private struct or class that owns the `didRepair` state as an instance
property instead of threading it through mutable parameters. Convert the static
repair method to an instance method that modifies the internal didRepair
property, then create an instance of the repairer, call the repair method on it,
and return both the repaired snapshot and the didRepair flag from the instance
property. This eliminates the static namespace pattern and properly encapsulates
the mutable state within the repairer instance.

In `@Sources/Workspace.swift`:
- Around line 901-902: The restore gate at line 901 validates
binding.trustedForSessionRestore to ensure trusted launch data, but the
retargeting fallback logic still reads directly from
restorableAgent.launchCommand?.workingDirectory, which could contain untrusted
persisted launch data. Update the retargeting fallback to use the trusted launch
command information from the binding's trustedForSessionRestore property instead
of directly accessing the untrusted
restorableAgent.launchCommand?.workingDirectory, ensuring all restore decisions
are based on validated trusted launch captures.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1d03160d-b039-49a6-8e97-e4c68fbd8f22

📥 Commits

Reviewing files that changed from the base of the PR and between dd7f7d3 and eda711d.

📒 Files selected for processing (6)
  • Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Session/SessionSnapshotRepository.swift
  • Sources/AppDelegate.swift
  • Sources/RestorableAgentSession.swift
  • Sources/SessionPersistence.swift
  • Sources/Workspace.swift
  • cmuxTests/SessionPersistenceTests.swift

Comment on lines +792 to +808
func repairedForSessionRestore(fallbackWorkingDirectory: String?) -> SessionRestorableAgentSnapshot {
let trustedLaunchCommand = trustedLaunchCommandForSessionRestore
var repaired = self
repaired.launchCommand = trustedLaunchCommand

let fallbackWorkingDirectory = Self.normalizedWorkingDirectory(fallbackWorkingDirectory)
if trustedLaunchCommand == nil {
if repaired.workingDirectory == nil
|| Self.normalizedWorkingDirectory(repaired.workingDirectory)
== Self.normalizedWorkingDirectory(launchCommand?.workingDirectory) {
repaired.workingDirectory = fallbackWorkingDirectory
}
} else if repaired.workingDirectory == nil {
repaired.workingDirectory = Self.normalizedWorkingDirectory(
trustedLaunchCommand?.workingDirectory
) ?? fallbackWorkingDirectory
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Preserve .ignore cwd semantics during repair.

When registration?.cwd == .ignore, this repair path can still backfill workingDirectory from the trusted launch command or panel/workspace fallback. That reintroduces a cwd for agents whose restore policy explicitly suppresses one.

🐛 Proposed fix
         let trustedLaunchCommand = trustedLaunchCommandForSessionRestore
         var repaired = self
         repaired.launchCommand = trustedLaunchCommand
+
+        if registration?.cwd == .ignore {
+            repaired.workingDirectory = nil
+            return repaired
+        }
 
         let fallbackWorkingDirectory = Self.normalizedWorkingDirectory(fallbackWorkingDirectory)
         if trustedLaunchCommand == nil {

Based on learnings, registrations with cwd: .ignore must keep the resume working directory nil so both the cwd guard and terminal placement cwd are suppressed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/RestorableAgentSession.swift` around lines 792 - 808, In the
repairedForSessionRestore function, add a check to preserve the `.ignore` cwd
semantics by preventing any workingDirectory backfilling when registration?.cwd
is set to .ignore. Before the existing conditional blocks that assign
fallbackWorkingDirectory or trustedLaunchCommand?.workingDirectory to
repaired.workingDirectory, add a guard condition that checks if
registration?.cwd == .ignore and returns early or skips the workingDirectory
assignment entirely. This ensures that when cwd is explicitly set to ignore, the
workingDirectory remains nil throughout the repair process.

Source: Learnings

Comment thread Sources/RestorableAgentSession.swift
Comment thread Sources/SessionPersistence.swift Outdated
Comment on lines +1958 to +1965
enum SessionSnapshotRepairer {
static func repair(_ snapshot: AppSessionSnapshot) -> (snapshot: AppSessionSnapshot, didRepair: Bool) {
var didRepair = false
var repaired = snapshot
repaired.windows = repaired.windows.map { window in
repair(window, didRepair: &didRepair)
}
return (snapshot: repaired, didRepair: didRepair)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Avoid adding a static namespace repairer.

SessionSnapshotRepairer is a new caseless enum used purely as a static-function namespace. Make it a private instance repairer that owns didRepair instead of threading mutable state through static helpers.

♻️ Suggested shape
 extension AppSessionSnapshot {
     static func repairLoadedSessionSnapshot(
         _ snapshot: AppSessionSnapshot
     ) -> (snapshot: AppSessionSnapshot, didRepair: Bool) {
-        SessionSnapshotRepairer.repair(snapshot)
+        var repairer = SessionSnapshotRepairer()
+        return repairer.repair(snapshot)
     }
 }
 
-enum SessionSnapshotRepairer {
-    static func repair(_ snapshot: AppSessionSnapshot) -> (snapshot: AppSessionSnapshot, didRepair: Bool) {
-        var didRepair = false
+private struct SessionSnapshotRepairer {
+    private var didRepair = false
+
+    mutating func repair(_ snapshot: AppSessionSnapshot) -> (snapshot: AppSessionSnapshot, didRepair: Bool) {
         var repaired = snapshot
         repaired.windows = repaired.windows.map { window in
-            repair(window, didRepair: &didRepair)
+            repair(window)
         }
         return (snapshot: repaired, didRepair: didRepair)
     }
 
-    private static func repair(
-        _ window: SessionWindowSnapshot,
-        didRepair: inout Bool
-    ) -> SessionWindowSnapshot {
+    private mutating func repair(_ window: SessionWindowSnapshot) -> SessionWindowSnapshot {
         var repaired = window
         repaired.tabManager.workspaces = repaired.tabManager.workspaces.map { workspace in
-            repair(workspace, didRepair: &didRepair)
+            repair(workspace)
         }
         return repaired
     }
 }

As per coding guidelines, .github/review-bot-rules/no-ambient-global-state.md disallows a caseless enum/empty struct used purely as a static func namespace in production Swift.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/SessionPersistence.swift` around lines 1958 - 1965, Replace the
caseless enum `SessionSnapshotRepairer` with a private struct or class that owns
the `didRepair` state as an instance property instead of threading it through
mutable parameters. Convert the static repair method to an instance method that
modifies the internal didRepair property, then create an instance of the
repairer, call the repair method on it, and return both the repaired snapshot
and the didRepair flag from the instance property. This eliminates the static
namespace pattern and properly encapsulates the mutable state within the
repairer instance.

Source: Coding guidelines

Comment thread Sources/Workspace.swift
Comment on lines +901 to +902
guard let binding = binding?.trustedForSessionRestore else { return nil }
guard binding.isAgentHookBinding, let restorableAgent else {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Use the trusted launch cwd in this restore gate too.

Line 901 filters the binding, but the later retargeting fallback still reads restorableAgent.launchCommand?.workingDirectory. If an older or unrepaired snapshot reaches this path, an untrusted persisted launch capture can still retarget the binding cwd.

🐛 Proposed fix
         // Restore has no live hook cwd; use the snapshot's derived restorable cwd
         // and fall back to launch capture only for older snapshots.
         let snapshotRestorableWorkingDirectory =
-            restorableAgent.workingDirectory ?? restorableAgent.launchCommand?.workingDirectory
+            restorableAgent.workingDirectory
+                ?? restorableAgent.trustedLaunchCommandForSessionRestore?.workingDirectory

This follows the PR objective to strip untrusted persisted agent launch captures from restore decisions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/Workspace.swift` around lines 901 - 902, The restore gate at line 901
validates binding.trustedForSessionRestore to ensure trusted launch data, but
the retargeting fallback logic still reads directly from
restorableAgent.launchCommand?.workingDirectory, which could contain untrusted
persisted launch data. Update the retargeting fallback to use the trusted launch
command information from the binding's trustedForSessionRestore property instead
of directly accessing the untrusted
restorableAgent.launchCommand?.workingDirectory, ensuring all restore decisions
are based on validated trusted launch captures.

Comment thread Sources/SessionPersistence.swift

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 90ab037. Configure here.

Comment thread Sources/Workspace.swift
) -> SurfaceResumeBindingSnapshot? {
guard let binding, binding.isAgentHookBinding, let restorableAgent else {
guard let binding = binding?.trustedForSessionRestore else { return nil }
guard binding.isAgentHookBinding, let restorableAgent else {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Untrusted launch cwd in retarget

Medium Severity

resumeBindingForSessionRestore still derives snapshotRestorableWorkingDirectory from the raw persisted launchCommand when the agent’s workingDirectory is missing, while resume/fork paths in the same change now use trustedLaunchCommandForSessionRestore. Snapshots that never pass load-time repair (e.g. closed-panel history) can retarget agent-hook bindings using a foreign launch capture’s cwd even though agent resume commands ignore that capture.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 90ab037. Configure here.

@lawrencecchen lawrencecchen added the stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening. label Sep 23, 2026
@github-project-automation github-project-automation Bot moved this from Todo to Done in cmux backlog Sep 23, 2026

This branch was successfully deployed

1 active deployment
Preview – cmux — 90ab0376 Deployed Jun 23, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants