Skip to content

Fix #6447: Claude Code 2.1.183 agent-team teammates open split panes again - #6499

Merged
austinywang merged 21 commits into
mainfrom
issue-6447-claude-teams-panes
Jun 20, 2026
Merged

austinywang merged 21 commits into
mainfrom
issue-6447-claude-teams-panes

Conversation

@austinywang

@austinywang austinywang commented Jun 20, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Fixes #6447. Starting a Claude Code agent team in cmux stopped opening a visible split pane per teammate — teammates silently fell back to in-process with no pane. Clean regression: Claude Code 2.1.181 works, 2.1.183 is broken.

Root cause (pinned empirically)

I diffed the two Bun-compiled claude binaries (~/.local/share/claude/versions/2.1.18{1,3}) and drove a real claude --teammate-mode auto against a logging tmux shim in cmux's exact env. Claude Code changed how it launches a teammate pane while running inside tmux:

  • 2.1.181: split-window … -P -F '#{pane_id}' (default-shell pane) → send-keys -t <pane> -l -- <cmd> + send-keys Enter — i.e. it typed the command into a live shell.
  • 2.1.183: split-window -d … -P -F '#{pane_id}' -- cat (placeholder cat pane) → set-option remain-on-exit failed → respawn-pane -k -t <pane> -- "cd <dir> && env … <claude> …" (respawn-pane/remain-on-exit appear 0× in 2.1.181, 2× in 2.1.183).

cmux forwards the respawn-pane command to the surface as the pane's process command. On macOS, Ghostty execs the surface command via exec -l <command> (ghostty/src/termio/Exec.zig), which only works for a single executable. The teammate command is a shell expression, so it became exec -l cd <dir> && env … claude … — bash execs the cd builtin as a binary, fails, and the pane exits before Claude ever starts. 2.1.181 was unaffected because it typed the command into a running shell.

Confirmed directly (no build needed):

# 2.1.183 path — second command never runs:
$ /bin/bash --noprofile --norc -c "exec -l cd /tmp && echo REACHED"     # prints nothing
# fix — runs through a shell:
$ /bin/bash --noprofile --norc -c "exec -l /bin/sh -c 'cd /tmp && echo REACHED'"   # prints REACHED

Fix

Both respawn-pane CLI paths (__tmux-compat respawn-pane and the public respawn-pane) now run their command through /bin/sh -c '<command>' via a shared helper, so Ghostty execs a shell rather than a bare builtin/expression. The whole command is single-quoted, so it round-trips verbatim regardless of operators/quoting — there is no fragile attempt to classify which commands "need" a shell. tmux_start_command stays the raw command so #{pane_start_command} / OMX-HUD detection are unaffected.

A POSIX shell (/bin/sh) is used deliberately rather than the user's $SHELL: the wrapped bodies are POSIX sh syntax (Claude Code's cd … && env …, and the no-command fallback exec ${SHELL:-/bin/sh} -l), and csh/tcsh cannot parse ${VAR:-default} / NAME=value prefixes. Ghostty still supplies a login-style argv0 on macOS.

Tests

CLITmuxCompatRemoteSplitTests.respawnPaneRunsShellExpressionsThroughLoginShell drives __tmux-compat respawn-pane against a mock socket and asserts the surface.respawn command is /bin/sh-wrapped (spaced operator, no-space operator, assignment prefix, already-shell-invoked) while tmux_start_command stays raw. Added to the already-wired CLITmuxCompatRemoteSplitTests.swift (no pbxproj change). tests/test_cli_omo_tmux_respawn_pane.py updated: both __tmux-compat and public respawn now expect the wrapped command.

Verification

  • Root cause + shell-level fix confirmed empirically (above).
  • End-to-end in the dev build: respawning a pane with a shell-expression command (cd /tmp && echo … > marker && exec sleep 600) through cmux's respawn-pane wrote the marker file and the pane persisted — i.e. the command that the old code could not run now runs in a real respawned pane. The teammate command (cd <dir> && env … claude) follows the identical path.
  • Localization: no user-facing strings added or changed (internal CLI plumbing), so no Localizable.xcstrings / message-catalog updates required.

🤖 Generated with Claude Code

austinywang and others added 2 commits June 20, 2026 02:28
Regression coverage for #6447. Claude Code 2.1.183 launches agent-team
teammates with `respawn-pane -k -- "cd <dir> && env … <claude> …"`. cmux
forwards that command to the surface as the pane's process command, which on
macOS Ghostty execs via `exec -l <command>` — that only works for a single
executable, so a `cd …&&… claude` shell expression makes Ghostty try to exec
the `cd` builtin as a binary, the pane dies immediately, and the teammate
never gets a visible pane.

This test asserts respawn-pane forwards a login-shell-invoked command while
keeping tmux_start_command raw. It fails until the fix lands.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude Code 2.1.183 changed how agent-team teammates open panes: instead of
`split-window` + `send-keys` (which typed the command into a live shell), it now
creates a placeholder pane (`split-window -- cat`) and runs the real teammate
command via `respawn-pane -k -- "cd <dir> && env … <claude> …"`.

cmux forwarded that command to the surface as the pane's process command. On
macOS, Ghostty execs the surface command via `exec -l <command>`
(ghostty/src/termio/Exec.zig), which only works for a single executable. The
teammate command is a shell expression, so `exec -l cd <dir> && …` tried to exec
the `cd` builtin as a binary, failed, and the pane exited before Claude ever
started — so teammates silently fell back to in-process with no visible pane.
Claude Code 2.1.181 was unaffected because it typed the command into a running
shell.

respawn-pane now rewrites shell-expression commands to run through
`/bin/zsh -lc '<command>'` (matching real tmux's `$SHELL -c` semantics) so
Ghostty execs the shell. Commands that are already a single executable —
including the `/bin/sh -c "…"` form OMO uses — are left unchanged so they are not
double-wrapped. `tmux_start_command` stays the raw command so
`#{pane_start_command}` / OMX-HUD detection are unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 20, 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 Jun 20, 2026 11:23pm
cmux-staging Building Building Preview, Comment Jun 20, 2026 11:23pm

@coderabbitai

coderabbitai Bot commented Jun 20, 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

Updates tmuxShellInvokedStartCommand to trim incoming commands and always wrap non-empty commands in ${SHELL:-/bin/zsh} -lc with proper quoting. Routes respawn-pane command construction through this wrapper while preserving raw commands in tmux_start_command. Validates behavior with Swift mock-server and Python OMO regression tests.

Changes

Tmux login-shell command rewriting

Layer / File(s) Summary
Login-shell command wrapping implementation
CLI/CMUXCLI+TmuxCompatSupport.swift
tmuxShellInvokedStartCommand trims the input; for non-empty commands, always wraps in ${SHELL:-/bin/zsh} -lc with the inner command safely single-quoted via tmuxShellQuote to preserve operators and quoting verbatim; empty commands return the original input unchanged.
Respawn payload wires command through wrapper
CLI/cmux.swift
Updates surface.respawn parameter construction to route commandText through tmuxShellInvokedStartCommand; preserves raw commandText as tmux_start_command for pane-start detection and OMX-HUD. Block comment documents the login-shell requirement for Ghostty compatibility (exec -l only supports single executable).
Swift regression test: respawn-pane runs through login shell
cmuxTests/CLITmuxCompatRemoteSplitTests.swift
Adds test helper CapturedRespawn to thread-safely capture surface.respawn payload fields; adds helper respawnPaneForwardedCommand to spin up a mock Unix socket server, run the bundled CLI with a teammate command, and return captured command and tmux_start_command values. Adds respawnPaneRunsShellExpressionsThroughLoginShell (for #6447) validating shell-expression commands are wrapped with ${SHELL:-/bin/zsh} -lc while preserving the original command verbatim inside and in tmux_start_command; covers shell operators, leading environment assignments, and already-wrapped invocations. Adds CLITmuxCompatRespawnTestError.noRespawnCommand to fail if mock server never receives respawn payload.
Python test validation: OMO respawn expectations
tests/test_cli_omo_tmux_respawn_pane.py
Adds login_shell_wrapped helper to format respawn commands as ${SHELL:-/bin/zsh} -lc ... with tmux-compatible quoting. Updates three OMO tmux-compat respawn-pane test assertions to expect the surface.respawn command field to contain the login-shell-wrapped version: for empty respawn (placeholder command), respawn-pane attach command, and public respawn test command.

Sequence Diagram(s)

sequenceDiagram
    participant CLI as respawn-pane CLI
    participant Wrapper as tmuxShellInvokedStartCommand
    participant Server as surface.respawn (Ghostty)

    CLI->>Wrapper: commandText
    Wrapper->>Wrapper: trim and check if empty
    alt empty after trim
        Wrapper-->>CLI: return original
    else non-empty
        Wrapper->>Wrapper: single-quote via tmuxShellQuote
        Wrapper-->>CLI: ${SHELL:-/bin/zsh} -lc 'quoted'
    end
    CLI->>Server: command = wrapped, tmux_start_command = raw commandText
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

Possibly related PRs

  • manaflow-ai/cmux#5465: Modifies the same respawn-pane command-building path in CLI/cmux.swift where command and tmux_start_command are assembled for surface.respawn.

Poem

🐇 Commands flow through login shells now,
${SHELL:-/bin/zsh} -lc takes a bow,
Trim and wrap with proper quoting tight,
Raw commands preserved—everything's right,
Tmux respawns with shell expression might! ✨

🚥 Pre-merge checks | ✅ 22 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (22 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly references the fixed issue (#6447) and concisely describes the main change: restoring visible split panes for Claude Code agent-team teammates.
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 Swift changes add only synchronous pure utility functions to CMUXCLI extension with no implicit MainActor isolation, mutable Sendable types, or UI-bound store access issues.
Cmux Swift Blocking Runtime ✅ Passed No blocking/timing-based synchronization introduced in production Swift code. Changes consist of a pure string-processing function (tmuxShellInvokedStartCommand) and its usage, with no semaphores,...
Cmux Expensive Synchronous Load ✅ Passed PR adds only lightweight string manipulation (trimming, escaping, interpolation) with no expensive synchronous loaders, disk I/O, syscalls, or file operations.
Cmux Cache Substitution Correctness ✅ Passed PR does not involve cache substitution. The respawn-pane handler fetches fresh data via surface.list API call, not from a cache, and transforms it for execution while preserving the original.
Cmux No Hacky Sleeps ✅ Passed PR changes are Swift production code (excluded from check) and test-only code with deterministic timeouts (allowed). No non-Swift production code changes that violate the runtime-no-hacky-sleeps rule.
Cmux Algorithmic Complexity ✅ Passed PR processes single command strings through O(n) string operations, not collections; respawn-pane called once per command, not in loops; test code is scaffolding only.
Cmux Swift Concurrency ✅ Passed Production Swift code changes are pure synchronous string transformations with no legacy async patterns. Test-only DispatchQueue/Semaphore usage for mock socket server synchronization is explicitly...
Cmux Swift @Concurrent ✅ Passed All Swift functions added are synchronous pure helpers with no async/await, @concurrent, or actor-isolation violations. The PR adds tmuxShellInvokedStartCommand (synchronous string processor) and t...
Cmux Swift File And Package Boundaries ✅ Passed PR adds 26 lines to 389-line TmuxCompatSupport file (coherent responsibility) and 9 lines to oversized cmux.swift (far under 250-line threshold). Qualifies as focused bug fix exception.
Cmux Swiftpm Lockfiles ✅ Passed All SwiftPM lockfiles (11 package-local + root Xcode Package.resolved) are committed, and cmux-owned .gitignore files do not ignore Package.resolved, meeting swiftpm-package-resolved.md requirements.
Cmux Swift Logging ✅ Passed The PR adds no production Swift logging violations. New function tmuxShellInvokedStartCommand() contains only documentation comments and logic; respawn-pane handler adds only explanatory comments...
Cmux User-Facing Error Privacy ✅ Passed All user-facing error messages and help text comply with user-facing-errors.md. No sensitive information (vendor names, credentials, environment variables, billing details) is exposed to end users....
Cmux Full Internationalization ✅ Passed PR uses only existing localization key (cli.tmuxCompat.respawnPane.requiresForce) with complete translations for both supported locales (en, ja); all test code is developer-only; no new user-facing...
Cmux Swiftui State Layout ✅ Passed No SwiftUI code in PR. Changes are CLI/tmux compatibility (Swift) and Python tests; zero SwiftUI imports, state annotations, or layout patterns detected.
Cmux Architecture Rethink ✅ Passed PR introduces a pure string-transformation function (tmuxShellInvokedStartCommand) with no timing repairs, blocking paths, locks, observers, or lifecycle issues. Test-only semaphore usage is allowe...
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR makes no changes to standalone cmux-owned windows (NSWindow, NSPanel, NSWindowController, SwiftUI Window, or WindowGroup). Changes are CLI command forwarding logic and test fixtures, not UI wind...
Cmux Source Artifacts ✅ Passed All changed files (CLI/.swift sources and test files in cmuxTests/ and tests/) are hand-written source code and test scripts intentionally part of the product. No artifact directories, generated lo...
Cmux No Test Or Debug Seam In Production Source ✅ Passed PR adds tmuxShellInvokedStartCommand() as a production function with no test/debug naming, #if DEBUG guards, or test-only visibility widening. Function serves real product behavior (fixing #6447),...
Description check ✅ Passed The PR description is comprehensive and well-structured. It clearly explains the problem, root cause with empirical confirmation, the fix, testing approach, and verification steps. All major sections are well-addressed.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-6447-claude-teams-panes

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 20, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

Fixes a clean regression introduced in Claude Code 2.1.183, where agent-team teammates silently fell back to in-process execution instead of opening split panes. The root cause was that the new respawn-pane-based launch path sent bare shell expressions (e.g. cd <dir> && env … claude) as the pane command to Ghostty, which passes them through exec -l <command> — a syscall that requires a single executable, not a shell expression.

  • Shell-wrapping fix (CMUXCLI+TmuxCompatSupport.swift): every respawn-pane command is now run through /bin/sh -c '…' via tmuxShellInvokedStartCommand/tmuxRespawnStartCommand, so Ghostty execs a shell rather than a builtin; tmux_start_command is kept raw for OMX-HUD detection and #{pane_start_command}.
  • Trust-gate bypass (CMUXCLI+ExecutableResolution.swift, AgentLaunchSanitizer.swift): CLAUDE_CODE_SANDBOXED is injected only when the user opts in with --dangerously-skip-permissions, using a prompt-boundary-aware option scanner; ambient markers are cleared on each non-opted invocation to prevent cross-session leakage.
  • Split-pane nudge (claudeTeamsExecArguments): appends a system-prompt nudge steering the lead toward named split-pane teammates (skipped when the user supplies their own system prompt).

Confidence Score: 5/5

Safe to merge — the shell-wrapping fix is unconditional and correct for all respawn-pane callers, and the trust-gate bypass is properly gated behind an explicit opt-in with ambient-marker cleanup preventing cross-session leakage.

Both changed code paths (the /bin/sh -c wrapper and the CLAUDE_CODE_SANDBOXED propagation) are narrow, well-tested, and backed by empirical root-cause analysis. The option-boundary-aware scanner correctly distinguishes flag-shaped prompt tokens from genuine options, the tmux_start_command field is kept raw so OMX-HUD detection is unaffected, and the cross-session marker cleanup is verified by a dedicated regression test.

No files require special attention. All changed production sources have corresponding test coverage, and the budget file is updated to match the new line counts.

Important Files Changed

Filename Overview
CLI/CMUXCLI+TmuxCompatSupport.swift Adds tmuxShellInvokedStartCommand and tmuxRespawnStartCommand to wrap every respawn command in /bin/sh -c, and tmuxClaudeTeamsRespawnEnvironment to propagate the sandboxed trust-bypass env var; core fix for #6447.
CLI/CMUXCLI+ExecutableResolution.swift Adds option-boundary-aware claudeTeamsHasDangerousSkipPermissions, claudeTeamsExtraEnvVars (gates CLAUDE_CODE_SANDBOXED on explicit opt-in), claudeTeamsExecArguments (injects split-pane nudge system prompt), and claudeTeamsHasExplicitSystemPrompt.
CLI/cmux.swift Wires new helpers into both respawn-pane code paths, adds ambient-marker clearance to prevent cross-session bypass leakage, and switches exec argv to claudeTeamsExecArguments.
Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/AgentLaunchSanitizer.swift Adds claudeTeamsLaunchHasOption — a prompt-boundary-aware scanner that detects whether a named option appears as a genuine option rather than inside a prompt payload, value slot, or after --.
cmuxTests/CLITmuxCompatRemoteSplitTests.swift Adds respawnPaneRunsShellExpressionsThroughLoginShell and respawnPaneInjectsClaudeTeamsTrustBypass tests driving __tmux-compat respawn-pane against a mock socket.
tests/test_cli_claude_teams_trust_optin.py New regression test: verifies trust-gate bypass is granted only on explicit --dangerously-skip-permissions opt-in and ambient markers are cleared on non-opted launches.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant User
    participant cmux as cmux claude-teams
    participant TmuxCompat as __tmux-compat
    participant Surface as Surface / Ghostty

    User->>cmux: cmux claude-teams [--dangerously-skip-permissions] prompt
    cmux->>cmux: claudeTeamsExtraEnvVars() sets CLAUDE_CODE_SANDBOXED if opted-in
    cmux->>cmux: clear ambient markers if NOT opted-in
    cmux->>Surface: execv claude with nudge system-prompt appended

    Note over Surface: Claude Code 2.1.183 teammate launch
    Surface->>Surface: split-window placeholder pane
    Surface->>TmuxCompat: __tmux-compat respawn-pane -k -- cd dir and env claude

    TmuxCompat->>TmuxCompat: tmuxClaudeTeamsRespawnEnvironment() reads CMUX_CLAUDE_TEAMS_SANDBOXED
    TmuxCompat->>TmuxCompat: tmuxRespawnStartCommand() wraps as /bin/sh -c with optional env exports
    TmuxCompat->>Surface: "surface.respawn command=/bin/sh -c wrapped tmux_start_command=raw"
    Surface->>Surface: exec -l /bin/sh succeeds - teammate pane runs correctly
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 User
    participant cmux as cmux claude-teams
    participant TmuxCompat as __tmux-compat
    participant Surface as Surface / Ghostty

    User->>cmux: cmux claude-teams [--dangerously-skip-permissions] prompt
    cmux->>cmux: claudeTeamsExtraEnvVars() sets CLAUDE_CODE_SANDBOXED if opted-in
    cmux->>cmux: clear ambient markers if NOT opted-in
    cmux->>Surface: execv claude with nudge system-prompt appended

    Note over Surface: Claude Code 2.1.183 teammate launch
    Surface->>Surface: split-window placeholder pane
    Surface->>TmuxCompat: __tmux-compat respawn-pane -k -- cd dir and env claude

    TmuxCompat->>TmuxCompat: tmuxClaudeTeamsRespawnEnvironment() reads CMUX_CLAUDE_TEAMS_SANDBOXED
    TmuxCompat->>TmuxCompat: tmuxRespawnStartCommand() wraps as /bin/sh -c with optional env exports
    TmuxCompat->>Surface: "surface.respawn command=/bin/sh -c wrapped tmux_start_command=raw"
    Surface->>Surface: exec -l /bin/sh succeeds - teammate pane runs correctly
Loading

Reviews (11): Last reviewed commit: "Document that the claude-teams trust byp..." | Re-trigger Greptile

Comment on lines +109 to +113
func tmuxShellInvokedStartCommand(_ command: String) -> String {
let trimmed = command.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty, tmuxCommandRequiresLoginShell(trimmed) else { return command }
return "/bin/zsh -lc \(tmuxShellQuote(trimmed))"
}

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 Hardcoded /bin/zsh diverges from tmuxStartupScript's ${SHELL:-/bin/sh} and from the doc comment's own claim of "matching real tmux's $SHELL -c semantics." tmuxStartupScript (line 256 in this same file) writes exec "${SHELL:-/bin/sh}" -lc … so the two code paths pick different shells for the same purpose. The fallback commandText of "exec ${SHELL:-/bin/sh} -l" also starts with exec — a member of tmuxShellExecBuiltins — so it gets re-wrapped as /bin/zsh -lc 'exec ${SHELL:-/bin/sh} -l', meaning every pane that respawns without an explicit command goes through an extra zsh process before exec-ing the user's configured shell.

Comment thread CLI/CMUXCLI+TmuxCompatSupport.swift Outdated
Comment on lines +87 to +92
func tmuxCommandRequiresLoginShell(_ command: String) -> Bool {
let words = tmuxShellWords(command)
guard let first = words.first else { return false }
if Self.tmuxShellExecBuiltins.contains(first) { return true }
return words.contains { Self.tmuxShellControlOperators.contains($0) }
}

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 Quoted single-character operators produce false-positive wrapping. tmuxShellWords strips the surrounding quotes but keeps the content as a token, so a command like prog --delim '|' yields the token |, which matches tmuxShellControlOperators and causes the whole command to be needlessly wrapped in /bin/zsh -lc. This is a false positive (the wrapping is functionally safe but applied when it shouldn't be). Checking whether the word came from a quoted span before testing it against the operator set would prevent this.

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!

Two follow-ups from review of the #6447 fix:

- Detect shell operators at the character level (respecting quotes) instead of
  only matching whitespace-separated `tmuxShellWords` tokens, so commands with
  operators that lack surrounding spaces (`echo a;echo b`, `a&&b`) are still
  recognized as shell expressions and wrapped.
- Run the wrapped command through `${SHELL:-/bin/zsh}` (resolved by Ghostty's
  shell wrapper at exec time) instead of hardcoding zsh, matching tmux's
  configured-shell semantics.

Single-executable commands (including OMO's `/bin/sh -c "…"`) still match no
builtin/operator and are forwarded unchanged. Expanded the regression test to
cover the spaced operator, non-spaced operator, plain-executable, and
already-shell-invoked cases.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
CLI/CMUXCLI+TmuxCompatSupport.swift (1)

70-74: ⚠️ Potential issue | 🟡 Minor

Add missing shell builtins to prevent command exec failures.

The tmuxShellExecBuiltins set is missing common shell builtins: exit, return, read, break, continue, and shift. When these appear as the first token in a respawn-pane command, they are not detected as requiring a login shell, causing the tmux compatibility layer to attempt a direct exec that fails (these builtins cannot be executed as standalone binaries). Add these 6 missing builtins to the set.

The test respawnPaneRunsShellExpressionsThroughLoginShell() does not cover these cases and should be expanded to catch this regression.

🤖 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 `@CLI/CMUXCLI`+TmuxCompatSupport.swift around lines 70 - 74, Add the six
missing shell builtins (exit, return, read, break, continue, and shift) to the
tmuxShellExecBuiltins set to prevent command exec failures when these builtins
appear as the first token in a respawn-pane command. Additionally, expand the
respawnPaneRunsShellExpressionsThroughLoginShell() test to include test cases
for each of these newly added builtins to ensure they are properly detected as
requiring a login shell and prevent regression.
🤖 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.

Outside diff comments:
In `@CLI/CMUXCLI`+TmuxCompatSupport.swift:
- Around line 70-74: Add the six missing shell builtins (exit, return, read,
break, continue, and shift) to the tmuxShellExecBuiltins set to prevent command
exec failures when these builtins appear as the first token in a respawn-pane
command. Additionally, expand the
respawnPaneRunsShellExpressionsThroughLoginShell() test to include test cases
for each of these newly added builtins to ensure they are properly detected as
requiring a login shell and prevent regression.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: c982e2dc-1389-459f-b1ff-191b62592dd7

📥 Commits

Reviewing files that changed from the base of the PR and between 6390836 and 46c4068.

📒 Files selected for processing (2)
  • CLI/CMUXCLI+TmuxCompatSupport.swift
  • cmuxTests/CLITmuxCompatRemoteSplitTests.swift

austinywang and others added 8 commits June 20, 2026 03:00
…ll invocations

The "detect what needs a shell" classifier was whack-a-mole — review surfaced
non-spaced operators, then assignment-prefix commands (`FOO=bar claude`), and
more forms (tilde, globs) would follow. Invert it: run every tmux respawn
shell-command through `${SHELL:-/bin/zsh} -lc` (matching tmux's `$SHELL -c`
semantics), and skip wrapping only for a command that is already a clean shell
invocation — a known shell (`sh`/`bash`/`zsh`/…) with a `-c`-style flag, e.g.
OMO's `/bin/sh -c "…"` — to avoid redundant double-wrapping. The skip is
conservative: an unrecognized form is harmlessly wrapped, never wrongly left
unwrapped. Updated the regression test to cover the assignment-prefix case and
the already-shell-invoked passthrough.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…he last token

A shell invocation with a trailing operator (`/bin/sh -c "setup" && claude`) is
still a shell expression — the `&& claude` runs after the inner shell — so it
cannot be forwarded raw onto Ghostty's `exec -l` path. Tighten the skip: only a
genuine complete `<shell> … -c <arg>` where the argument is the final token
qualifies; anything with tokens after the `-c` argument is wrapped like every
other shell expression. Added a regression case.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… skip)

`tmuxShellWords` is a whitespace tokenizer, not a shell parser, so any "is this
already a clean shell invocation" check is fooled by operators with no
surrounding whitespace (`/bin/sh -c "x";y` tokenizes as three words). Rather than
keep refining a classifier that cannot be made reliable, run every tmux respawn
shell-command through `${SHELL:-/bin/zsh} -lc '<command>'`. Single-quoting the
whole original makes it round-trip verbatim, so it runs correctly regardless of
operators or quoting, and there is no longer any classification to get wrong.
Commands that are themselves a shell invocation (e.g. OMO's `/bin/sh -c "…"`) are
run through one more shell that execs straight into them — harmless and matching
tmux's `$SHELL -c` semantics. Updated the OMO respawn test accordingly;
`tmux_start_command` still carries the raw command.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The public `cmux respawn-pane --command …` CLI is a separate handler from
`__tmux-compat respawn-pane` and is not part of the #6447 shell-wrapping change,
so its forwarded command stays raw. Only the `__tmux-compat respawn-pane`
assertions expect the login-shell wrapper.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`/bin/csh` and `/bin/tcsh` are valid macOS login shells that reject `-l`, so a
combined `-lc` would make respawn fail outright for those users. Every shell
accepts `-c`, and on macOS Ghostty already execs the wrapper with a login-style
argv0 (`exec -l`), so the inner shell is still a login shell. Switch the wrapper
to `${SHELL:-/bin/zsh} -c`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The public `cmux respawn-pane --command …` handler reaches the same
surface.respawn / Ghostty `exec -l <command>` path as `__tmux-compat
respawn-pane`, so a shell-expression command (`cd /tmp && env FOO=bar tool`) hit
the same failure. Wrap its command through `tmuxShellInvokedStartCommand` too,
keeping tmux_start_command raw. Updated the public-respawn OMO assertion to
expect the wrapper.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The shell-wrap call sites are self-documenting via tmuxShellInvokedStartCommand
and its doc comment, so drop the redundant inline comments to keep cmux.swift at
net-zero growth (workflow-guard-tests swift-file-length-budget).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The wrapped commands are POSIX `sh` syntax — Claude Code's `cd … && env …` and
the no-command fallback `exec ${SHELL:-/bin/sh} -l` — so they must run under a
POSIX shell. `csh`/`tcsh` login shells cannot parse `${VAR:-default}` parameter
expansion or `NAME=value` command prefixes, so routing these through the user's
`$SHELL` would still exit immediately for those users. Use `/bin/sh -c` (always
present, POSIX, and the body interpreter every command was written for); Ghostty
still supplies a login-style argv0 on macOS. Updated tests to expect the /bin/sh
prefix.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
austinywang and others added 2 commits June 20, 2026 14:36
…panes (#6447)

Regression coverage beyond the respawn shell-wrap, for the two startup gaps
that still left `cmux claude-teams` non-functional:

- The lead and every respawned teammate must start with CLAUDE_CODE_SANDBOXED
  so Claude Code's interactive "Do you trust this folder?" gate (which
  --dangerously-skip-permissions does not cover) cannot deadlock the unattended
  panes. cmuxTests asserts the teammate respawn command exports it; the env
  test asserts the lead receives it.
- `cmux claude-teams` must append a system-prompt nudge so a plain
  "make a demo team with 5 subagents" spawns NAMED split-pane teammates instead
  of nameless in-process subagents (or a "demo what?" question).

These fail without the follow-up fix.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…anes

Two startup gaps remained after the respawn shell-wrap, both of which made the
agent-team teammates look broken:

1. Trust gate deadlock. The lead and every teammate `claude` blocked forever on
   Claude Code's interactive "Do you trust this folder?" dialog (which
   `--dangerously-skip-permissions` does NOT cover), so teammate panes opened
   but never checked in. Set CLAUDE_CODE_SANDBOXED=1 — the first short-circuit
   in Claude Code's trust check — for the lead (extraEnvVars) and inject it into
   each teammate respawn command via tmuxRespawnStartCommand /
   tmuxClaudeTeamsRespawnEnvironment (teammates are respawned by cmux, so they
   do not inherit the launcher env). Scoped to claude-teams; OMO and the public
   respawn-pane are unchanged.

2. Hard-to-start teams. A plain `cmux claude-teams "make a demo team with 5
   subagents"` tended to run nameless in-process subagents (no panes) or stop
   to ask "demo what?". Append a small system-prompt nudge (claudeTeamsExec
   Arguments / claudeTeamsTeamSpawnGuidance) steering the lead to named,
   split-pane teammates for team/parallel requests, so no elaborate prompt is
   needed. Kept out of the exported restore command (which re-invokes
   claude-teams and re-applies the nudge); skipped if the user passes their own
   system prompt.

Verified on a dev build: `cmux claude-teams --dangerously-skip-permissions
"make a demo of this feature with 5 subagents"` opens 5 named teammate panes
(@Researcher/@Architect/@Builder/@Tester/@scribe) that check in and complete,
with no trust prompts and no manual steps.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…-skip-permissions

Autoreview (P1) correctly flagged that unconditionally exporting
CLAUDE_CODE_SANDBOXED removed Claude Code's "Do you trust this folder?" safety
check for every `cmux claude-teams` launch, including untrusted/cloned checkouts.

Only waive the trust gate when the user has already opted into skipping safety
prompts with --dangerously-skip-permissions:
- lead: claudeTeamsExtraEnvVars adds CLAUDE_CODE_SANDBOXED only when the
  claude-teams args carry the flag;
- teammates: tmuxClaudeTeamsRespawnEnvironment(forCommand:) injects it only when
  the respawn command carries the flag (Claude Code adds
  --dangerously-skip-permissions to the teammate command only in bypass mode).

Without the flag the trust prompt is left in place and the user vets the
directory normally. Tests cover both the bypass and gated-off paths.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…not command text

Autoreview (P1) flagged that inferring the --dangerously-skip-permissions opt-in
from the teammate respawn command text is unsafe: that substring can appear in a
cwd, quoted value, or other non-flag position, granting the trust bypass even when
the real Claude argv is not in dangerous-skip mode.

Decide once, from the launcher's own argv, and propagate explicitly:
- the lead records the opt-in in CMUX_CLAUDE_TEAMS_SANDBOXED (claudeTeamsExtraEnvVars),
  which the tmux shim already propagates to the __tmux-compat process;
- tmuxClaudeTeamsRespawnEnvironment() injects CLAUDE_CODE_SANDBOXED only when that
  marker is set, with no command-text matching.

Tests: the teammate inject case is driven purely by the marker, and a new case
proves a bare --dangerously-skip-permissions substring in the command does NOT
grant the bypass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
austinywang and others added 2 commits June 20, 2026 15:27
…on parsing, not a raw arg scan

Autoreview (P1) flagged that scanning every forwarded Claude argument for
--dangerously-skip-permissions can promote prompt text into a safety opt-in: in
claude-teams, tokens after a prompt-boundary option (--tmux), after --, or
consumed as another option's value are prompt/value text, not options.

Add AgentLaunchSanitizer.claudeTeamsLaunchHasOption, which walks the args with the
Teams policy's option/value/prompt-boundary rules and matches how Claude itself
treats positions — crucially it honors an option that follows the positional
prompt (verified on a dev build: `claude "do x" --dangerously-skip-permissions`
enables bypass mode), while ignoring tokens after --tmux/-- or in a value slot.
claudeTeamsHasDangerousSkipPermissions now defers to it.

Adds CMUXAgentLaunch unit coverage for leading/after-prompt/=value detection and
the --tmux / -- / value-slot negatives. Budget bumped for the new helper.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…pt boundary

Autoreview (P1) noted that claudeTeamsLaunchHasOption stopped at any `--tmux`
token, so `cmux claude-teams --tmux classic --dangerously-skip-permissions ...`
lost the trust-gate opt-in and its teammates could still hang on the trust
prompt. The existing claude-teams sanitizer treats `--tmux classic` /
`--tmux=classic` as a launch mode and keeps scanning later flags.

Reuse that exact handling: claudeTeamsLaunchHasOption now defers to
consumePromptBoundaryOption, so a `--tmux` launch mode is skipped and scanning
continues, while only a real `--tmux <prompt>` payload (or `--`, or a value slot)
ends the options. Unit tests cover `--tmux classic` / `--tmux=classic` detection
plus the real-payload negative.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… values as values

Two trust-boundary fixes flagged by autoreview:

1. Ambient marker leak. The launcher only added CMUX_CLAUDE_TEAMS_SANDBOXED /
   CLAUDE_CODE_SANDBOXED when --dangerously-skip-permissions was present, never
   clearing one inherited from a parent opted-in session — so a nested, non-opted
   `cmux claude-teams` could bypass the trust gate. configureClaudeTeamsEnvironment
   now unsets both markers whenever this invocation did not opt in, so the bypass
   reflects only the current command.

2. File-option values misread as flags. claudeTeamsLaunchHasOption relies on the
   policy's option widths to skip value slots, but --append-system-prompt-file /
   --system-prompt-file were not in the claude policy's valueOptions, so
   `--append-system-prompt-file --dangerously-skip-permissions` treated the path
   value as a real opt-in. Add both to valueOptions.

Tests: new CLI test asserts the bypass markers are set only on opt-in and cleared
when an ambient marker is inherited without opt-in; unit tests add the file-option
value-slot negatives.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
austinywang and others added 2 commits June 20, 2026 15:59
…ms-panes

# Conflicts:
#	.github/swift-file-length-budget.tsv
…ch (not restored)

Autoreview noted a restored teammate pane relaunches from the raw
tmux_start_command without CLAUDE_CODE_SANDBOXED and re-shows the trust prompt.
That is intentional, not a regression: persisting the bypass into restore would
re-introduce the trust-boundary leak the earlier fixes closed (a restored pane is
not a fresh --dangerously-skip-permissions opt-in, and after an app restart the
agent team/parent session is gone, so the pane is an orphan). Falling back to the
trust prompt is the safe behavior; record the invariant where the env is supplied.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@austinywang
austinywang merged commit 990d7f7 into main Jun 20, 2026
34 checks passed
austinywang added a commit that referenced this pull request Jul 4, 2026
Add the respawn-pane/respawnp case to the Go remote daemon's tmux-compat
dispatcher, mirroring the local Swift CLI (CLI/cmux.swift, added in
#5465, refined in #6499): require -k, resolve the -t target to its
surface, and forward to the existing surface.respawn RPC. The command
after -- is shell-invoked via /bin/sh -c for the pane process (#6447)
and kept raw in tmux_start_command; without a command, the surface's
stored start command and then a login shell are used; -c becomes
working_directory; the claude-teams sandbox opt-in recorded in
CMUX_CLAUDE_TEAMS_SANDBOXED is re-exported inside the wrapping shell.

Claude Code >= 2.1.183 launches agent-team teammate panes with
`split-window ... cat` followed by `respawn-pane -k ...`, so over
`cmux ssh` the shimmed tmux previously failed with "unsupported tmux
command: respawn-pane" and teammate panes never started.

Forwarding-only: no new daemon backend, and independent of the
Swift-side surface.respawn env-propagation rework in #6309.

Fixes #7014

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

This branch was successfully deployed

1 active deployment
Preview – cmux — 606a0607 Deployed Jun 20, 2026 by vercel[bot]
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.

Claude Code 2.1.183 regression: agent-team teammates no longer open split panes (fall back to in-process)

1 participant