Skip to content

Drop poisoned shell-wrapper restore commands#6012

Open
lawrencecchen wants to merge 3 commits into
mainfrom
issue-session-restore-resume-command
Open

Drop poisoned shell-wrapper restore commands#6012
lawrencecchen wants to merge 3 commits into
mainfrom
issue-session-restore-resume-command

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes a real session restoration failure seen after upgrading from 0.64.14 to 0.64.15: persisted agent-hook resume bindings shaped like bash resume <session> -c ... could still be trusted before the restored agent snapshot, producing bash: resume: No such file or directory.

This PR now repairs persisted session snapshots at load time for all users. Old snapshots are normalized before restore sees them, poisoned shell-wrapper bindings are dropped, untrusted saved agent launch captures are stripped, and recovered agent working directories fall back to the terminal/panel/workspace directory. When a repair happens, the cleaned snapshot is saved back to disk so the upgrade is durable.

Verification

  • xcodebuild test -project cmux.xcodeproj -scheme cmux-unit -destination 'platform=macOS' -derivedDataPath /tmp/cmux-rsres-green -only-testing:cmuxTests/SessionPersistenceTests/testLoadRepairsAndPersistsPoisonedAgentHookShellWrapperResumeBinding -only-testing:cmuxTests/SessionPersistenceTests/testLoadRepairsWrongForkAgentLaunchCommandAndRecoversWorkingDirectory -only-testing:cmuxTests/SessionPersistenceTests/testRestoreDropsPoisonedAgentHookShellWrapperResumeBindingAndUsesAgentSnapshot -only-testing:cmuxTests/SessionPersistenceTests/testRestoreDropsPoisonedAgentHookShellWrapperResumeBindingWithoutAgentSnapshot -only-testing:cmuxTests/SocketListenerAcceptPolicyTests/testPersistedAgentSnapshotDropsShellWrapperLaunchCommandWhenRenderingResume
  • ./scripts/reload-cloud.sh --tag rsfix
  • Launched tagged app via http://127.0.0.1:17320/rsfix, confirmed the rsfix debug socket responds with list-workspaces, and visually confirmed the app rendered.

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

Summary by CodeRabbit

  • New Features

    • Session loading now auto-repairs snapshots and prefers trusted resume commands, persisting repaired snapshots back to disk.
  • Bug Fixes

    • Improved session restoration to detect and drop “poisoned” shell-wrapper resume bindings and prefer direct resume invocations, preventing problematic command reinvocation and more reliable recovery.
  • Tests

    • Added regression tests covering poisoned shell-wrapper resume bindings, repair behavior, and resume-command rendering.

@vercel

vercel Bot commented Jun 12, 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 12, 2026 10:50pm
cmux-staging Building Building Preview, Comment Jun 12, 2026 10:50pm

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds trust gating that rejects shell-wrapper and poisoned agent-hook resume captures, uses only trusted launch captures when reconstructing resume/fork/startup commands and working directories, repairs loaded snapshots to drop untrusted resume bindings, and updates command-parsing helpers and tests.

Changes

Trust filtering for shell-wrapper removal in session restore

Layer / File(s) Summary
Surface resume binding trust & canonicalization helpers
Sources/SessionPersistence.swift
SurfaceResumeBindingSnapshot.trustedForSessionRestore + isPoisonedAgentHookShellWrapperResume and SurfaceResumeCommandCanonicalizer.commandStartIndexAfterCwdGuard(_:) detect and gate poisoned shell-wrapper agent-hook resume bindings.
Restorable agent: trust gating for launch command use
Sources/RestorableAgentSession.swift
Adds trustedLaunchCommandForSessionRestore; resumeCommand and forkCommand now use the trusted capture; repairedForSessionRestore(fallbackWorkingDirectory:) and normalizedWorkingDirectory(_:) updated to prefer trusted capture's launchCommand and workingDirectory for repairs and fallback logic; resumeStartupCommand prefers trusted capture cwd for post-exit launcher prefix.
Snapshot repair and persisted-rewrite on load
Sources/SessionPersistence.swift
Introduces SessionSnapshotRepairer that walks windows→workspaces→panels, drops untrusted resumeBinding, repairs terminal agent snapshots via repairedForSessionRestore, and causes SessionPersistenceStore.loadOutcome(fileURL:) to persist the repaired snapshot when changes were made.
Workspace parsing refactor and terminal restore integration
Sources/Workspace.swift
Generalizes command-start indexing with surfaceResumeCommandStartIndexAfterCwdGuard(_:), refactors resume-word slicing to use it, and switches terminal restore to consult snapshot.terminal?.resumeBinding?.trustedForSessionRestore.
Regression tests for poisoned wrapper cases
cmuxTests/SessionPersistenceTests.swift
Adds tests that persist poisoned bash-wrapper resume bindings and assert loader repairs and persisted JSON rewrite, plus tests verifying resume rendering drops shell-wrapper fragments and restore-time behavior with/without restorable agent snapshots.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • manaflow-ai/cmux#5937: Both PRs implement distrust/filtering of shell-wrapper and cross-agent captured launch data in resume/fork rendering.
  • manaflow-ai/cmux#5154: Both PRs change how workingDirectory is resolved during agent fork/resume restore flows.
  • manaflow-ai/cmux#4237: Both PRs extend the surface-resume binding pipeline and restore-time handling for resume bindings.

