Skip to content

Fix fish Claude restore fallback - #6954

Closed
austinywang wants to merge 45 commits into
mainfrom
issue-5796-fish-no-shell-integration-ships-so-claude-byp
Closed

austinywang wants to merge 45 commits into
mainfrom
issue-5796-fish-no-shell-integration-ships-so-claude-byp

Conversation

@austinywang

@austinywang austinywang commented Jun 26, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #5796

Summary

  • reject shell/login executables when building agent resume argv, falling back to the agent executable and dropping shell bootstrap flags
  • prefer a newer Claude agent launch snapshot over an older agent-hook resume binding during session restore
  • add red/green regression coverage for shell-bootstrap argv and stale poisoned bindings

Validation

  • swift test --package-path Packages/macOS/CMUXAgentLaunch

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


Summary by cubic

Fix auto-restore when a shell/login dispatcher is captured by falling back to the agent launcher so Claude and Codex resume correctly while keeping cwd and env. Prefer newer same‑kind launch snapshots over shell‑poisoned or legacy resume bindings; preserve shell‑named wrappers that aren’t dispatchers. Fixes #5796.

  • Bug Fixes
    • Switched to an instance AgentLaunchCaptureTrust used across CLI, resume argv, restore, and sessions list; adds executableLooksLikeShell and improves argv scanning to handle combined/split startup flags and option operands (-l/-i/-m/-s, --noprofile/--norc/--no-rcs/--no-config, -o, --rcfile).
    • When argv is a shell bootstrap (sh -c …, zsh -lc …), resume falls back to the agent verb and clears args; shell‑named wrappers that aren’t dispatchers are preserved.
    • Sanitized shell‑dispatch captures now strip executable/argv but keep cwd/env; sessions list fork preserves this env and withholds startup input for shell wrappers.
    • Restore preference: newer same‑kind agent launch snapshots replace poisoned or legacy bindings (updatedAt=0) or when the session id matches; detection guards only shell‑poisoned bindings and wrapper tokens (claude-teams/codex-teams), cross‑kind or kind‑less bindings remain.
    • Stabilized Codex hook parsing by extracting nested JSON and external script contents.
    • CI isolates the app‑host home via CFFIXED_USER_HOME and forwards CARGO_HOME/RUSTUP_HOME.

Written for commit b69993b. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features
    • Enhanced session restore/auto-resume to prefer the newest valid agent snapshot, minimizing stale resume-state carryover.
    • Improved shell-wrapper detection and trust handling to reconstruct resumed executable/arguments more accurately.
    • More consistent snapshot resume-binding selection for terminal restore.
  • Bug Fixes
    • Fixed restore flows that could keep outdated bindings and/or preserve wrapper-like executables instead of the intended agent executable.
    • Shell-wrapper-like captures are now sanitized appropriately during restore (rather than being dropped).
  • Tests
    • Added restore/launch-trust coverage for shell-wrapper argv/environment handling and expanded regression cases.
    • Migrated/updated unit tests to Swift’s Testing framework.

@vercel

vercel Bot commented Jun 26, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Jul 5, 2026 5:51am
cmux-staging Building Building Preview, Comment Jul 5, 2026 5:51am

@coderabbitai

coderabbitai Bot commented Jun 26, 2026 •

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Launch trust now uses instance-based helpers across classification, resume argv building, and trusted-launch checks. Workspace restore and snapshot logic now compare binding freshness against restorable agent captures before reusing bindings.

Changes

Agent launch trust and restore precedence

Layer / File(s) Summary
Trust policy instance and callers
Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/AgentLaunchCaptureTrust.swift, CLI/cmux.swift, Sources/RestorableAgentSession.swift, Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/AgentLaunchCaptureTrustTests.swift, scripts/lint-namespace-types-baseline.txt
AgentLaunchCaptureTrust becomes an instance-based policy object, the CLI and trusted-launch command use its instance methods, the trust tests switch to the new API, and the namespace-types baseline drops the old offender entry.
Resume argv fallback
Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/AgentResumeArgv.swift, Sources/RestorableAgentSession.swift, Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/AgentResumeArgvTests.swift, cmuxTests/AgentSessionAutoResumeSwiftTests.swift, cmuxTests/RestorableAgentLaunchTrustTests.swift, cmux.xcodeproj/project.pbxproj
AgentResumeArgv and command-part builders store a trust policy, stop on shell-wrapper-shaped argv, and return the fallback executable with an empty tail when executable normalization fails; tests cover shell bootstrap, shell-wrapper, and restorable-launch cases, and the new test file is wired into the test target.
Session snapshot preference
Sources/Workspace.swift, cmuxTests/AgentSessionAutoResumeSwiftTests.swift
Workspace computes a snapshot-specific resume binding, uses it for restore compatibility and terminal snapshot creation, and drops agent-hook bindings when a newer restorable agent snapshot should win; the regression tests assert the newer snapshot is retained in restore and persistence flows.

Sequence Diagram(s)

sequenceDiagram
  participant Workspace.sessionPanelSnapshot
  participant resumeBindingForSessionSnapshot
  participant shouldPreferRestorableAgentSnapshot
  participant SessionTerminalPanelSnapshot
  Workspace.sessionPanelSnapshot->>resumeBindingForSessionSnapshot: derive snapshotResumeBinding
  resumeBindingForSessionSnapshot->>shouldPreferRestorableAgentSnapshot: compare binding.updatedAt and launch capture time
  shouldPreferRestorableAgentSnapshot-->>resumeBindingForSessionSnapshot: prefer or keep binding
  resumeBindingForSessionSnapshot-->>Workspace.sessionPanelSnapshot: snapshotResumeBinding
  Workspace.sessionPanelSnapshot->>SessionTerminalPanelSnapshot: initialize with snapshotResumeBinding
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • manaflow-ai/cmux#5937: Directly overlaps the AgentLaunchCaptureTrust refactor and the same launcher-kind/shell-wrapper trust checks used here.
  • manaflow-ai/cmux#6580: Shares the PID-derived agent classification path that now uses nativeProcessDescribesKnownAgent and nativeProcessDescribesKind.
  • manaflow-ai/cmux#5312: Touches the same resume argv construction path that now stores and consults AgentLaunchCaptureTrust.

Poem

I hopped through shells and snapshots neat,
with trustful paws and resume beat.
Old bindings thudded; fresh ones sprout,
“Hop, hop!” said I, “the right path’s out.” 🐰


Important

Pre-merge checks failed

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

❌ Failed checks (1 error, 1 warning)

Check name Status Explanation Resolution
Cmux No Test Or Debug Seam In Production Source ❌ Error Sources/Workspace.swift adds runSSHControlMasterCommandOverrideForTesting and remoteSessionProcessRunnerOverrideForTesting, creating test seams in production code. Move those hooks into the test target (or a dedicated debug file) and expose needed state via @testable import after widening private to internal.
Docstring Coverage ⚠️ Warning Docstring coverage is 14.63% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (23 passed)
Check name Status Explanation
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 New trust policy is an immutable Sendable struct; changed call sites are pure/nonisolated helpers, with no MainActor or shared mutable Sendable reference types introduced.
Cmux Swift Blocking Runtime ✅ Passed No new blocking waits, sleeps, syncs, or locks were introduced; the diff only routes trust logic and adds tests.
Cmux Browser Automation Off-Main ✅ Passed PR touches AgentLaunch/Workspace/CLI tests, not the browser-automation routing files; no off-main WebKit wait or socket-worker policy change is introduced.
Cmux Expensive Synchronous Load ✅ Passed No unsanctioned sync history load was added; the UI path uses SharedLiveAgentIndex.shared with only a nil-guarded cold-cache fallback.
Cmux Cache Substitution Correctness ✅ Passed PASS: snapshot uses SharedLiveAgentIndex.currentIndexSchedulingRefresh() with fresh-load fallback/TTL, and stale same-kind bindings are gated by shouldPreferRestorableAgentSnapshot.
Cmux No Hacky Sleeps ✅ Passed PR changes are Swift/test-only plus project/baseline files; no modified TS/JS/shell/build-runtime scripts add sleeps or polling.
Cmux Algorithmic Complexity ✅ Passed New scans are over tiny fixed-size alias/argv collections or bounded ancestor chains; no nested scalable rescans or hot-path rebuilds were added.
Cmux Swift Concurrency ✅ Passed Touched diffs are synchronous value/restoration logic; scans of modified files show no new DispatchQueue, Combine, completion-handler, or fire-and-forget Task patterns.
Cmux Swift @Concurrent ✅ Passed No new @concurrent/nonisolated-async issues: touched helpers are synchronous, and existing @MainActor code only calls pure sync trust logic.
Cmux Swift File And Package Boundaries ✅ Passed The reusable trust/resume logic lives in the small CMUXAgentLaunch package; app-target edits are tiny glue in already oversized files, so no boundary rule is violated.
Cmux Swiftpm Lockfiles ✅ Passed No cmux-owned .gitignore ignores Package.resolved, and the Xcode project change is just a test-file add; no package-ref or lockfile change is required.
Cmux Swift Logging ✅ Passed The patch only changes resume/restore behavior; no added or changed print/debugPrint/dump/NSLog/Logger usage appears in the diff, and existing CLI/DEBUG logging is unchanged.
Cmux User-Facing Error Privacy ✅ Passed Changed production code only adjusts trust/restore logic; no user-facing errors, alerts, output, or secrets were added or modified.
Cmux Full Internationalization ✅ Passed Touched production files only changed trust/restore logic; no new user-facing Swift text, string-catalog, Info.plist, or web locale entries were added.
Cmux Swiftui State Layout ✅ Passed No new SwiftUI view/state/layout patterns were introduced; Workspace.swift changes are non-body helper logic, and existing ObservableObject/@published state is legacy/incidental.
Cmux Architecture Rethink ✅ Passed No prohibited timing/lock/observer patching; the change centralizes shell/capture trust in pure value helpers and gates newer same-kind snapshots via an explicit timestamp/kind invariant.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed The PR only changes launch/restore trust logic and tests; no new or modified standalone NSWindow/NSPanel/WindowGroup code or cmuxAuxiliaryWindowIdentifiers entries appear.
Cmux Source Artifacts ✅ Passed Changed paths are hand-written source, tests, config, and project file; no logs, temp dirs, caches, build output, or scratch artifacts appear.
Cmux No Ambient Global State ✅ Passed New trust logic is injectable instance state; added helpers are private/static inside owning types, with no new ambient globals or singletons.
Title check ✅ Passed The title is concise and clearly points to the restore fallback fix for Claude shell-wrapper handling.
Description check ✅ Passed The description includes a summary and validation, but it omits the Demo Video, Review Trigger, and Checklist sections from the template.
✨ 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-5796-fish-no-shell-integration-ships-so-claude-byp

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.

@austinywang

Copy link
Copy Markdown
Contributor Author

@codex review
@coderabbitai review
@greptile-apps review
@cubic-dev-ai review

@cubic-dev-ai

cubic-dev-ai Bot commented Jun 26, 2026

Copy link
Copy Markdown

@codex review
@coderabbitai review
@greptile-apps review
@cubic-dev-ai review

@austinywang I can't start this review because your workspace has reached its free monthly review limit. cubic has reviewed 241,260 of the 240,000 allowed lines of code this month. Reviews resume on 1 July 2026 (in 5 days). Paid plans include much higher monthly review limits. Upgrade now to resume reviews.

To help optimise your usage, you can tune cubic to get the most out of your usage limits:

Learn more →

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@coderabbitai

coderabbitai Bot commented Jun 26, 2026 •

Copy link
Copy Markdown

@austinywang Reviewing the changes now.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@greptile-apps

greptile-apps Bot commented Jun 26, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

