Skip to content

Launch workspace initial commands via the user's login shell - #8801

Merged
azooz2003-bit merged 2 commits into
mainfrom
feat-task-login-shell-path
Jul 24, 2026
Merged

azooz2003-bit merged 2 commits into
mainfrom
feat-task-login-shell-path

Conversation

@azooz2003-bit

@azooz2003-bit azooz2003-bit commented Jul 24, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes the iOS "New Task" failure where every template command died with Error: claude not found in PATH.

Bug. Workspace-create initial_command (iOS task composer, CLI cmux workspace create --command) was passed verbatim to Ghostty, which launches string commands as /usr/bin/login -flp <user> /bin/bash --noprofile --norc -c "exec -l <cmd>". No shell profile runs, so PATH is the app-inherited launchd default plus the per-surface cmux-cli-shims dir. The claude shim is found and runs, but its find_real_claude scans that same bare PATH, misses /opt/homebrew/bin/claude, prints the error, and exits 127.

Fix. v2PrepareWorkspaceCreateExecution now wraps a non-blank initial_command via the new WorkspaceInitialCommandLoginShell helper into '<usershell>' -lc '<payload>'. The payload re-prepends $CMUX_CLAUDE_WRAPPER_SHIM_ROOT to PATH (login profiles prepend /opt/homebrew/bin, which would otherwise shadow the cmux claude/codex wrapper hooks) and then runs the original command verbatim. fish gets fish syntax; unknown shells fall back to /bin/zsh with the POSIX payload. This matches the login-shell wrapping that session restore (SessionRestoredTerminalCommandStore) and the right-dock (DockSplitStore+TerminalStartup) already do; first launch was the only path executing user commands with a bare environment.

Commit 1 adds the regression test only (CI red), commit 2 adds the fix (CI green). Pure string transform: no temp files, respawn-safe, nothing user-facing to localize.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.


Summary by cubic

Run workspace initial commands through the user’s login shell so PATH and profiles load correctly. Fixes the iOS “New Task” failure where commands like claude were not found.

  • Bug Fixes
    • Wrap non-empty initial_command via the user’s shell with -lc, preserving the original command, whitespace, and newlines.
    • Re-prepend $CMUX_CLAUDE_WRAPPER_SHIM_ROOT to PATH so cmux Claude/Codex wrapper hooks stay active.
    • Use fish-specific syntax when the shell is fish; otherwise use POSIX syntax, falling back to /bin/zsh.
    • Align first-launch behavior with session restore and right-dock startup.
    • Added tests for shell selection/fallback, PATH shim injection, single-quote escaping, whitespace/newline preservation, and blank-command handling.

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

Review in cubic

Summary by CodeRabbit

  • New Features

    • Workspace startup commands now run through the system’s configured login shell.
    • Supports common shells (zsh, bash, fish, sh, ksh, dash) with a safe fallback.
    • Preserves original command content, including whitespace and newlines, with robust escaping.
    • Ensures required command-line integration hooks remain available after shell startup.
  • Bug Fixes

    • Improved consistency for workspace creation with initial commands across repeated and concurrent scenarios.

@coderabbitai

coderabbitai Bot commented Jul 24, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Workspace creation now wraps non-empty initial commands in supported login-shell invocations, preserves command content through quoting, includes the helper in both build targets, and updates tests for shell selection, shim paths, whitespace, newlines, and concurrency.

Changes

Workspace initial command wrapping

Layer / File(s) Summary
Login-shell command wrapper
Sources/WorkspaceInitialCommandLoginShell.swift
Resolves the user shell, selects supported shells with zsh fallback, preserves command payloads, applies the shim path, and quotes embedded single quotes.
Workspace creation integration and build wiring
Sources/TerminalController+WorkspaceCreateExecutionPreparation.swift, cmux.xcodeproj/project.pbxproj
Trims initial commands, omits empty values, wraps non-empty commands, and adds the helper to both source phases.
Workspace command behavior tests
cmuxTests/WorkspaceCreateWorkingDirectoryTests.swift
Tests login-shell wrapping, shell fallbacks, quoting, whitespace and newline preservation, idempotency, and concurrent creation.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant WorkspaceCreation
  participant WorkspaceCreateExecutionPreparation
  participant WorkspaceInitialCommandLoginShell
  participant LoginShell
  WorkspaceCreation->>WorkspaceCreateExecutionPreparation: prepare initial command
  WorkspaceCreateExecutionPreparation->>WorkspaceInitialCommandLoginShell: wrap non-empty command
  WorkspaceInitialCommandLoginShell->>LoginShell: resolve shell and build -lc payload
  LoginShell-->>WorkspaceInitialCommandLoginShell: return wrapped command
  WorkspaceInitialCommandLoginShell-->>WorkspaceCreateExecutionPreparation: provide wrapped command
  WorkspaceCreateExecutionPreparation-->>WorkspaceCreation: store initial command
Loading

Suggested reviewers: lawrencecchen


Important

Pre-merge checks failed

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

❌ Failed checks (2 errors, 2 warnings)

Check name Status Explanation Resolution
Cmux Swift Package Boundaries ❌ Error The new shell-wrapping helper is pure, unit-tested domain logic added under app Sources, not a package target; similar launch logic already lives in SwiftPM packages. Extract WorkspaceInitialCommandLoginShell into a small SwiftPM target (e.g. CmuxTerminal or a new CmuxWorkspaceLaunch) and keep the app target as glue only.
Cmux No Ambient Global State ❌ Error Sources/WorkspaceInitialCommandLoginShell.swift adds a caseless enum used only as a static helper namespace. Move shell resolution/wrapping onto a constructable, injectable owner (e.g. TerminalController/workspace-create prep) and inject it at the workspace-creation seam.
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% 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 covers the change and rationale, but it omits required template sections like Testing, Demo Video, Review Trigger, and Checklist. Add the missing template sections, including testing details, a Demo Video or N/A note, the Review Trigger block, and checklist items.
✅ Passed checks (21 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 The PR adds a pure string wrapper used from MainActor TerminalController code; it introduces no new Sendable shared state, protocols, or background UI-store access.
Cmux Swift Blocking Runtime ✅ Passed PASS: The only new timing primitive is a 10ms Task.sleep in a test loop; the production Swift changes add no waits, semaphores, syncs, polling, or locks.
Cmux Browser Automation Off-Main ✅ Passed Diff only wraps workspace initial_command and adds tests; no browser.* routing, WebKit/AppKit worker hops, or socket-worker policy changes were introduced.
Cmux Expensive Synchronous Load ✅ Passed Diff only wraps workspace initial_command; no new agent-history/index loads or heavy JSON/JSONL parsing were added to main-actor or interactive paths.
Cmux Cache Substitution Correctness ✅ Passed No fresh authoritative read was swapped for a cache/opportunistic value in any persistence/history/undo/snapshot path; the PR only wraps initial commands and adds tests.
Cmux No Hacky Sleeps ✅ Passed Only Swift source, tests, and project metadata changed; no covered runtime script or non-test sleep/delay logic was introduced.
Cmux Algorithmic Complexity ✅ Passed PASS: The production changes only trim/quote a string and switch on a resolved shell; no scalable collections, nested scans, or hot-path sorting/filtering were introduced.
Cmux Swift Concurrency ✅ Passed No new production async patterns were introduced; the new helper is synchronous string wrapping, and the Task/continuation usage is confined to XCTest-bound tests.
Cmux Swift @Concurrent ✅ Passed PASS: The new login-shell wrapper and workspace-create change are synchronous/pure; no nonisolated async or misplaced @concurrent was introduced on the @MainActor controller.
Cmux Swiftpm Lockfiles ✅ Passed HEAD only adds a Swift source file to cmux.xcodeproj; no Package.swift, .gitignore, or SwiftPM package-reference changes were present, so no Package.resolved diff was required.
Cmux Swift Logging ✅ Passed The changed production Swift files add no print/debugPrint/dump/NSLog/Logger calls; the only print-like text is shell code in tests and wrapper payload strings.
Cmux User-Facing Error Privacy ✅ Passed Diff adds shell-wrapping logic and tests only; no new user-facing errors, alerts, or API error bodies expose vendor/internal details.
Cmux Full Internationalization ✅ Passed PASS: The changed production Swift adds login-shell wrapping only; no new user-facing Swift text, string-catalog, Info.plist, or web message changes were introduced.
Cmux Swiftui State Layout ✅ Passed The PR only changes terminal/workspace command prep and tests; no SwiftUI view/state/layout patterns from the rule are introduced or expanded.
Cmux Architecture Rethink ✅ Passed PASS: The change is a pure login-shell string wrapper on the shared workspace-create path; no sleeps, observers, duplicate wiring, or split ownership were added.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR only changes workspace command wrapping and tests; it adds no NSWindow/NSPanel/WindowGroup code or cmuxAuxiliaryWindowIdentifiers changes.
Cmux Source Artifacts ✅ Passed The diff only touches source, tests, and Xcode project config; no logs, caches, build output, temp dirs, or other artifact paths are introduced.
Cmux No Test Or Debug Seam In Production Source ✅ Passed No test/debug seam was added to production Sources; the PR adds a login-shell wrapper and a call site, while debug-only APIs remain in Tests or existing code.
Title check ✅ Passed The title accurately summarizes the main change: launching initial workspace commands through the user's 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 feat-task-login-shell-path

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 Jul 24, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a bug where workspace initial_commands (iOS "New Task" flow, CLI cmux workspace create --command) would fail with claude not found in PATH because Ghostty launches them via bare Bash with --noprofile --norc, skipping user profile setup. The fix wraps every non-blank initial command through the user's login shell (-lc) and re-prepends $CMUX_CLAUDE_WRAPPER_SHIM_ROOT to PATH so cmux wrapper hooks stay active. This aligns first-launch behavior with session restore and right-dock startup, which already do this wrapping.

  • WorkspaceInitialCommandLoginShell.swift (new): Reads the user's shell from the password database (getpwuid) with fallback to $SHELL and /bin/zsh; constructs a login-shell-wrapped payload for fish (fish syntax) or any POSIX-compatible shell; escapes the payload via '...' quoting with '"'"' substitution for embedded single quotes.
  • TerminalController+WorkspaceCreateExecutionPreparation.swift: Replaces the bare pass-through of initial_command with a call to WorkspaceInitialCommandLoginShell.wrap() on any non-blank command string.
  • Tests: Comprehensive unit tests for exact output snapshots across zsh, bash, fish, nil/unknown-shell fallback, single-quote escaping, and end-to-end integration covering both the synchronous v2WorkspaceCreate and concurrent mobile paths.

Confidence Score: 5/5

Safe to merge — the change is a pure string transform applied at a single call site, with no side effects on non-command workspace creates and good test coverage for all shell paths and edge cases.

The fix is narrow and well-contained: one call site in v2PrepareWorkspaceCreateExecution now wraps any non-blank initial_command through the user's login shell before handing it to Ghostty. The quoting logic is correct, the fallback chain is sensible, and the fish/POSIX split handles the two meaningful shell families. All affected test assertions were updated and new snapshot tests pin every shell branch.

No files require special attention.

Important Files Changed

Filename Overview
Sources/WorkspaceInitialCommandLoginShell.swift New helper that wraps initial commands via the user's login shell; correct quoting, proper fish/POSIX split, reasonable fallback chain.
Sources/TerminalController+WorkspaceCreateExecutionPreparation.swift Three-line change: bare pass-through of initial_command replaced by WorkspaceInitialCommandLoginShell.wrap(); blank-command guard unchanged.
cmuxTests/WorkspaceCreateWorkingDirectoryTests.swift Tests updated to assert wrapped command shape; new unit tests cover all supported shells, fallback paths, quoting edge cases, and end-to-end integration.
cmux.xcodeproj/project.pbxproj Standard Xcode project file additions registering WorkspaceInitialCommandLoginShell.swift in file references and build phases.

Reviews (2): Last reviewed commit: "Launch workspace initial commands via us..." | Re-trigger Greptile

import Darwin
import Foundation

enum WorkspaceInitialCommandLoginShell {

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 as static-function namespace

WorkspaceInitialCommandLoginShell is a caseless enum whose entire API is static functions — the exact shape flagged by the cmux-no-ambient-global-state rule. Because there are no cases, the type can never be constructed or injected, making it a static-only namespace bag. The preferred shape per the rule is a constructable, injectable owning type (a struct with an init, or the wrap logic moved as extension methods onto a type that already owns the wrapping concern). This is a new top-level type introduced by this PR.

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!

Ghostty launches string commands through a profile-free Bash, leaving task agents with the app-inherited PATH. Resolve the user login shell, run the original command through -lc, and re-prepend the per-surface shim directory so cmux wrapper hooks stay active. Session restore and dock startup already use login-shell wrapping for the same profile-loading behavior.
@azooz2003-bit
azooz2003-bit force-pushed the feat-task-login-shell-path branch from 0b132cf to d506ed0 Compare July 24, 2026 03:09

@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

🤖 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/WorkspaceInitialCommandLoginShell.swift`:
- Around line 4-67: Replace the static-only WorkspaceInitialCommandLoginShell
enum with a constructable wrapper type that stores the resolved shell
dependency, allowing tests to inject it directly. Move default login-shell
resolution to the workspace-creation ownership boundary, and make the wrapper’s
command-wrapping behavior instance-based while preserving shell selection and
payload generation.
🪄 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 Plus

Run ID: 1e6bd38c-91e7-48d1-9354-12208375aedf

📥 Commits

Reviewing files that changed from the base of the PR and between 0b132cf and d506ed0.

📒 Files selected for processing (4)
  • Sources/TerminalController+WorkspaceCreateExecutionPreparation.swift
  • Sources/WorkspaceInitialCommandLoginShell.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/WorkspaceCreateWorkingDirectoryTests.swift

Comment on lines +4 to +67
enum WorkspaceInitialCommandLoginShell {
/// Resolves the user's login shell and wraps an externally supplied workspace command.
///
/// Ghostty otherwise launches string commands through Bash with `--noprofile --norc`,
/// so the user's profile cannot contribute tools such as Homebrew-installed agents.
/// Ghostty prepends `exec -l`, so the returned command starts with the quoted shell path.
static func wrap(_ command: String) -> String {
let databaseShell: String?
if let record = getpwuid(getuid()),
let shell = record.pointee.pw_shell {
let copiedShell = String(cString: shell)
databaseShell = copiedShell.isEmpty ? nil : copiedShell
} else {
databaseShell = nil
}

let userShell = databaseShell
?? ProcessInfo.processInfo.environment["SHELL"]
?? "/bin/zsh"
return wrap(command, userShell: userShell)
}

/// Wraps a command in a supported login shell while preserving the command verbatim.
///
/// Login profiles can prepend other tool directories (Homebrew's `shellenv` puts
/// `/opt/homebrew/bin` first) ahead of the per-surface shim directory that cmux
/// seeds into the spawned PATH, which would route `claude`/`codex` around cmux's
/// wrapper hooks. The payload therefore re-prepends the shim directory
/// unconditionally after profiles run; a duplicate PATH entry is harmless and
/// matches what interactive shell integration already produces.
static func wrap(_ command: String, userShell: String?) -> String {
var shellPath: String
if let userShell, userShell.hasPrefix("/") {
shellPath = userShell
} else {
shellPath = "/bin/zsh"
}
let payload: String

switch (shellPath as NSString).lastPathComponent {
case "fish":
payload = """
if test -n "$CMUX_CLAUDE_WRAPPER_SHIM_ROOT"; and test -d "$CMUX_CLAUDE_WRAPPER_SHIM_ROOT"; set -gx PATH "$CMUX_CLAUDE_WRAPPER_SHIM_ROOT" $PATH; end
\(command)
"""
case "zsh", "bash", "sh", "ksh", "dash":
payload = """
if [ -n "${CMUX_CLAUDE_WRAPPER_SHIM_ROOT:-}" ] && [ -d "${CMUX_CLAUDE_WRAPPER_SHIM_ROOT}" ]; then PATH="${CMUX_CLAUDE_WRAPPER_SHIM_ROOT}${PATH:+:$PATH}"; export PATH; fi
\(command)
"""
default:
shellPath = "/bin/zsh"
payload = """
if [ -n "${CMUX_CLAUDE_WRAPPER_SHIM_ROOT:-}" ] && [ -d "${CMUX_CLAUDE_WRAPPER_SHIM_ROOT}" ]; then PATH="${CMUX_CLAUDE_WRAPPER_SHIM_ROOT}${PATH:+:$PATH}"; export PATH; fi
\(command)
"""
}

return "\(shellSingleQuoted(shellPath)) -lc \(shellSingleQuoted(payload))"
}

private static func shellSingleQuoted(_ value: String) -> String {
"'" + value.replacingOccurrences(of: "'", with: "'\"'\"'") + "'"
}

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

Replace the static-only namespace with an injectable wrapper.

This adds a namespace-only enum with global behavior. Make the wrapper constructable, inject the resolved shell for tests, and keep resolution at the workspace-creation ownership boundary.

As per coding guidelines, “Production Swift code must not add … static-only namespaces … Put state and behavior on a constructable, injectable owning type.”

🤖 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/WorkspaceInitialCommandLoginShell.swift` around lines 4 - 67, Replace
the static-only WorkspaceInitialCommandLoginShell enum with a constructable
wrapper type that stores the resolved shell dependency, allowing tests to inject
it directly. Move default login-shell resolution to the workspace-creation
ownership boundary, and make the wrapper’s command-wrapping behavior
instance-based while preserving shell selection and payload generation.

Source: Coding guidelines

@azooz2003-bit
azooz2003-bit merged commit a0ba929 into main Jul 24, 2026
8 checks passed
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