Poem

🐰 I found a sneaky bash that hid a resume line,
I sniffed the argv and said, "Not this time."
Poisoned wrappers dropped, repaired snapshots sing,
Restores use trusted captures — hop, spring! 🥕✨

🚥 Pre-merge checks | ✅ 20 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (20 passed)
Check name Status Explanation
Title check ✅ Passed The title precisely describes the main change: adding logic to remove poisoned shell-wrapper resume commands during session restoration.
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 Actor Isolation ✅ Passed Diff shows new trusted/poison checks implemented as pure value-type computed property and nonisolated static helpers; no new MainActor-protocols, Sendable shared mutable refs, or UI-store access...
Cmux Swift Blocking Runtime ✅ Passed Scanned PR-relevant production Swift logic (RestorableAgentSession.swift, SessionPersistence.swift, and Workspace.swift resume-index helper region) for semaphores/sleeps/wait/locks/main-queue sync;...
Cmux Expensive Synchronous Load ✅ Passed Only RestorableAgentSessionIndex.load appears in RestorableAgentSession.swift within a Task.detached loader; SessionPersistence.swift/Workspace.swift changes introduce no new sync loader on main/in...
Cmux Cache Substitution Correctness ✅ Passed PR changes add trust/poison filtering on persisted resume/launch snapshots; no diff replaces fresh authoritative reads with cached/opportunistic values in persistence/history/undo/snapshot paths.
Cmux No Hacky Sleeps ✅ Passed PR #6012 only changes .swift files (3 files shown on the GitHub “files changed” view); no TS/JS/shell/build/runtime script edits or hacky sleeps/polling/timers found.
Cmux Algorithmic Complexity ✅ Passed PR adds defensive trust checks and resume-command parsing; reviewed changes show only linear/small-constant scans over per-snapshot arrays (no nested full rescans/sorts in hot paths).
Cmux Swift Concurrency ✅ Passed Inspected the new restoration/trust code blocks and related tests; no DispatchQueue, Task {, Combine/@published, or completion-handler async patterns appear in the PR’s added logic areas.
Cmux Swift @Concurrent ✅ Passed In Sources/RestorableAgentSession.swift, Workspace.swift, and SessionPersistence.swift (and all Sources/*.swift), there are 0 occurrences of @concurrent or nonisolated async; no Swift-concurrent-ru...
Cmux Swift File And Package Boundaries ✅ Passed PR #6012 only adds small amounts to existing production files (RestorableAgentSession.swift +12, Workspace.swift +27) and adds tests only; no new oversized files or >250-line production growth.
Cmux Swift Logging ✅ Passed Checked git diff (origin/main..HEAD) for prohibited Swift logging (print/debugPrint/dump/NSLog/Logger, stdout/stderr) in Sources/*.swift touched by PR; found no matches.
Cmux User-Facing Error Privacy ✅ Passed PR diff adds only internal session-restore trust/parsing logic; no added user-facing alerts/errors/recovery copy (no NSAlert/localized string changes in added lines), so no vendor/provider/session-...
Cmux Full Internationalization ✅ Passed Inspected PR-touched Swift logic/tests for user-facing UI strings (String(localized:), Text(, alerts, NSAlert). No newly introduced unlocalized user-facing text found; no web/UI/i18n catalog edits...
Cmux Swiftui State Layout ✅ Passed Workspace.swift changes are in parsing/restore helpers (no struct ...: View, no GeometryReader); other changed Swift files contain no SwiftUI state/layout code.
Cmux Architecture Rethink ✅ Passed Checked added trust/repair + resume-parsing helpers in the touched Swift files; they’re synchronous guard/parse logic with no new sleep/poll/lock/observer/duplicate-entrypoint patterns in the nearb...
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR #6012 only modifies session restore/persistence logic in RestorableAgentSession.swift, Workspace.swift, and SessionPersistenceTests.swift; no NSWindow/NSPanel/WindowGroup/closeShortcut or cmuxAu...
Cmux Source Artifacts ✅ Passed GitHub PR files view shows only 3 changed source files (two Sources/.swift and one cmuxTests/.swift); none match source-control artifact patterns (DerivedData/tmp/logs/screenshots/etc.).
Description check ✅ Passed PR description covers what changed, why, verification steps and testing, but lacks a demo video and incomplete checklist items.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 issue-session-restore-resume-command

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 and usage tips.

@greptile-apps

greptile-apps Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a session-restore regression introduced in 0.64.15 where persisted agent-hook bindings shaped like bash resume <session> -c … were trusted before the valid agent snapshot, producing bash: resume: No such file or directory. It adds a defense-in-depth filter at two layers — a SessionSnapshotRepairer that strips poisoned bindings and untrusted launch commands when loading the persisted file, and a trustedForSessionRestore check inside Workspace.createPanel for snapshots that bypass disk load.

  • SessionSnapshotRepairer (new in SessionPersistence.swift): walks the full snapshot tree on loadOutcome, drops agent-hook bindings whose executable+next-token matches the shell-wrapper pattern, drops untrusted agent launch commands, and re-saves the cleaned snapshot so subsequent launches read clean state.
  • trustedLaunchCommandForSessionRestore (new in RestorableAgentSession.swift): routes resumeCommand, forkCommand, and resumeStartupCommand through AgentLaunchCaptureTrust so poisoned launch argv never drives resume rendering, even when the in-memory snapshot has not been through the repairer.
  • Three new regression tests cover the poisoned-binding-with-agent-snapshot path, the poisoned-binding-without-agent-snapshot path, and the snapshot-level launch command rejection.

Confidence Score: 5/5

Safe to merge — the changes are purely defensive filters on incoming persisted data, with clear fallbacks and regression tests for all new paths.

No correctness issues were found in the new filter logic. Defense is applied at two independent layers (file load and workspace restore), both correctly using value-type transformations on Sendable structs without actor isolation concerns. The repair is idempotent and the in-memory snapshot is always the cleaned version regardless of whether the re-save succeeds. The three new regression tests cover the most important failure scenarios described in the PR.

No files require special attention. The single-&& limitation in commandStartIndexAfterCwdGuard (already flagged in a prior review thread) is the only known edge case, and it was a pre-existing assumption that this PR does not worsen.

Important Files Changed

Filename Overview
Sources/RestorableAgentSession.swift Adds trustedLaunchCommandForSessionRestore computed property and repairedForSessionRestore method to SessionRestorableAgentSnapshot; routes resumeCommand/forkCommand/resumeStartupCommand through the trust gate so poisoned shell-wrapper launch argv can no longer drive resume rendering.
Sources/SessionPersistence.swift Adds isPoisonedAgentHookShellWrapperResume / trustedForSessionRestore to SurfaceResumeBindingSnapshot, a new commandStartIndexAfterCwdGuard helper on SurfaceResumeCommandCanonicalizer, and the SessionSnapshotRepairer enum that cleans up poisoned bindings and untrusted launch commands in persisted snapshots on first load.
Sources/Workspace.swift Renames hermesAgentCommandStartIndexAfterCwdGuard to surfaceResumeCommandStartIndexAfterCwdGuard and threads trustedForSessionRestore through createPanel so poisoned bindings are dropped at workspace restore time even for in-memory snapshots not loaded from disk.
cmuxTests/SessionPersistenceTests.swift Adds three regression tests: full load-repair-save cycle for a poisoned binding, working-directory recovery for a mismatched launch command, and snapshot-level shell-wrapper launch command rejection; also extracts a makeSnapshot helper for test setup.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[SessionPersistenceStore.loadOutcome] --> B[Decode AppSessionSnapshot]
    B --> C[SessionSnapshotRepairer.repair]
    C --> D{For each panel}
    D --> E{resumeBinding present?}
    E -- yes --> F[trustedForSessionRestore\nisPoisonedAgentHookShellWrapperResume?]
    F -- poisoned --> G[Set resumeBinding = nil\ndidRepair = true]
    F -- clean --> H[Keep resumeBinding]
    D --> I{agent present?}
    I -- yes --> J[repairedForSessionRestore\ntrustedLaunchCommandForSessionRestore?]
    J -- untrusted --> K[Drop launchCommand\nReplace workingDirectory with fallback\ndidRepair = true]
    J -- trusted --> L[Keep launchCommand]
    C --> M{didRepair?}
    M -- yes --> N[save repaired snapshot]
    M -- no --> O[Return as-is]
    N --> O
    O --> P[Workspace.restoreSessionSnapshot]
    P --> Q[createPanel from snapshot]
    Q --> R[resumeBinding?.trustedForSessionRestore\ndefense-in-depth filter]
    R --> S[restorableAgent?.resumeStartupCommand\ncalls trustedLaunchCommandForSessionRestore]
Loading

Reviews (2): Last reviewed commit: "Repair persisted session restore snapsho..." | Re-trigger Greptile

Comment thread Sources/Workspace.swift Outdated
Comment on lines +1066 to +1078
nonisolated private static func agentHookBindingLooksLikeShellWrapperResume(
_ binding: SurfaceResumeBindingSnapshot
) -> Bool {
guard binding.isAgentHookBinding else { return false }
let words = surfaceResumeShellWords(in: binding.command)
let commandStart = surfaceResumeCommandStartIndexAfterCwdGuard(words)
guard commandStart + 1 < words.endIndex else { return false }
let executable = (words[commandStart].value as NSString).lastPathComponent.lowercased()
let shells: Set<String> = ["sh", "bash", "zsh", "dash", "fish", "csh", "tcsh", "ksh"]
guard shells.contains(executable) else { return false }
let resumeWord = words[commandStart + 1].value
return resumeWord == "resume" || resumeWord == "--resume" || resumeWord.hasPrefix("--resume=")
}

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 fish included in shell set may cause false positives

fish is listed alongside traditional POSIX shells, but fish resume <session> is a plausible legitimate invocation for a user whose agent-hook command genuinely calls a Fish script named resume. Because agentHookBindingLooksLikeShellWrapperResume only fires for isAgentHookBinding bindings, the blast radius is narrow, but fish does not appear in the original poisoned binding pattern described in the PR (bash resume …) and adding it increases the heuristic surface without a concrete motivating case.

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!

Comment on lines 1889 to 1920
}

final class SocketListenerAcceptPolicyTests: XCTestCase {
func testPersistedAgentSnapshotDropsShellWrapperLaunchCommandWhenRenderingResume() {
let snapshot = SessionRestorableAgentSnapshot(
kind: .codex,
sessionId: "codex-session-123",
workingDirectory: "/tmp/repo",
launchCommand: AgentLaunchCommandSnapshot(
launcher: "codex",
executablePath: "bash",
arguments: [
"bash",
"-c",
"payload=\"$1\"; shift; \"$@\" <\"$payload\" &",
],
workingDirectory: "/tmp/dispatch-shell",
environment: ["CODEX_HOME": "/tmp/codex"],
capturedAt: 123,
source: "process"
)
)

XCTAssertEqual(
snapshot.resumeCommand,
"{ cd -- '/tmp/repo' 2>/dev/null || [ ! -d '/tmp/repo' ]; } && 'codex' 'resume' 'codex-session-123'"
)
}

func testClaudeResumeCommandRoutesThroughWrapperInsteadOfCapturedRealBinary() {
// The captured launch executable is the real claude binary
// (CMUX_AGENT_LAUNCH_EXECUTABLE). Resuming with it directly bypasses

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 Test placed in wrong class

testPersistedAgentSnapshotDropsShellWrapperLaunchCommandWhenRenderingResume exercises SessionRestorableAgentSnapshot.resumeCommand filtering, but it lives inside SocketListenerAcceptPolicyTests. The two other new tests correctly land in SessionPersistenceTests. Moving this test keeps the file's grouping consistent and makes it easier to run the persistence test suite in isolation.

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: 2

🤖 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 722-738: In repairedForSessionRestore, when
trustedLaunchCommandForSessionRestore is nil we currently backfill
repaired.workingDirectory with fallbackWorkingDirectory unconditionally; change
this so we preserve a nil cwd for registrations marked cwd == .ignore by
checking registration?.cwd == .ignore (or the equivalent enum case) before
assigning fallbackWorkingDirectory — if registration?.cwd == .ignore leave
repaired.workingDirectory as nil; keep existing logic using
Self.normalizedWorkingDirectory(...) and the trustedLaunchCommand branch intact.

In `@Sources/SessionPersistence.swift`:
- Around line 700-708: The helper commandStartIndexAfterCwdGuard currently only
recognizes a cwd guard terminated by "&&" and returns tokens.startIndex for the
other in-repo form; update it to also detect the "|| exit $?" guard (or the full
known prefix) and treat it the same as the "&&" case. Concretely, in
commandStartIndexAfterCwdGuard(_:), after confirming tokens.first is "{" or
"cd", check for a terminating operator of either "&&" or "||" (optionally
followed by "exit" and "$?") and when found return tokens.index(after:
thatOperatorIndex) so the resume command start is computed the same for both
guard spellings.
🪄 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: f109ed32-9a97-42ff-96f6-1a3c7be9e3c8

📥 Commits

Reviewing files that changed from the base of the PR and between 5b59abe and 46afbe7.

📒 Files selected for processing (4)
  • Sources/RestorableAgentSession.swift
  • Sources/SessionPersistence.swift
  • Sources/Workspace.swift
  • cmuxTests/SessionPersistenceTests.swift

Comment on lines +722 to +738
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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Preserve .ignore by keeping the repaired snapshot cwd nil.

When trustedLaunchCommandForSessionRestore is nil, this repair path backfills workingDirectory from fallbackWorkingDirectory without checking registration?.cwd == .ignore. That reintroduces a saved cwd into snapshots that are supposed to restore from the caller’s current directory, and downstream restore code in this file explicitly relies on workingDirectory being nil for .ignore agents.

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

Based on learnings: “Registrations with cwd: .ignore set resumeWorkingDirectory to nil, suppressing both the cwd guard in the resume command and the terminal working directory at placement time.”

🤖 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 722 - 738, In
repairedForSessionRestore, when trustedLaunchCommandForSessionRestore is nil we
currently backfill repaired.workingDirectory with fallbackWorkingDirectory
unconditionally; change this so we preserve a nil cwd for registrations marked
cwd == .ignore by checking registration?.cwd == .ignore (or the equivalent enum
case) before assigning fallbackWorkingDirectory — if registration?.cwd ==
.ignore leave repaired.workingDirectory as nil; keep existing logic using
Self.normalizedWorkingDirectory(...) and the trustedLaunchCommand branch intact.

Source: Learnings

Comment on lines +700 to +708
static func commandStartIndexAfterCwdGuard(_ tokens: [String]) -> Int {
guard let first = tokens.first,
first == "{" || first == "cd" else {
return tokens.startIndex
}
guard let andIndex = tokens.firstIndex(of: "&&") else {
return tokens.startIndex
}
return tokens.index(after: andIndex)

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

Handle the repo’s || exit $? cwd guard here too.

This helper only skips guards terminated by &&. Sources/SessionRestoredTerminalCommandStore.swift already emits the other in-repo form, { cd -- <cwd> ...; } || exit $? before exec ..., so callers of commandStartIndexAfterCwdGuard will fall back to tokens.startIndex for those commands and parse { as the executable instead of the real resume command. Pattern-match both guard spellings (or the full known guard prefix) here so the canonical start index stays correct everywhere. Based on learnings from the supplied cross-file snippet: Sources/SessionRestoredTerminalCommandStore.swift:3-44 uses a || exit $? cwd guard that this helper does not currently recognize.

🤖 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 700 - 708, The helper
commandStartIndexAfterCwdGuard currently only recognizes a cwd guard terminated
by "&&" and returns tokens.startIndex for the other in-repo form; update it to
also detect the "|| exit $?" guard (or the full known prefix) and treat it the
same as the "&&" case. Concretely, in commandStartIndexAfterCwdGuard(_:), after
confirming tokens.first is "{" or "cd", check for a terminating operator of
either "&&" or "||" (optionally followed by "exit" and "$?") and when found
return tokens.index(after: thatOperatorIndex) so the resume command start is
computed the same for both guard spellings.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant