Skip to content

Fix Claude Code auto-resume under a fish login shell (#6285) - #6866

Closed
austinywang wants to merge 8 commits into
mainfrom
issue-6285-resume-binding-cwd-guard-uses-posix
Closed

austinywang wants to merge 8 commits into
mainfrom
issue-6285-resume-binding-cwd-guard-uses-posix

Conversation

@austinywang

@austinywang austinywang commented Jun 26, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #6285

Problem

On 0.64.16 (which already includes the fish shell integration from #5678), Claude Code sessions still fail to auto-resume when the login shell is fish. The executable resolves correctly to claude --resume <id> (so this is not the bash --resume problem of #5796), but the resume binding's command carries a POSIX { cd …; } && … cwd-guard (emitted whenever cwd is non-null), and the restore launcher dispatches it to the login shell:

fish: '{ ... }' is not supported for grouping commands. Please use 'begin; ...; end'
{ cd -- '<dir>' 2>/dev/null || [ ! -d '<dir>' ]; } && /bin/sh -c '… claude --resume <id>'

fish has no { …; } command grouping (it uses begin; …; end), so it errors before claude launches and the tab drops to a bare fish prompt. zsh/bash parse the guard fine — this is specific to fish (and csh/tcsh) as login shell. This is a distinct failure mode from #5796 and is not resolved by #5678.

Root cause

TerminalStartupReturnShellScript.commandThenReturnLines (the session-restore launcher) dispatches the resume command to the user's login shell via $SHELL -c <command> for its csh|tcsh and * (fish) branches. The #5639 fix already wraps the inner claude command in /bin/sh -c '…' so the POSIX-only wrapper token parses in non-POSIX shells — but it deliberately left the { cd …; } cwd-guard outside that wrap (so cd-prefix rewriting keeps composing), and that outer guard is itself fish-incompatible.

Fix

Hand the non-POSIX login shells a single external /bin/sh -c '<command>' invocation (the csh|tcsh and * case branches) instead of the raw POSIX command. Every shell parses one external command identically; the user's shell still sources its own config (PATH/env) before exec'ing /bin/sh, which inherits it; and the cwd-guard runs inside /bin/sh exactly as it does on zsh/bash — so the session resumes in its working directory (an improvement over the cwd: null user workaround, which dropped the cd). zsh/bash keep running the command natively under -lic.

This is the issue's proposed fix #2 (wrap the dispatched command), applied at the dispatch seam — so the stored binding, the command generators, and the cd-prefix canonicalizers are all unchanged.

Verification

/bin/tcsh is a faithful non-POSIX proxy for fish (same { …; } limitation) and is present on every macOS runner. Empirically:

  • Without the wrap, tcsh fails on the guard-bearing command: rc=1, {: Command not found.
  • With the wrap, the guard-bearing resume runs cd then reaches the cmux wrapper: rc=0.

The same /bin/sh -c '…' form is what the reporter independently verified resumes correctly under fish.

Tests (two-commit red/green)

  1. testResumeLauncherWrapsCommandForNonPOSIXLoginShells (unit, CI-active, no fish needed) — asserts commandThenReturnLines wraps the command in /bin/sh -c for the fish/csh/tcsh branches while zsh/bash keep running it natively under -lic. Committed first as a failing test (red), then the fix (green).
  2. testClaudeResumeCommandWithWorkingDirectoryExecutesThroughWrapperInsideTcshLauncher (end-to-end, CI-active) — runs the guard-bearing resume through /bin/tcsh and asserts it reaches the cmux wrapper with --settings.
  3. Updated the existing runClaudeResumeCommand test harness to model the production dispatch (wrap for non-POSIX shells), and corrected the stale Claude resume still drops cmux hooks after #5430: bare claude doesn't resolve to the wrapper inside the $SHELL -lic restore launcher #5639 fish-test comment that incorrectly claimed fish parses { …; }.

Scope / localization

No user-facing strings are added or changed (only shell command generation, code comments, and tests), so no Localizable.xcstrings / web message-catalog updates are required.

🤖 Generated with Claude Code


Summary by cubic

Fixes Claude Code auto-resume under fish by running the resume command through /bin/sh for non-POSIX login shells, so sessions restore in the correct working directory. Fixes #6285.

  • Bug Fixes

    • For fish, csh, and tcsh, dispatch a single /bin/sh -c '<command>' instead of the raw POSIX command; zsh/bash keep -lic.
  • Refactors

    • Extracted the POSIX wrapper helper to file scope for reuse and clarity.
    • Moved non-POSIX resume launcher tests to Swift Testing, retaining the wrapper-selection unit test and /bin/tcsh end-to-end resume test; updated the harness and file-length budget.

Written for commit 50ff976. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Bug Fixes
    • Improved session restore behavior in non-POSIX login shells, so resume commands now launch correctly in shells like csh, tcsh, and fish.
    • Kept existing behavior unchanged for zsh and bash while avoiding shell parsing errors during resume.
    • Fixed cases where restoring a session with a working directory could fail due to shell command-grouping incompatibilities.

cmux and others added 2 commits June 26, 2026 01:21
… login shells

The Claude Code resume binding carries a POSIX `{ cd …; } && …` cwd-guard.
fish (and csh/tcsh) cannot parse `{ …; }` command grouping, so when the
session-restore launcher dispatches the command via `$SHELL -c <command>` to a
fish login shell it errors before the agent launches and the tab drops to a bare
fish prompt.

This test asserts commandThenReturnLines hands fish/csh/tcsh a single external
`/bin/sh -c '<command>'` invocation while zsh/bash keep running the command
natively. It fails until the accompanying fix lands.

Test-only commit (red) per the regression test commit policy.

Refs #6285

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Claude Code resume binding carries a POSIX `{ cd …; } && …` cwd-guard
(emitted whenever cwd is non-null). The session-restore launcher
(TerminalStartupReturnShellScript.commandThenReturnLines) dispatched that
command to the user's login shell via `$SHELL -c <command>`. zsh/bash parse it
natively, but fish rejects `{ …; }` command grouping ("'{ ... }' is not
supported for grouping commands. Please use 'begin; ...; end'") and csh/tcsh
cannot parse `{ …; }`/`2>` either, so the resume errored out before the agent
launched and the tab dropped to a bare login-shell prompt — the #6285 failure,
distinct from the #5796 executable-resolution problem and not fixed by #5678.

Hand the non-POSIX shells (the `csh|tcsh` and `*` case branches) a single
external `/bin/sh -c '<command>'` invocation instead. Every shell parses one
external command identically; the user's shell still sources its own config
(PATH/env) before exec'ing /bin/sh, which inherits it; and the cwd-guard runs
inside /bin/sh exactly as on zsh/bash, so the session still resumes in its
working directory (an improvement over the cwd:null user workaround). zsh/bash
keep running the command natively under -lic.

Verified end-to-end with /bin/tcsh (a non-POSIX proxy for fish, present on every
macOS runner): the unwrapped guard-bearing command fails with
"{: Command not found"; the wrapped form resumes through the cmux wrapper.

Adds a CI-active tcsh end-to-end regression plus faithful test dispatch
modelling; corrects the stale #5639 fish test comment.

Fixes #6285

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@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 3:20am
cmux-staging Building Building Preview, Comment Jul 5, 2026 3:20am

@coderabbitai

coderabbitai Bot commented Jun 26, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The resume-launch script now wraps POSIX-only commands in /bin/sh -c for non-POSIX login shells, while keeping zsh and bash on the native path. The regression tests were updated to cover tcsh, fish, and shell-specific dispatch.

Changes

Resume command dispatch

Layer / File(s) Summary
Shell dispatch quoting
Sources/SessionPersistence.swift
commandThenReturnLines now selects native resume text for zsh and bash, and /bin/sh -c wrapping for csh, tcsh, and the default shell branch; new helpers quote the wrapped command for POSIX shell execution.
Resume launcher regressions
cmuxTests/SessionPersistenceTests.swift, cmuxTests/AgentSessionAutoResumeSettingsTests.swift
The resume-launcher tests now assert the wrapper behavior for tcsh, fish, zsh, and bash, and the shared runner dispatches the resume command differently for POSIX and non-POSIX shells.

Sequence Diagram(s)

sequenceDiagram
  participant Launcher as restore launcher
  participant Script as TerminalStartupReturnShellScript
  participant Shell as login shell
  participant Sh as /bin/sh

  Launcher->>Script: commandThenReturnLines(command, workingDirectory)
  alt zsh or bash
    Script-->>Launcher: resumeCommand
    Launcher->>Shell: run native POSIX resume command
  else csh, tcsh, fish, other
    Script-->>Launcher: /bin/sh -c 'resumeCommand'
    Launcher->>Shell: run wrapper command
    Shell->>Sh: invoke /bin/sh -c
    Sh->>Launcher: execute original resume command
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

  • Issue 6285: The same resume-command path is updated to avoid raw POSIX grouping in non-POSIX shells and route those shells through /bin/sh -c.

Possibly related PRs

  • manaflow-ai/cmux#4777: Both PRs touch TerminalStartupReturnShellScript and its launcher-script output when return-to-login-shell behavior is enabled.
  • manaflow-ai/cmux#6515: Both PRs modify return-shell script generation in Sources/SessionPersistence.swift, including the zsh reentry path and launcher command text.

Suggested reviewers

  • lawrencecchen

Poem

A bunny tapped the shell-script log,
And hopped through sh beside the fog.
tcsh said "no," but /bin/sh gleamed,
While bash and zsh kept right on beam'd.
🐇✨ The resume path now purrs along.


Important

Pre-merge checks failed

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

❌ Failed checks (1 error, 2 warnings)

Check name Status Explanation Resolution
Cmux No Ambient Global State ❌ Error Sources/SessionPersistence.swift:1330 adds posixShellDispatchCommand to the static-only TerminalStartupReturnShellScript enum, expanding a namespace of static helpers. Move the wrapper/quoting helper to a private/fileprivate file-scope function or onto an owning launcher/quoting type, and use it from the dispatch seam.
Docstring Coverage ⚠️ Warning Docstring coverage is 72.73% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ⚠️ Warning The description is substantive but omits the required template sections and headings, so it does not match the repository format. Add the required template sections: Summary, Testing, Demo Video, Review Trigger, and Checklist, even if some entries are brief.
✅ Passed checks (22 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 Production change stays in a nonisolated enum and only adds pure shell-string helpers; no MainActor/UI/shared-mutable Sendable issue is introduced.
Cmux Swift Blocking Runtime ✅ Passed Changed production code only adds shell-string quoting; no new waits/locks/sleeps. The lone semaphore wait is in test-only bounded-wait scaffolding, which is allowed.
Cmux Browser Automation Off-Main ✅ Passed PASS: The PR only changes session-persistence shell resume logic and tests; no browser.* routing, WebKit/AppKit, or socketWorkerMethods changes are present in the touched files.
Cmux Expensive Synchronous Load ✅ Passed Diff only changes shell-command assembly and tests; no expensive agent-history load, JSON parse, or main-actor interactive scan was added or moved.
Cmux Cache Substitution Correctness ✅ Passed Diff only changes shell resume dispatch and tests; no authoritative-read was replaced by a cached/opportunistic value in a persistence/history/snapshot path.
Cmux No Hacky Sleeps ✅ Passed Only Swift files changed; the lone timeout is a test-only bounded wait helper, not a production TS/JS/shell runtime sleep.
Cmux Algorithmic Complexity ✅ Passed The production change only rewrites constant-size launcher branches and string quoting; no scalable collection scans, rescan loops, or hot-path filtering were introduced.
Cmux Swift Concurrency ✅ Passed Diff only changes shell-command generation and XCTest coverage; no new DispatchQueue, Combine, completion-handler, or fire-and-forget Task patterns appear.
Cmux Swift @Concurrent ✅ Passed The PR only adds synchronous shell-dispatch helpers and tests; no new async/@Concurrent actor-isolation issues appear in the changed Swift code.
Cmux Swift File And Package Boundaries ✅ Passed Focused ~33-line fix in existing 2109-line SessionPersistence.swift; no new oversized production file or new app-target feature boundary introduced.
Cmux Swiftpm Lockfiles ✅ Passed PR diff only changes Swift source/tests and the file-length budget; no .gitignore, Package.resolved, Xcode, workflow, or dependency files were touched.
Cmux Swift Logging ✅ Passed No added production logging found: SessionPersistence.swift has no print/debugPrint/dump/NSLog/Logger usage; the only print is in a test fixture.
Cmux User-Facing Error Privacy ✅ Passed No production user-facing error/alert/output text was added; changes only adjust shell dispatch code and test/developer comments, which the rule allows.
Cmux Full Internationalization ✅ Passed The PR only changes internal shell-command generation and tests/comments; no user-facing Swift/web copy or locale assets were added.
Cmux Swiftui State Layout ✅ Passed The touched files are SessionPersistence utilities/tests only; no SwiftUI views, ObservableObject/@published, GeometryReader, lazy rows, or render-time state writes appear.
Cmux Architecture Rethink ✅ Passed Localized shell-dispatch correctness fix with clear owner/invariant; no sleeps, observers, split ownership, or other rethink-patterns were introduced.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR only changes SessionPersistence logic and tests; no NSWindow/NSPanel/WindowGroup code or cmuxAuxiliaryWindowIdentifiers changes, so the window-shortcut rule isn’t implicated.
Cmux Source Artifacts ✅ Passed Changed paths are source/test Swift files only; no artifact directories, logs, caches, or generated outputs were added.
Cmux No Test Or Debug Seam In Production Source ✅ Passed The added shell-dispatch helper is used by production code, with no #if DEBUG or test-only/debug-only accessor introduced in Sources/SessionPersistence.swift.
Title check ✅ Passed The title clearly identifies the bug fix for Claude Code auto-resume under a fish login shell.
✨ 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-6285-resume-binding-cwd-guard-uses-posix

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.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Jun 26, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

Fixes Claude Code auto-resume failures under fish, csh, and tcsh login shells by wrapping the resume command in /bin/sh -c '<command>' for non-POSIX shells before dispatching it from the zsh launcher script. zsh and bash continue to receive the raw POSIX command under -lic as before.

  • Sources/SessionPersistence.swift: Two new private helper functions (posixShellDispatchCommand, posixSingleQuoted) produce the /bin/sh -c '<posix-wrapped-command>' invocation, and commandThenReturnLines now uses that wrapped form for the csh|tcsh and * (fish) case branches, leaving the zsh|bash branch unchanged.
  • cmuxTests/ResumeLauncherNonPOSIXTests.swift: Adds a unit test asserting the correct branch output for each shell family, plus an integration test that actually runs the guard-bearing resume command through /bin/tcsh (a safe macOS proxy for fish's { …; } incompatibility) and confirms the cmux wrapper is reached.
  • .github/swift-file-length-budget.tsv / cmux.xcodeproj/project.pbxproj: Updated line-count budget and Xcode project membership for the new test file.

Confidence Score: 5/5

Minimal-footprint fix touching only the non-POSIX shell dispatch branch of commandThenReturnLines; zsh/bash behaviour is entirely unchanged.

The two-layer quoting (posixSingleQuoted for the inner /bin/sh payload, TerminalStartupShellQuoting.singleQuoted for the outer zsh-script embedding) is correct and verified by both a unit test that checks the generated script text and an end-to-end test that runs the command through /bin/tcsh. The private helpers are pure functions with no ambient state, the DispatchSemaphore in the test harness is standard bounded-wait scaffolding, and no production actor isolation or logging paths are touched.

No files require special attention.

Important Files Changed

Filename Overview
Sources/SessionPersistence.swift Adds posixShellDispatchCommand/posixSingleQuoted private helpers and updates the non-POSIX shell branches of commandThenReturnLines to wrap the resume command in /bin/sh -c; logic and quoting are correct.
cmuxTests/ResumeLauncherNonPOSIXTests.swift New Swift Testing suite with a unit test for branch-output format and a /bin/tcsh end-to-end integration test; DispatchSemaphore usage is acceptable test-only scaffolding.
.github/swift-file-length-budget.tsv Routine budget update reflecting the small growth in SessionPersistence.swift and GhosttySurfaceView.swift line counts.
cmux.xcodeproj/project.pbxproj Adds ResumeLauncherNonPOSIXTests.swift to the Xcode test target; one inserted line has slightly different indentation than its neighbours, but this is cosmetic and matches the indentation of the pre-existing C3408A line.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant ZSH as zsh launcher script
    participant SHELL as Login Shell ($SHELL)
    participant SH as /bin/sh
    participant Claude as claude (agent)

    ZSH->>ZSH: "_cmux_resume_shell="${SHELL}""
    alt zsh or bash
        ZSH->>SHELL: "$SHELL -lic '<POSIX resume command>'"
        SHELL->>Claude: exec claude --resume id
    else fish / csh / tcsh
        ZSH->>SHELL: $SHELL -c '/bin/sh -c POSIX-resume-command'
        SHELL->>SH: "/bin/sh -c '<POSIX resume command>'"
        SH->>Claude: exec claude --resume id
    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"}}}%%
sequenceDiagram
    participant ZSH as zsh launcher script
    participant SHELL as Login Shell ($SHELL)
    participant SH as /bin/sh
    participant Claude as claude (agent)

    ZSH->>ZSH: "_cmux_resume_shell="${SHELL}""
    alt zsh or bash
        ZSH->>SHELL: "$SHELL -lic '<POSIX resume command>'"
        SHELL->>Claude: exec claude --resume id
    else fish / csh / tcsh
        ZSH->>SHELL: $SHELL -c '/bin/sh -c POSIX-resume-command'
        SHELL->>SH: "/bin/sh -c '<POSIX resume command>'"
        SH->>Claude: exec claude --resume id
    end
Loading

Reviews (7): Last reviewed commit: "Move POSIX resume wrapper helper to file..." | Re-trigger Greptile

The GitHub Actions check suite was not created for the prior commit (missed
pull_request synchronize event under heavy org CI load); this empty commit
forces a fresh synchronize so required checks run on the budget-fixed tree.
Squash-merge discards this commit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@austinywang austinywang reopened this Jun 26, 2026
…ding-cwd-guard-uses-posix

# Conflicts:
#	.github/swift-file-length-budget.tsv
@blacksmith-sh

This comment has been minimized.

@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 — 50ff9765 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.

Resume binding cwd-guard uses POSIX { cd ...; } grouping that fish cannot parse (persists after #5678)

3 participants