Fixes the fish-shell (and similar) session-restore regression where a bash --noprofile --norc -c "exec fish" bootstrap argv was captured as the agent's own launch command, causing Claude/Codex to resume as bash instead of the correct agent binary. The fix has two complementary layers: reject/sanitize shell-dispatcher captures at the argv level and prefer a fresher agent-launch snapshot over a same-kind poisoned hook binding at restore time.

  • AgentLaunchCaptureTrust converted from a static-only enum to an injectable struct; argvLooksLikeShellWrapper now walks all tail arguments (not just arguments[1]), handles --noprofile/--norc startup-only flags, option-consuming flags (-o pipefail), and the sanitized form (startup flags with no -c) via sawShellStartupOnlyFlag.
  • Shell-dispatch captures are sanitized (executable and argv cleared, cwd/env retained) in trustedLaunchCommand, CMUXCLI+SessionsListForkStartupInput, and AgentResumeArgv.commandParts, so resume falls back to the agent's own executable while preserving working directory and environment variables.
  • Workspace.shouldPreferRestorableAgentSnapshot prefers a newer same-kind agent-launch snapshot over a poisoned binding when session IDs match or the snapshot's capturedAt exceeds the binding's updatedAt; updatedAt now defaults to 0 for legacy bindings so they are eligible for replacement.

Confidence Score: 5/5

Safe to merge; the fix correctly sanitizes shell-dispatch captures and prefers fresher agent snapshots without discarding working directory or environment data.

The shell-flag parser in argvLooksLikeShellWrapper is well-tested against the fish bootstrap pattern and a variety of edge cases. The shouldPreferRestorableAgentSnapshot logic is guarded by kind match, poisoned-command detection, and a resume-command availability check before any timestamp comparison. The updatedAt=0 default for legacy bindings is safe because non-poisoned bindings are never touched by the preference logic regardless of their timestamp.

The commandLooksLikePoisonedShellResumeBinding heuristic in Sources/Workspace.swift and the timestamp-comparison path in shouldPreferRestorableAgentSnapshot are the most nuanced new logic; a follow-up test covering the differing-session timestamp branch would close the remaining coverage gap.

Important Files Changed

Filename Overview
Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/AgentLaunchCaptureTrust.swift Converts AgentLaunchCaptureTrust from a caseless static-only enum to a constructable, injectable struct with a full shell-flag parser replacing the single-argument check. The argvLooksLikeShellWrapper rewrite correctly handles multi-flag bootstrap sequences (--noprofile/--norc/-c), option-consuming flags (-o pipefail), and the sanitized-form (flags-only, no -c) termination via sawShellStartupOnlyFlag.
Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/AgentResumeArgv.swift Injects AgentLaunchCaptureTrust into AgentResumeArgv and adds a shell-wrapper early-return in commandParts before the tail is computed; the fallback-executable path is now hit even when executablePath is populated with a shell path.
Sources/Workspace.swift Adds resumeBindingForSessionSnapshot, shouldPreferRestorableAgentSnapshot, and commandLooksLikePoisonedShellResumeBinding to prefer a fresher agent-launch snapshot over a same-kind poisoned shell binding at both snapshot and restore time; updatedAt=0 is the legacy sentinel for bindings that predate the field.
Sources/RestorableAgentSession.swift Sanitizes shell-dispatch launch captures (strip executable/argv, keep cwd/env) instead of dropping them entirely; a shared AgentLaunchCaptureTrust instance is created once before the per-session loop; AgentResumeCommandBuilder.commandParts adds an inline shell-wrapper guard.
Sources/SessionPersistence.swift Changes the updatedAt decode fallback from Date().timeIntervalSince1970 to 0, making legacy bindings without the field sort as the oldest possible timestamp so a fresher agent snapshot can replace them.
CLI/CMUXCLI+SessionsListForkStartupInput.swift Switches to instance AgentLaunchCaptureTrust; shell-wrapper captures are now sanitized (nil executable, empty args, env/cwd retained) instead of returning nil, so fork has an env but no startup input for shell-bootstrap records.
cmuxTests/AgentSessionAutoResumeSwiftTests.swift Adds four regression tests for the poisoned-binding scenarios: newer snapshot wins over older poisoned binding, older snapshot loses to a newer poisoned binding, legacy updatedAt=0 bindings replaced for both claude and codex (direct and teams variants), and shell-named non-wrapper executables preserved.
cmuxTests/RestorableAgentLaunchTrustTests.swift New test verifying that a shell-wrapper launch capture (bash --noprofile --norc) retains cwd and environment for resume while stripping the shell executable and flags from the rendered command.
.github/workflows/ci.yml Prepares an isolated CFFIXED_USER_HOME per shard for app-host tests and forwards CARGO_HOME/RUSTUP_HOME through the console session, preventing cross-test home directory pollution.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Hook as Agent Hook
    participant Trust as AgentLaunchCaptureTrust
    participant TLC as trustedLaunchCommand
    participant SSPR as shouldPreferRestorableAgentSnapshot
    participant Resume as AgentResumeCommandBuilder

    Hook->>Trust: argvLooksLikeShellWrapper(["bash","--noprofile","--norc","-c","exec fish"])
    Trust-->>Hook: true (shell dispatcher)

    Hook->>TLC: "launchCommand {exec=bash, args=[...]}"
    TLC->>Trust: launcherDescribesKind("claude", kind:"claude")
    Trust-->>TLC: true
    TLC->>Trust: argvLooksLikeShellWrapper(args)
    Trust-->>TLC: true
    TLC-->>Hook: "sanitized {exec=nil, args=[], cwd=launchCwd, env=CLAUDE_CONFIG_DIR}"

    Note over Hook,Resume: At restore time

    Hook->>SSPR: "binding(kind=claude, cmd=bash --resume SID, updatedAt=T1) vs snapshot(kind=claude, capturedAt=T2>T1)"
    SSPR->>SSPR: commandLooksLikePoisonedShellResumeBinding?
    SSPR-->>Hook: "true — prefer snapshot, binding=nil"

    Hook->>Resume: "resumeShellCommand(kind=claude, launchCommand=sanitized)"
    Resume->>Trust: argvLooksLikeShellWrapper([]) — false
    Resume-->>Hook: claude --resume SID (fallback executable)
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Hook as Agent Hook
    participant Trust as AgentLaunchCaptureTrust
    participant TLC as trustedLaunchCommand
    participant SSPR as shouldPreferRestorableAgentSnapshot
    participant Resume as AgentResumeCommandBuilder

    Hook->>Trust: argvLooksLikeShellWrapper(["bash","--noprofile","--norc","-c","exec fish"])
    Trust-->>Hook: true (shell dispatcher)

    Hook->>TLC: "launchCommand {exec=bash, args=[...]}"
    TLC->>Trust: launcherDescribesKind("claude", kind:"claude")
    Trust-->>TLC: true
    TLC->>Trust: argvLooksLikeShellWrapper(args)
    Trust-->>TLC: true
    TLC-->>Hook: "sanitized {exec=nil, args=[], cwd=launchCwd, env=CLAUDE_CONFIG_DIR}"

    Note over Hook,Resume: At restore time

    Hook->>SSPR: "binding(kind=claude, cmd=bash --resume SID, updatedAt=T1) vs snapshot(kind=claude, capturedAt=T2>T1)"
    SSPR->>SSPR: commandLooksLikePoisonedShellResumeBinding?
    SSPR-->>Hook: "true — prefer snapshot, binding=nil"

    Hook->>Resume: "resumeShellCommand(kind=claude, launchCommand=sanitized)"
    Resume->>Trust: argvLooksLikeShellWrapper([]) — false
    Resume-->>Hook: claude --resume SID (fallback executable)
Loading

Reviews (28): Last reviewed commit: "Merge remote-tracking branch 'origin/mai..." | Re-trigger Greptile

@greptile-apps

greptile-apps Bot commented Jun 26, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes fish-shell bootstrap poisoning of agent resume argv (issue #5796). When the cmux launch-capture PID fallback resolved to a bash -c "exec -l fish" shell dispatcher rather than the actual agent binary, the hook binding recorded bash --resume … — making resume silently fail. Two coordinated fixes address the root cause: commandParts now detects shell executables via executableLooksLikeShell and falls back to the agent kind's default name with an empty tail, and resumeBindingForSessionRestore now prefers a fresher launch snapshot over an older agent-hook binding when the snapshot's capturedAt timestamp exceeds the binding's updatedAt.

  • AgentLaunchCaptureTrust: new executableLooksLikeShell public API (with login-shell dash-stripping) replaces the inline shell set in argvLooksLikeShellWrapper; the wrapper detection is broadened to scan all remaining arguments (not just position 1) so bash --noprofile --norc -c … is correctly identified.
  • AgentResumeArgv / RestorableAgentSession: commandParts in both paths now rejects shell executables and returns (fallbackExecutable, []), dropping bootstrap flags from the resume argv.
  • Workspace: new shouldPreferRestorableAgentSnapshot uses a capturedAt > updatedAt (TimeInterval) comparison to discard a stale poisoned binding in favour of the agent launch snapshot; regression tests cover both the shell-bootstrap argv path and the stale-binding preference logic.

Confidence Score: 4/5

The fix is targeted and well-tested; the two new regression tests cover the fish-bootstrap argv and the stale-poisoned-binding paths end-to-end. The only open question is whether a newer snapshot for a different agent kind should silently win over an older binding of a different kind (old code failed closed; new code resumes with the snapshot's kind).

Both production paths (AgentResumeArgv and AgentResumeCommandBuilder) receive the same shell-detection fix, the timestamp types are both TimeInterval and compare correctly, and all existing argvLooksLikeShellWrapper tests continue to pass. The one behavioral edge case — a cross-kind surface where the snapshot is newer than the binding — changes 'no resume' into 'resume with the snapshot's kind', which is plausibly better UX but is not explicitly tested or documented as intentional.

Sources/Workspace.swift — specifically shouldPreferRestorableAgentSnapshot and how it interacts with restorableAgentForSessionRestore when the snapshot and binding are for different kinds or session IDs.

Important Files Changed

Filename Overview
Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/AgentLaunchCaptureTrust.swift Extracts executableLooksLikeShell (with login-shell dash-stripping), expands shell set to include login, and broadens argvLooksLikeShellWrapper to scan all remaining arguments instead of just position 1 — correct and intentional for the fish bootstrap case.
Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/AgentResumeArgv.swift commandParts now guards against shell executables and returns (fallbackExecutable, []) — eliminates the fish bootstrap argv from the resume command and falls back to the bare agent name with no inherited flags.
Sources/RestorableAgentSession.swift Parallel fix in commandParts on the app-target AgentResumeCommandBuilder: same shell-executable guard as the package builder, keeping both paths consistent.
Sources/Workspace.swift Adds shouldPreferRestorableAgentSnapshot with a capturedAt > updatedAt (TimeInterval) timestamp comparison; when true, discards the agent-hook binding before the session-ID and kind checks in resumeBindingForSessionRestore, routing restore through the launch snapshot instead of the poisoned binding command.
Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/AgentResumeArgvTests.swift Adds green regression test for the fish bootstrap argv (/bin/bash --noprofile --norc -c exec) falling back to the bare agent executable for both claude and codex kinds.
cmuxTests/AgentSessionAutoResumeSwiftTests.swift Adds integration-level regression test verifying that a newer claude launch snapshot supersedes a stale poisoned bash binding during session restore, checking both the resumed session ID and the absence of bash/stale session tokens in the startup script.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["Session Restore: resumeBindingForSessionRestore()"] --> B{binding exists?}
    B -- No --> Z["return nil (no restore)"]
    B -- Yes --> C{isAgentHookBinding AND\nshouldPreferRestorableAgentSnapshot?}
    C -- Yes --> D["return nil\n(discard stale binding)"]
    C -- No --> E{isAgentHookBinding AND\nrestorableAgent exists?}
    E -- No --> F["return binding as-is"]
    E -- Yes --> G{checkpointId == sessionId?}
    G -- No --> F
    G -- Yes --> H{kind match?}
    H -- No --> F
    H -- Yes --> I["return binding with\nreconciled working directory"]
    D --> J["restorableAgentForSessionRestore(snapshot, nil)"]
    J --> K["return snapshot as-is"]
    subgraph commandParts
        L["executablePath or argv[0]"] --> M{executableLooksLikeShell?}
        M -- Yes --> N["return (fallbackExecutable, [])"]
        M -- No --> O["return (executable, tail)"]
    end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A["Session Restore: resumeBindingForSessionRestore()"] --> B{binding exists?}
    B -- No --> Z["return nil (no restore)"]
    B -- Yes --> C{isAgentHookBinding AND\nshouldPreferRestorableAgentSnapshot?}
    C -- Yes --> D["return nil\n(discard stale binding)"]
    C -- No --> E{isAgentHookBinding AND\nrestorableAgent exists?}
    E -- No --> F["return binding as-is"]
    E -- Yes --> G{checkpointId == sessionId?}
    G -- No --> F
    G -- Yes --> H{kind match?}
    H -- No --> F
    H -- Yes --> I["return binding with\nreconciled working directory"]
    D --> J["restorableAgentForSessionRestore(snapshot, nil)"]
    J --> K["return snapshot as-is"]
    subgraph commandParts
        L["executablePath or argv[0]"] --> M{executableLooksLikeShell?}
        M -- Yes --> N["return (fallbackExecutable, [])"]
        M -- No --> O["return (executable, tail)"]
    end
Loading

Comments Outside Diff (1)

  1. Sources/Workspace.swift, line 933-941 (link)

    P2 Timestamp comparison discards binding before kind/session checks

    shouldPreferRestorableAgentSnapshot compares timestamps alone and, when it returns true, exits resumeBindingForSessionRestore with nil before reaching the checkpointId and kind-match guards. A snapshot whose capturedAt is newer than the binding's updatedAt will cause the binding to be dropped even if the snapshot is for a different agent kind or session ID than the one stored in checkpointId. With the old code those mismatches caused a fail-closed (no resume); with the new code the snapshot is passed through unconditionally by restorableAgentForSessionRestore(snapshot, nil).

    In the fish-bootstrap scenario this is exactly right (same session, snapshot holds the correct binary). But there's no guard preventing a recently-captured codex snapshot from discarding a valid claude hook binding on the same surface. Adding an early kind check — or at least a comment explaining that cross-kind displacement is intentional — would make the invariant explicit.

Reviews (2): Last reviewed commit: "Merge remote-tracking branch 'origin/mai..." | Re-trigger Greptile

@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
`@Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/AgentLaunchCaptureTrust.swift`:
- Around line 101-107: The shell-wrapper detection in AgentLaunchCaptureTrust
should not stop at the first non-flag argument, because value-taking shell
options like "-o" can make operands such as "pipefail" look like the command
payload. Update the argument scan in the trust-check logic to recognize known
shell options that consume a following value before deciding to return false, so
cases like bash "-o" "pipefail" "-c" "exec claude" are still detected as shell
bootstrap commands. Use the existing shellCommandStringFlag path as the anchor
and extend the loop to parse option/value pairs before treating a non-flag as
the payload.

In `@Sources/RestorableAgentSession.swift`:
- Around line 1263-1265: `trustedLaunchCommand` is creating a new
`AgentLaunchCaptureTrust` for every record, which repeats its lookup-table
initialization on the load path. Hoist or reuse a single
`AgentLaunchCaptureTrust` instance in the restore flow and pass it into
`trustedLaunchCommand` (or otherwise cache it in the caller) so the trust checks
use the shared instance instead of constructing one per session record.
🪄 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: bed5fcee-31b5-413c-a931-687b3951dcae

📥 Commits

Reviewing files that changed from the base of the PR and between 1e4b334 and 1cb4ad2.

⛔ Files ignored due to path filters (1)
  • .github/swift-file-length-budget.tsv is excluded by !**/*.tsv
📒 Files selected for processing (9)
  • CLI/cmux.swift
  • Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/AgentLaunchCaptureTrust.swift
  • Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/AgentResumeArgv.swift
  • Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/AgentLaunchCaptureTrustTests.swift
  • Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/AgentResumeArgvTests.swift
  • Sources/RestorableAgentSession.swift
  • Sources/Workspace.swift
  • cmuxTests/AgentSessionAutoResumeSwiftTests.swift
  • scripts/lint-namespace-types-baseline.txt
💤 Files with no reviewable changes (1)
  • scripts/lint-namespace-types-baseline.txt

Comment thread Sources/RestorableAgentSession.swift Outdated

@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: 1

♻️ Duplicate comments (1)
Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/AgentLaunchCaptureTrust.swift (1)

99-116: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Value-taking shell options still defeat detection.

["bash", "-o", "pipefail", "-c", "exec claude"] returns false: -o is not a command-string flag nor a startup-only flag, so the loop hits pipefail (a non-flag operand) and returns false, letting a shell bootstrap survive the restore trust filter. Skip operands for known value-taking shell options (-o, -c value handling) before treating the first non-flag token as the command payload.

🤖 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
`@Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/AgentLaunchCaptureTrust.swift`
around lines 99 - 116, The shell-wrapper detection in argvLooksLikeShell
currently treats value-taking options like -o and -c as if the next token were a
non-flag operand, which can incorrectly return false for real shell invocations.
Update argvLooksLikeShell to recognize known value-taking shell options and skip
their associated argument(s) before applying the non-flag/-- rejection logic,
while keeping the existing shellCommandStringFlag and shellStartupOnlyFlag
checks intact. Use the existing symbols argvLooksLikeShell,
shellCommandStringFlag, shellStartupOnlyFlag, and executableLooksLikeShell to
locate and adjust the parsing flow.
🤖 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
`@Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/AgentLaunchCaptureTrust.swift`:
- Around line 110-113: The `isValueTakingOption` logic in
`AgentLaunchCaptureTrust` has a redundant `if` branch because both paths return
false, so the `--` versus non-flag distinction is currently ignored. Either
restore the intended special handling for `--`/non-flag arguments in that
method, or simplify the `argument == "--" || !argument.hasPrefix("-")` check to
a single unconditional false return if no differentiation is needed.

---

Duplicate comments:
In
`@Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/AgentLaunchCaptureTrust.swift`:
- Around line 99-116: The shell-wrapper detection in argvLooksLikeShell
currently treats value-taking options like -o and -c as if the next token were a
non-flag operand, which can incorrectly return false for real shell invocations.
Update argvLooksLikeShell to recognize known value-taking shell options and skip
their associated argument(s) before applying the non-flag/-- rejection logic,
while keeping the existing shellCommandStringFlag and shellStartupOnlyFlag
checks intact. Use the existing symbols argvLooksLikeShell,
shellCommandStringFlag, shellStartupOnlyFlag, and executableLooksLikeShell to
locate and adjust the parsing flow.
🪄 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: 72ab95cf-efa7-4ae3-9b00-fad16ea566b2

📥 Commits

Reviewing files that changed from the base of the PR and between 1cb4ad2 and 4f4e86c.

📒 Files selected for processing (6)
  • Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/AgentLaunchCaptureTrust.swift
  • Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/AgentLaunchCaptureTrustTests.swift
  • Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/AgentResumeArgvTests.swift
  • Sources/RestorableAgentSession.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/RestorableAgentLaunchTrustTests.swift

…ell-integration-ships-so-claude-byp

# Conflicts:
#	.github/swift-file-length-budget.tsv
@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 — b69993b7 Deployed Jul 5, 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.

fish: no shell integration ships, so claude bypasses the wrapper — session restore degrades to bash --resume <id> (invalid option) and loses cwd

3 participants