Skip to content

Consolidate SSH security, shim hardening, and restored terminal replay fixes - #15116

Merged
austinywang merged 164 commits into
mainfrom
ssh-private-control-path
Sep 29, 2026
Merged

austinywang merged 164 commits into
mainfrom
ssh-private-control-path

Conversation

@austinywang

@austinywang austinywang commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Summary

This PR consolidates cmux’s SSH, relay, local-state, terminal-restore, and shim hardening into one reviewable change. SSH control masters now live in a private per-user directory and are separated whenever route or host-key policy differs. Remote exec and restored terminal surfaces retain their remote origin before OSC 52 callbacks can run, so remote output cannot overwrite the local clipboard.

It also hardens daemon and relay authorization, forwarding defaults, proxy credentials, local socket peer checks, shell shims, debug logs, wait-for state, remote links, browser proxy access, and Cloud/SSH replay restoration. The latest main merge is included through 79f62d7026a.

Security fixes

  • Route-sensitive SSH options persist ControlMaster=no and ControlPath=none across repeated option merges; host-key and handshake policy changes cannot reuse a weaker %C master.
  • Initial SSH/VM terminals, splits, session restores, respawns, dock restores, and Cloud VM attach replacements carry isRemoteTerminal before runtime callbacks are active.
  • Remote OSC 52 clipboard writes are rejected for both manual mirrors and remote exec PTYs.
  • Private socket, log, shim, marker, proxy-credential, and wait-for paths fail closed on symlinks, hard links, ownership mismatches, or unsafe permissions.

Validation

  • Final local verification: scripts/verify-local.py --all passed 15/15 checks.
  • SSH route and host-key policy tests: 26 passed.
  • Remote daemon timeout isolation: 2 passed.
  • Remote clipboard trust regression: passed.
  • Full Go daemon suite: passed.
  • Hosted Rust verification for the exact PR SHA: run 36541783897 passed macOS/Linux tests, clippy, MSRV, and release-path checks after the runner retry.
  • Gitleaks scanned 106 commits in the final range and found no leaks.
  • Independent final GHSA-class audit found no verified P0, P1, or P2 issues. Receipt: 79f62d7-3fcb2b8-ghsa-final.json.

The combined PR’s two macOS lanes are still pending; I’m waiting for those required checks to finish before calling the PR fully green.

Changelog

  • Fixed: Harden SSH and local state security, preserve remote terminal trust, and repair restored Cloud/SSH terminal rendering.

austinywang and others added 12 commits September 25, 2026 05:45
…ath-shim

# Conflicts:
#	CLI/CMUXCLI+Events.swift
#	Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Wire/EventStreamFailure.swift
#	Packages/macOS/CmuxControlSocket/Tests/CmuxControlSocketTests/EventStreamFailureTests.swift
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cmux's shared OpenSSH control socket lived at /tmp/cmux-ssh-<uid>-%C.
Move it to ~/.cmux/ssh, which cmux creates with mode 0700 and checks is
owned by the user and not writable by anyone else, along with every
directory above it. When no such directory is available, or the home
path is too long for a socket, cmux adds no connection-sharing defaults.

A ControlPath pointing at an older cmux's /tmp socket is rewritten to the
private one, or to none, even with ControlMaster=no. Those /tmp paths are
no longer treated as cmux-owned, so cmux never checks, reuses or removes
them.

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

coderabbitai Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

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

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository: manaflow-ai/cmux/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 099be40f-498b-4849-a413-8af57d0a15da

📥 Commits

Reviewing files that changed from the base of the PR and between 67b0300 and 0ea0857.

📒 Files selected for processing (3)
  • tests/test_issue_13343_claude_integration_toggle.py
  • tests/test_issue_6714_zsh_shim_noclobber.py
  • tests/test_issue_9356_bash_shim_noclobber.py

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 5 remain after this review.


📝 Walkthrough

Walkthrough

This pull request updates SSH and Unix-socket transport, file access, browser proxy authentication, relay policies, terminal links and clipboard reads, shell integration, and cloud replay handling. It also adds related validation and tests.

Changes

Remote access and transport

Layer / File(s) Summary
Private SSH control paths
Packages/macOS/CmuxFoundation/Sources/CmuxFoundation/SSHControlSocket*, Packages/macOS/CmuxFoundation/Tests/CmuxFoundationTests/*SSHControlSocket*Tests.swift, Packages/macOS/CmuxRemoteSession/Tests/CmuxRemoteSessionTests/*
SSH control paths use a validated private directory. Legacy shared-/tmp cmux paths are replaced, and ownership and preflight checks recognize sockets in the configured directory.
Unix-socket forwarding and peer validation
Packages/macOS/CmuxFoundation/Sources/CmuxFoundation/UnixSocket/*, Packages/macOS/CmuxRemoteDaemon/Sources/CmuxRemoteDaemon/Client/*, Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Transport/*, daemon/remote/cmd/cmuxd-remote/*
Daemon forwarding uses a private Unix socket. Socket clients check peer identity before sending data.
SSH arguments, forwarding, and authentication
Packages/macOS/CmuxFoundation/Sources/CmuxFoundation/SSH*, Packages/macOS/CmuxRemoteSession/Sources/CmuxRemoteSession/Session/*, Sources/SSHPTYAttachStartupCommandBuilder.swift, cmux-tui/crates/cmux-remote/src/*
SSH commands add -- before destinations and apply background-forwarding options. Foreground authentication loads tokens from the environment and builds readiness reports.

Proxy and relay behavior

Layer / File(s) Summary
Credentialed browser proxy
Packages/macOS/CmuxCore/Sources/CmuxCore/Remote/BrowserProxy*, Packages/macOS/CmuxRemoteWorkspace/Sources/CmuxRemoteWorkspace/Broker/*, Packages/macOS/CmuxRemoteWorkspace/Sources/CmuxRemoteWorkspace/Tunnel/*, Sources/Panels/Browser*
Each tunnel receives a random credential. SOCKS5 and HTTP CONNECT validate credentials before opening a daemon stream. Browser proxy configurations include the credential.
Relay policy and payloads
Packages/macOS/CmuxRemoteWorkspace/Sources/CmuxRemoteWorkspace/Relay/*, Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/*, Sources/TerminalController+ControlNotificationContext.swift
Relay policy excludes surface.resume.* methods and reply_shape for relayed notifications. Relay status responses retain selected fields, and relayed notifications use relay origin and a remote-host title.

Terminal and shell behavior

Layer / File(s) Summary
Terminal links and clipboard reads
Packages/macOS/CmuxCore/Sources/CmuxCore/Remote/Private*, Packages/macOS/CmuxCore/Sources/CmuxCore/Remote/RemoteLink*, Sources/TerminalLinkOpenCoordinator.swift, Sources/Ghostty*Clipboard*, Sources/TerminalImageTransfer.swift
Link routing distinguishes remote requests, local exports, local files, and machine routes. Terminal-initiated clipboard reads use plain text and a confirmation flow.
Cloud replay fidelity
Packages/macOS/CmuxCloudTui/Sources/CmuxCloudTui/CloudTuiReplayFidelity.swift, Sources/Cloud/CloudTuiManualMirrorSession*, Packages/macOS/CmuxTerminal/Sources/CmuxTerminal/Surface/*RemoteReplay.swift, cmuxTests/CloudRestoreReplayGridTests.swift
Replay completion tracks whether parsing used the remote grid. The mirror can reconnect to fetch a replacement replay when repair conditions are met. Buffered replay callbacks report application or discard.
Private shell state and durable shims
Resources/shell-integration/*, Packages/macOS/CmuxTerminal/Sources/CmuxTerminal/Runtime/TerminalSurfaceRuntimeFilesystem.swift, Sources/TerminalSurfaceRuntimeWiring.swift, Sources/WorkspaceInitialCommandLoginShell.swift
Shell integrations validate shim roots and ancestry. Shim roots use home-directory paths, while PR state and Bash history use private per-user directories.
Owned files and supporting behavior
Packages/macOS/CmuxFoundation/Sources/CmuxFoundation/FileSystem/*, Packages/Shared/CmuxAuthRuntime/Sources/CmuxAuthRuntime/Diagnostics/AuthDebugLog.swift, Packages/macOS/CMUXDebugLog/Sources/CMUXDebugLog/DebugEventLog.swift, CLI/TmuxWaitForSignal.swift, Resources/Localizable.xcstrings
Shared file helpers validate file ownership and type. Debug logs and tmux wait signals use those checks. Other changes include stricter input validation, notification subtitles, replay tests, and localization updates.

Priority: ➖ Normal

Estimated code review effort: 5 (Critical) | ~120 minutes

Merge Risk: 🟡 Moderate · up to 0ea08

Fix wait-for channel isolation and log-file protections, then replace the bounded test polling before merging.

Security Architecture Review

Security architecture risk: 🟡 Moderate · up to 0ea08

The changes add protections across several remote-access paths, but one local-file opening decision depends on an unverified guarantee about which terminal actions remote output can produce. No local-file bypass has been established.

Retained concerns

  • Medium · security · inferred: A non-UNKNOWN terminal action kind is promoted to local-export authority, bypassing remote-terminal file-link deferral. This is a conditional boundary risk: a remote-output route to that kind has not been established.
Security review details

Security Blast Radius

  • inferred — If remote-controlled terminal output can produce a non-UNKNOWN open_url kind, the affected sink is local file opening in the user's macOS session. The evidence does not establish that producer capability, an automatic remote exploit, or cross-user access.

Security Findings and Attack Paths

  • inferred — The conditional path is a non-UNKNOWN Ghostty URL action from a remote terminal, followed by isLocalExport, skipped remote-file deferral, and admission of a file URL as local content. Its first step is unverified, so this is not a demonstrated bypass.

Trust Boundaries and Controls

  • observed — UNKNOWN-kind terminal links retain the pointer-release authorization check. Separately, Cloud guest requests are marked remote-initiated and stopped before routing; neither control proves the provenance of non-UNKNOWN Ghostty actions.

Hardening Proposals

  • proposed — Verify the pinned Ghostty producer contract or exercise it with a focused integration test before relying on non-UNKNOWN action kinds as proof of local file provenance.

Important

Pre-merge checks failed

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

❌ Failed checks (8 errors, 1 warning)

Check name Status Explanation Resolution
Cmux Swift Actor Isolation ❌ Error The production diff adds private let replayFidelityLogger = Logger(...) at Sources/Cloud/CloudTuiManualMirrorSession+ReplayFidelity.swift:6 without nonisolated. This is a new file-scoped `Logger… Declare the new logger as private nonisolated let replayFidelityLogger = Logger(subsystem: "com.cmuxterm.app", category: "CloudManualMirror"). Keep the logger independent of MainActor; retain the session methods and their explicit `@Mai…
Cmux Swift Blocking Runtime ❌ Error The diff adds production timing-based synchronization. RemoteDaemonRPCClient+Transport.swift retries socket connection failures with Thread.sleep(forTimeInterval: 0.05) inside a loop, for up to fi… Replace the daemon socket retry sleep with event-driven directory/socket readiness plus a cancellation-aware deadline. Replace the periodic kevent recheck with a single real filesystem notification or async signal path that handles the ti…
Cmux Swift Package Boundaries ❌ Error The new CLI/TmuxWaitForSignal.swift keeps independently testable filesystem and signal-state logic in the cmux-cli target. The diff adds a TmuxWaitForSignal type that sanitizes names, creates an… Extract TmuxWaitForSignal into the dependency-free CmuxFoundation package. Expose TmuxWaitForSignal as the first public type, and add a package-owned error type or return mechanism for filesystem failures. Add focused `CmuxFoundationT…
Cmux Swift Logging ❌ Error The new production log declaration in Sources/Cloud/CloudTuiManualMirrorSession+ReplayFidelity.swift:6 is private let replayFidelityLogger = Logger(...). The repository rule requires file-scoped `… Declare the logger as nonisolated private let replayFidelityLogger = Logger(subsystem: "com.cmuxterm.app", category: "CloudManualMirror"). Keep the existing private redaction for terminalID and review any future dynamic fields before lo…
Cmux User-Facing Error Privacy ❌ Error The PR violates the user-facing error privacy rule by exposing a user ID (UID) in an error message visible to end users. Violation identified: In daemon/remote/cmd/cmuxd-remote/cli.go, the `dial… Remove the UID from the error message in daemon/remote/cmd/cmuxd-remote/cli.go line 1148. Change: go return nil, fmt.Errorf("socket is served by another user (uid %d)", uid) To: ```go return nil, fmt.Errorf("socket is served by ano…
Cmux Full Internationalization ❌ Error The PR adds new user-facing text without complete internationalization. The five new Resources/Localizable.xcstrings keys contain only ar, de, en, es, fr, ja, ko, zh-Hans, and `zh-Ha… Add translated catalog entries for all 20 supported locales for notification.remoteRelay.hostFallback, notification.remoteRelay.title, and the three terminal.clipboardReadConfirmation.* keys. Route every newly added user-facing CLI, e…
Cmux Architecture Rethink ❌ Error The replay repair introduces a production timing workaround. Sources/Cloud/CloudTuiManualMirrorSession+ReplayFidelity.swift creates fidelityCheckTask and calls await Task.yield() before `repairU… Make replay application and resize completion one explicit state transition owned by the terminal/session coordinator. Have TerminalSurface report replay completion with the native parser generation and the settled grid after the output l…
Cmux No Test Or Debug Seam In Production Source ❌ Error The PR adds test-only seams in production Swift source. Packages/macOS/CMUXDebugLog/Sources/CMUXDebugLog/DebugEventLog.swift is guarded by #if DEBUG and adds init(logPath:) with the comment “tes… Remove the test-only DebugEventLog.init(logPath:) and waitForPendingAppends() members from production source. Move path setup and queue synchronization into the test target, using @testable import and internal state only where direct …
Docstring Coverage ⚠️ Warning Docstring coverage is 27.36% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 329 functions across 106 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (16 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 Cloud Persistent Session And Early Input ✅ Passed No explicit rule violation is introduced. The Cloud change adds a bounded replay-fidelity repair only for stale, wrong-grid replays; it requires matching local/remote grids, an attached visible surfac…
Cmux Browser Automation Off-Main ✅ Passed The PR does not change the browser socket-automation source of truth. Sources/TerminalController.swift, ControlCommandExecutionPolicy.swift, and ControlCommandExecutionPolicyTests.swift have ide…
Cmux Expensive Synchronous Load ✅ Passed The PR does not add or move RestorableAgentSessionIndex.load(), SharedLiveAgentIndex.shared usage, agent-store/transcript/trajectory/workstream loading, directory scans, or unbounded agent-history…
Cmux Cache Substitution Correctness ✅ Passed PASS. The reviewed production changes do not replace a fresh authoritative persistence, history, undo, or snapshot read with an unhandled cache. The snapshot restore diff only adds SSH-destination val…
Cmux No Hacky Sleeps ✅ Passed PASS. The changed production shell integrations add private-path validation and state handling, but no new sleep, timer, polling, or fixed-delay primitive. The new /bin/sleep 0 appears only in test …
Cmux Algorithmic Complexity ✅ Passed PASS — The production diff does not introduce a prohibited complexity pattern. PrivateAddressRouteSelector.machine scans machine addresses linearly, and BrowserPanel+CloudConnection performs one c…
Cmux Swift Concurrency ✅ Passed The diff does not introduce a prohibited legacy async pattern. The only new runtime Task is the replay-fidelity check, stored in fidelityCheckTask, cancelled before replacement, and cancelled duri…
Cmux Swift @Concurrent ✅ Passed PASS. The diff adds no invalid @concurrent annotation and removes none. The file-heavy shim installation keeps an explicit @concurrent async boundary and uses Task.detached. The new replay check is ex…
Cmux Swiftpm Lockfiles ✅ Passed PASS. The changed Package.swift files add only local ../CmuxFoundation dependencies and products; they do not add or change external SwiftPM pins, so no package-local Package.resolved diff is required…
Cmux Swiftui State Layout ✅ Passed PASS. The changed SwiftUI-facing files only contain incidental logging, SSH, and proxy edits. The diff adds no ObservableObject, @Published, @Observable, GeometryReader, lazy/list row store, or render…
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed The PR does not add or materially change a standalone cmux-owned window. The only changed standalone-window file, Sources/Panels/BrowserPopupWindowController.swift, adds proxy-authentication state a…
Cmux Source Artifacts ✅ Passed PASS. The authoritative diff contains 270 changed paths, all under normal source, test, fixture, documentation, configuration, localization, or build-metadata locations. No changed path matches the pr…
Title check ✅ Passed The title clearly summarizes the main themes of the changes: SSH security, shim hardening, and restored terminal replay fixes.
Description check ✅ Passed The description includes a clear summary, detailed validation results, a changelog entry, and known verification limits. It uses a “Validation” heading instead of “Testing” and omits the Demo Video an…
Full details: Cmux Swift Actor Isolation

Explanation

The production diff adds private let replayFidelityLogger = Logger(...) at Sources/Cloud/CloudTuiManualMirrorSession+ReplayFidelity.swift:6 without nonisolated. This is a new file-scoped Logger constant, which Swift 6 can implicitly isolate to MainActor and which the actor-isolation rules require to opt out when it is a pure logger. The logger is used by the CloudTuiManualMirrorSession replay extension at line 73. The repository already uses the required form for comparable file-scoped loggers, such as private nonisolated let agentChatThemeSyncLogger and private nonisolated let sudoApprovalLogger.

Resolution

Declare the new logger as private nonisolated let replayFidelityLogger = Logger(subsystem: "com.cmuxterm.app", category: "CloudManualMirror"). Keep the logger independent of MainActor; retain the session methods and their explicit @MainActor isolation.

Full details: Cmux Swift Blocking Runtime

Explanation

The diff adds production timing-based synchronization. RemoteDaemonRPCClient+Transport.swift retries socket connection failures with Thread.sleep(forTimeInterval: 0.05) inside a loop, for up to five seconds. CLI/TmuxWaitForSignal.swift adds a periodic one-second recheck loop around kevent. The existing Unix-socket poll was moved from CLISocketPathResolver without materially expanding its behavior, and test-only waits are not counted.

Resolution

Replace the daemon socket retry sleep with event-driven directory/socket readiness plus a cancellation-aware deadline. Replace the periodic kevent recheck with a single real filesystem notification or async signal path that handles the timeout without polling.

Full details: Cmux Swift Package Boundaries

Explanation

The new CLI/TmuxWaitForSignal.swift keeps independently testable filesystem and signal-state logic in the cmux-cli target. The diff adds a TmuxWaitForSignal type that sanitizes names, creates and validates private directories and marker files, watches with kqueue, and consumes signals. It imports only Darwin and Foundation, and the Xcode project places it in the cmux-cli Sources phase. This logic does not require AppKit, Ghostty, or app lifecycle state. The CLI command only needs to adapt user-facing CLIError messages. Other new core policies, replay state, socket utilities, and authentication helpers are already behind SwiftPM package targets.

Resolution

Extract TmuxWaitForSignal into the dependency-free CmuxFoundation package. Expose TmuxWaitForSignal as the first public type, and add a package-owned error type or return mechanism for filesystem failures. Add focused CmuxFoundationTests for name sanitization, ownership and mode checks, symlink rejection, signal consumption, wake-up, and timeout. Keep CLI argument parsing, output, timeout wording, and conversion to CLIError in CLI/cmux.swift, then import the package type from the cmux-cli target.

Full details: Cmux Swift Logging

Explanation

The new production log declaration in Sources/Cloud/CloudTuiManualMirrorSession+ReplayFidelity.swift:6 is private let replayFidelityLogger = Logger(...). The repository rule requires file-scoped Logger values in MainActor-isolated code to use nonisolated private let. The added notice is otherwise unified logging and hashes terminalID; the failure is the changed declaration shape.

Resolution

Declare the logger as nonisolated private let replayFidelityLogger = Logger(subsystem: "com.cmuxterm.app", category: "CloudManualMirror"). Keep the existing private redaction for terminalID and review any future dynamic fields before logging.

Full details: Cmux User-Facing Error Privacy

Explanation

The PR violates the user-facing error privacy rule by exposing a user ID (UID) in an error message visible to end users. Violation identified: In daemon/remote/cmd/cmuxd-remote/cli.go, the dialSocketUntil function returns an error: go return nil, fmt.Errorf("socket is served by another user (uid %d)", uid) This error is user-facing because: 1. The error is returned from dialSocketUntil() to socketRoundTripV2() 2. socketRoundTripV2() is called from multiple CLI command handlers (lines 333, 459, 512, etc.) 3. When errors occur, they are printed to stderr via fmt.Fprintf(os.Stderr, "cmux: %v\n", err) 4. This error output reaches the end user as: cmux: socket is served by another user (uid 1001) The UID exposes internal system state information that violates .github/review-bot-rules/user-facing-errors.md: "user-facing errors, alerts, command output, API error bodies, or recovery copy must not expose... [credentials, tokens, headers, private keys, refresh tokens, session ids, or unredacted payload dumps]." UIDs are sensitive identifiers in Unix systems and should not be exposed to users in error messages, even though they are technically public. The error should use generic language like "socket is served by another user" without disclosing the UID number. Secondary observation: The error message in daemon/remote/cmd/cmuxd-remote/claude_hook.go: go return "", fmt.Errorf("claude settings directory %q is not owned by this user", dir) This error includes the directory path, which may be acceptable as it helps diagnose issues, but the "this user" language should be verified as not exposing sensitive information. This appears acceptable since it is describing a validation failure in a way that helps the operator understand what went wrong.

Resolution

Remove the UID from the error message in daemon/remote/cmd/cmuxd-remote/cli.go line 1148. Change: go return nil, fmt.Errorf("socket is served by another user (uid %d)", uid) To: go return nil, fmt.Errorf("socket is served by another user") This maintains the diagnostic value (the user knows another user owns the socket) while removing the exposed system identifier. If debugging is needed, the UID can still be logged at a different level (debug/trace) rather than in user-facing error output.

Full details: Cmux Full Internationalization

Explanation

The PR adds new user-facing text without complete internationalization. The five new Resources/Localizable.xcstrings keys contain only ar, de, en, es, fr, ja, ko, zh-Hans, and zh-Hant; the catalog also supports bs, da, it, km, nb, pl, pt-BR, ru, th, tr, and uk. The new CLI/TmuxWaitForSignal.swift errors, socket ownership errors in CLI/cmux.swift, new NSLocalizedDescriptionKey errors in the remote daemon and remote workspace sources, the foreground-authentication stderr message, and the new workspace API error are not routed through localized APIs.

Resolution

Add translated catalog entries for all 20 supported locales for notification.remoteRelay.hostFallback, notification.remoteRelay.title, and the three terminal.clipboardReadConfirmation.* keys. Route every newly added user-facing CLI, error, recovery, stderr, and API message through String(localized:defaultValue:) or an equivalent app-resolved localization mechanism, including the messages in CLI/TmuxWaitForSignal.swift, the new socket ownership messages, the new remote daemon and relay error descriptions, the foreground-authentication diagnostic, and destination must not start with '-'. Add matching translations for each new localization key in every supported locale.

Full details: Cmux Architecture Rethink

Explanation

The replay repair introduces a production timing workaround. Sources/Cloud/CloudTuiManualMirrorSession+ReplayFidelity.swift creates fidelityCheckTask and calls await Task.yield() before repairUnfaithfulReplayIfNeeded(). The comment states that this defers the decision so sizing callbacks and responses can settle. This is a main-actor scheduling dependency used to repair a rendering and resize lifecycle race. The new replayFidelity state and fidelityCheckTask also leave replay state dependent on event ordering between TerminalSurface.settledGridCells(), resize callbacks, and the native output lane. A bounded repair count reduces repeated reconnects, but it does not make the bad intermediate state unrepresentable.

Resolution

Make replay application and resize completion one explicit state transition owned by the terminal/session coordinator. Have TerminalSurface report replay completion with the native parser generation and the settled grid after the output lane and resize operation complete. Have the session consume that event and decide repair synchronously from that authoritative snapshot. Remove fidelityCheckTask, Task.yield(), and the separate scheduling calls. Keep only a single owner for replay fidelity and its generation state.

Full details: Cmux No Test Or Debug Seam In Production Source

Explanation

The PR adds test-only seams in production Swift source. Packages/macOS/CMUXDebugLog/Sources/CMUXDebugLog/DebugEventLog.swift is guarded by #if DEBUG and adds init(logPath:) with the comment “tests pass a temporary path” plus waitForPendingAppends(). The test target calls both members, while production code has no caller for either. This encodes test setup and synchronization in the production type. The change also widens appendAuthDebugLineToFile from private to internal in Packages/Shared/CmuxAuthRuntime/Sources/CmuxAuthRuntime/Diagnostics/AuthDebugLog.swift; its tests call that debug helper directly.

Resolution

Remove the test-only DebugEventLog.init(logPath:) and waitForPendingAppends() members from production source. Move path setup and queue synchronization into the test target, using @testable import and internal state only where direct observation is required, or test the existing public behavior through the configured debug-log path. Restore appendAuthDebugLineToFile to private and test the public logging path or the shared file-safety implementation instead. Keep genuinely user-invoked debug logging in its dedicated debug file; do not add test-only accessors or injection seams there. See #6452.

✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch ssh-private-control-path
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

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.

@github-actions

Copy link
Copy Markdown
Contributor

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

austinywang and others added 3 commits September 27, 2026 20:32
The baked-VM daemon forward carries daemon RPC without a credential of
its own. The new tests drive start() through a fake ssh that honors -L
and expect the local end to be a Unix socket in a directory only the
current user can open, removed on stop and when ssh exits. The
socket-forward path now honors the transport executable test seam.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The loopback SOCKS5 and HTTP CONNECT listener that serves the ssh
workspace browser accepts any local client. Add a per-tunnel
BrowserProxyCredential and an optional session seam, then test that a
handshake without it is refused before a daemon stream opens and that the
credentialed handshake still relays bytes. The Network.framework cases
drive URLSession through ProxyConfiguration, the stack WKWebView uses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A clipboard read the terminal program starts should ask in a window
sheet whatever the unsafe-paste setting, and reject when there is no
window. It should get the pasteboard's plain text only, while a native
paste keeps files and images.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

CI failure attribution

CI failed on 3fcb2b854e (run 36541734609 attempt 1): 2 code.

Job Verdict Why
macos / swift-package-tests code a test failed
macos / macOS compile admission code a compile error
Matched log lines
macos / swift-package-tests: ✘ Test "An unresolved cmux template reaches the shared master without a config probe" recorded an issue at RemoteSessionReverseRelayTransportTests.swift:151:25: Issue recorded
macos / macOS compile admission: /tmp/cmux-ci-2/src/Sources/Workspace.swift:4199:17: error: argument 'initialEnvironmentOverrides' must precede argument 'isRemoteTerminal'

Not re-run automatically: macos / swift-package-tests, macos / macOS compile admission are not machine failures.

Written by scripts/ci/classify_failures.py (ci-failure-attribution.yml); signatures are its SIGNATURES table. A machine verdict is the runner's fault, not this PR's.

austinywang and others added 12 commits September 27, 2026 20:50
The baked-VM ssh transport forwarded the daemon socket to a loopback TCP
port. Daemon RPC on that path carries no credential of its own, so the
local end now lives in a fresh mkdtemp directory (0700) under the
per-user temporary directory, falling back to /private/tmp. ssh binds it
with StreamLocalBindMask=0177 and StreamLocalBindUnlink=yes ahead of any
configured options. The client accepts the socket only when its peer runs
as the current user, and removes the directory on stop or when ssh exits.

The remote side of the forward is unchanged (same direct-streamlocal
channel to /run/cmuxd-remote.sock), so deployed VMs need no update.

ssh creates the socket only after it authenticates, so the connect now
retries a missing or not-yet-listening socket while ssh runs, for up to
five seconds after the startup grace period.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The loopback SOCKS5 and HTTP CONNECT listener for the ssh workspace
browser now requires the credential its tunnel minted. SOCKS5 only
accepts username/password authentication (RFC 1929), CONNECT needs a
matching Proxy-Authorization: Basic header, and both compare in constant
time before any daemon stream opens. A refused client gets 05 FF, 01 01
or 407 and the connection closes.

The broker mints a fresh credential per tunnel start and hands it to the
tunnel and the BrowserProxyEndpoint. The browser applies it to its
WKWebsiteDataStore SOCKS5 and CONNECT proxy configurations, and the
favicon session moves from the legacy proxy dictionary, which cannot
carry a credential, to a credentialed ProxyConfiguration. Descriptions
of the credential and endpoint are redacted, and workspace.remote.status
keeps reporting the endpoint without it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A clipboard read the terminal program starts (for example OSC 52 under
Ghostty's default clipboard-read = ask) now always asks in a window sheet,
and is rejected when there is no window, instead of being approved unasked.
The sheet uses clipboard access wording rather than the paste wording.

Such a read also takes only the pasteboard's plain-text flavor. It never
prepares, saves or uploads Finder files or images, for any pane kind, and
never becomes input to a remote tmux mirror pane. Native paste gestures
keep the full pasteboard, including image and file upload.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… and remote status details

Remote relay callers can still reach surface.resume.set/get/clear, request a
notification reply field, and read the full remote status payload. These
tests fail until the relay schema and coordinator narrow those paths.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…arding

Covers the stale-socket preflight, the CmuxCore batch builders and the
coordinator's exec, bootstrap, scp and reverse-relay argv: each must put
the destination after `--`, and batch runs must turn off agent, X11 and
port forwarding ahead of the configured options. Also covers shell
quoting of values that end in, or contain, a line terminator.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…her user

Adds the shared checks the fix will use (a socket peer-uid check, an
append opener that refuses symlinks, hard links and foreign owners, and
an owned marker-file reader) with their own tests, injectable seams at
each client connect and at the debug log path, and failing regression
tests for the behavior the fix must provide:

- the control socket probe sends nothing to a server run by another user;
- the remote relay forwards nothing to such a local socket;
- the cloud CLI bridge writes no password or request to it;
- the debug event log does not write through a symlink or hard link and
  creates its file with mode 0600;
- the zsh PR watcher writes no file at a shared /tmp name and does not
  use a state directory that was replaced with a symlink.

Red, before the fix:

  swift test --disable-index-store --package-path Packages/macOS/CmuxControlSocket \
    --filter SocketTransportProbeCommandTests
    -> 7 tests, 1 failed (probeCommandSendsNothingToAServerRunningAsAnotherUser:
       response "PONG", commandReceived true)
  swift test --disable-index-store --package-path Packages/macOS/CmuxRemoteWorkspace \
    --filter "RemoteCLIRelayServerTests|RemoteDaemonProxyTunnelCloudCLITests"
    -> 19 tests, 2 failed (relay wrote 177 bytes; bridge threw nothing
       and wrote 159 bytes)
  swift test --disable-index-store --package-path Packages/macOS/CMUXDebugLog \
    --filter DebugEventLogFileSafetyTests
    -> 3 tests, 3 failed (symlink and hard-link targets appended to;
       new file mode 0644)
  python3 tests/test_shell_pr_watch_private_state.py
    -> 4 tests, 3 failed (force signal, cache and debug log wrote through
       /tmp symlinks)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds failing regression tests for:
- PrivateNetworkHostPolicy: loopback, private, link-local and legacy IPv4 spellings
- RemoteLinkOpenPolicy: remote-initiated opens never hand private or loopback URLs to the default browser
- PrivateAddressRouteSelector: machines sharing 127.0.0.1 route only to the browser's owner
- RemoteMachineNotificationSubtitle: an explicit subtitle still names the machine

The sources are stubs that keep today's behavior.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…runs

Shell-word quoting now uses one byte-comparing helper
(POSIXShellWord) instead of per-file `^…$` regexes, which NSString's
ICU matcher lets match before a trailing line terminator. Validation
regexes that gate ssh, scp and shell arguments anchor with `\z`.

Background ssh and scp argv built by cmux put `--` ahead of the
destination. The interactive ssh command cannot, so its input paths
(CLI VM usernames, workspace context, restored snapshots) reject a
destination that starts with `-`.

Batch runs that never become a ControlMaster turn off agent and X11
forwarding, and drop configured port forwards where the run has no
forward of its own. The daemon carrier keeps forwarding because
persistent remote shells inherit its SSH_AUTH_SOCK and DISPLAY, and
runs that can create the shared master keep the configured settings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The CLI, the control-socket probe, the remote relay and the cloud CLI
bridge now read the listening process's user from the connected socket
and refuse to write anything to a socket served by another user. The CLI
also lstats the socket path so a link someone else planted is never
followed. The debug event log opens its /tmp file with O_NOFOLLOW and
only appends to a regular, single-link file this user owns.

The zsh PR watcher regression test from the previous commit is dropped
from this change and left as a follow-up. The probe test's server now
answers only when it received a command, so a refusing probe no longer
raises SIGPIPE in the test process.

Green:
- swift test --disable-index-store --package-path Packages/macOS/CmuxControlSocket --filter SocketTransportProbeCommandTests: 7 tests passed
- swift test --disable-index-store --package-path Packages/macOS/CmuxRemoteWorkspace --filter "RemoteCLIRelayServerTests|RemoteDaemonProxyTunnelCloudCLITests": 19 tests passed
- swift test --disable-index-store --package-path Packages/macOS/CMUXDebugLog --filter DebugEventLogFileSafetyTests: 3 tests, 0 failures

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… relay

The relay no longer forwards surface.resume.get/set/clear, so a remote
session cannot read, write or clear a Mac-side resume command, and
command-bearing params are denied on every method with no exceptions.

A relayed notification is delivered with the relay origin, no reply
affordance and the remote destination in its title; reply_shape is out of
the relay schema.

workspace.remote.status and the terminal_session_* lifecycle replies carry
only enabled, state and connected in remote for a relay caller.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A url-open that a remote machine starts without a click no longer hands a
  loopback, private, link-local or local-name URL to the default browser.
  SSH loopback routes stay in the cmux browser and are never opened in the
  system browser for a remote request; refusals return false so the remote
  side prints the URL instead.
- Browser private-address routing picks the machine that owns the browser
  when several SSH machines share 127.0.0.1, instead of the first match.
- A cloud notification's explicit subtitle still names its machine, with line
  breaks, control characters and bidi overrides removed and lengths capped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…socket

Move the CLI's socket discovery probe into CmuxFoundation as
UnixSocketConnectProbe and route workspace.remote.configure's
local_socket_path through ControlWorkspaceRemoteLocalSocketPath, both
unchanged in behavior. The new tests expect the probe to refuse a listener
running as another user and the forwarding path to be the app's own control
socket; they fail here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

Merge receipt for b9e418a0b7, merged 2026-09-29 09:31:05 UTC

  • Not verified at merge: ci-status (not reported), macOS compile admission (in progress), Claude wrapper regressions (in progress), guards (17) (in progress), package-conventions-lint (in progress), seven-language live conformance (in progress), swift-package-tests (in progress)
  • Verified: Web complexity, web-validation, auth-refresh-tests, catalog-structure, CI fast guards, detect-ios-changes, Fast static checks, GhosttyKit release check, inventory, ios-e2e-status, protocol contract, remote-daemon-admission, and 5 more
  • Skipped by policy: ${{ matrix.language }} consumer, ${{ matrix.language }} package, admission-placement, backend, browser, Dogfood build #​${{ github.event.pull_request.number }}, ios-e2e, mac-host, Rust SDK MSRV (1.88), suite-coverage, web, web-build, and 2 more
  • Full suite: runs on main after merge.

Labeled merged-unverified: if main breaks near this merge, look here first.

@github-actions github-actions Bot added the merged-unverified A judging check was not green at merge; see the merge receipt comment label Sep 29, 2026
rustybret pushed a commit to rustybret/bmux that referenced this pull request Sep 29, 2026
c4900c2 docs: CI routing is minis first; never send a lane straight to Blacksmith (manaflow-ai#15563)
7538f00 ci: keep the kept build when it and the local seed both recompile the app (manaflow-ai#15552)
217ef13 ci: fingerprint package interfaces and shadow the app-compile skip (manaflow-ai#15551)
ad7906a Consolidate SSH security, shim hardening, and restored terminal replay fixes (manaflow-ai#15116)
ff12362 Fix pane drop target rendering in the wrong pane (manaflow-ai#15550)
ed49329 ci: report healthy Aqua console sessions accurately (manaflow-ai#15504)

# Conflicts:
#	.github/workflows/ci-guards.yml
#	.github/workflows/ci-macos.yml
teamleaderleo added a commit that referenced this pull request Sep 29, 2026
With cmuxTests compiling again, the changed-suites job runs tests that
could not run on main since #15116 and #15550: four remote tmux mirror
topology tests see one pane too many and two browser drop-preview tests
find no overlay. Disable them against #15564 so ci-status can go green.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

main no longer compiles after this merge

@austinywang: after ad7906a261 landed on main, the app-host test product (the app and cmuxTests, build-for-testing) stops compiling. The compile of the commit before it (ff12362be9) passed and its own merge commit ad7906a261 fails, so this pull request is the cause (possibly with a semantic conflict against something merged earlier that its own CI did not see).

Evidence: https://github.com/manaflow-ai/cmux/actions/runs/36566630813/job/109400047856

cmuxTests/RemoteTmuxMirrorCLIObservabilityTests.swift:399: error: immutable value 'self.nonMirrorPanelID' may only be initialized once
cmuxTests/RemoteTmuxMirrorCLIObservabilityTests.swift:415: error: immutable value 'self.peerSurfaceID' may only be initialized once
cmuxTests/RemoteTmuxMirrorCLIObservabilityTests.swift:423: error: immutable value 'self.connection' may only be initialized once
cmuxTests/RemoteTmuxMirrorCLIObservabilityTests.swift:435: error: immutable value 'self.controlPipe' may only be initialized once
cmuxTests/RemoteTmuxMirrorCLIObservabilityTests.swift:436: error: immutable value 'self.controlWriter' may only be initialized once
cmuxTests/RemoteTmuxMirrorCLIObservabilityTests.swift:457: error: immutable value 'self.controlPaneIDs' may only be initialized once
cmuxTests/RemoteTmuxMirrorCLIObservabilityTests.swift:465: error: immutable value 'self.mirror' may only be initialized once
cmuxTests/WorkspaceCreateReviewRegressionTests.swift:277: error: method does not override any method from its superclass
cmuxTests/WorkspaceUnitTests.swift:3508: error: method does not override any method from its superclass

Nothing blocks merging meanwhile. A fix-forward (or, failing that, a revert) is attempted automatically unless an open pull request already fixes this.

main_compile_attribution.py: post-merge, nothing here gates a merge.

lawrencecchen pushed a commit that referenced this pull request Sep 29, 2026
* fix(tests): compile cmuxTests again after #15116 and #15550

main's app-host test target no longer compiles (every PR's compile
admission fails, e.g. job 109346405399):

- #15116 added initialTerminalIsRemote to TabManager's
  makeWorkspaceForCreation and addWorkspaceIfActive; three test
  overrides still had the old signature (does not override).
- #15116's RemoteTmux harness captured self in a closure before every
  stored property was set, which fails definite initialization.
- #15550's test passed a zero-argument closure as frameForZone, which
  takes the zone.

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

* test: disable six app-host tests that fail on main (#15564)

With cmuxTests compiling again, the changed-suites job runs tests that
could not run on main since #15116 and #15550: four remote tmux mirror
topology tests see one pane too many and two browser drop-preview tests
find no overlay. Disable them against #15564 so ci-status can go green.

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

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
rustybret pushed a commit to rustybret/bmux that referenced this pull request Sep 29, 2026
6093e59 test(cloud-vm): cover missing attach address
b639b65 fix(iroh-v2): omit bearer device attribution
c9a74d6 ci: E2E picker routes owned labels by the online runners, not CI_OWNED_POOL_SLOTS (manaflow-ai#15582)
194ae87 fix(tests): compile cmuxTests again after manaflow-ai#15116 and manaflow-ai#15550 (manaflow-ai#15561)
8bfc872 ci: live runners decide root, gui and side routing; CI_OWNED_POOL_SLOTS is the fallback only (manaflow-ai#15572)
1516426 ci: route hardcoded Blacksmith labels through the runner variables (manaflow-ai#15592)

# Conflicts:
#	.github/workflows/cmux-cloud-cli.yml
#	.github/workflows/cmux-tui.yml
#	.github/workflows/ios-e2e.yml
austinywang added a commit that referenced this pull request Sep 30, 2026
The relay's local socket and the policy fixture's listener now set
SO_NOSIGPIPE, so a write to a socket that was shut down or whose peer
hung up fails with EPIPE instead of killing a host process that hasn't
ignored SIGPIPE. The app only ignores it through Ghostty's startup, so
package tests and any other embedder had no protection. Darwin refuses
the option once the peer is gone, so both set it before connecting or
listening. Accepted sockets inherit it from the listener, as in the two
sibling fixtures #15116 fixed.

On a socket-creation failure the relay closes the descriptor and reports
the existing "failed to create local relay socket" error, so the app's
behavior is unchanged.

Refs #15488

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ammachado added a commit to ammachado/cmux that referenced this pull request Oct 1, 2026
`wait-for -L`/`-U` keep their lock at /tmp/cmux-wait-for-<name>.lock,
the shared world-writable pattern manaflow-ai#15116 removed for signals: another
user can pre-create the lock to block a channel forever, or plant a
symlink at it. These cases pin the lock beside the signals in the
private per-user directory: exclusive until unlocked, a blocked locker
wakes on unlock, no symlink is followed, distinct names get distinct
files, and a group-writable directory is refused.

Red: the fixture does not compile yet, because TmuxWaitForSignal has
no lock API (lockPath, lock, unlock). This is a compile-level red, not
a behavioral one: the CLI's lock path needs a live socket to reach.

  python3 tests/test_cli_tmux_wait_for_private_dir.py

_Claude Code on behalf of Adriano Machado (@ammachado)_

_This was generated by an AI agent and may contain inaccuracies.
Please verify before relying on it._
jeremysamuel13 added a commit to jeremysamuel13/cmux that referenced this pull request Oct 7, 2026
…the clipboard

Since manaflow-ai#15116, cmux drops every OSC 52 clipboard write from a remote
terminal: cmux ssh mirror panes and cmux ssh-tmux mirror panes. That is
the right default, but it also removes the only copy path agent TUIs have
on a remote host. OpenCode, Codex and Claude Code capture the mouse,
select text themselves, and copy it with OSC 52; without a display server
on the remote they have nothing else to write to. Before 0.65.0 these
copies reached the Mac clipboard (manaflow-ai#18324, and the OSC 52 half of manaflow-ai#17472).

Add terminal.trustedClipboardWriteHosts, a list of ssh_config-style host
globs, empty by default. A remote surface records the SSH hosts its
output comes from (the destination and, for a brokered connection, the
HostName it reaches, matched the way terminal.uploadCommands.hostPattern
is). The Ghostty write_clipboard_cb admits an OSC 52 write from such a
surface when one of its hosts matches a trusted pattern. Local terminals
and Cloud writer shims keep their existing behavior, and OSC 52 clipboard
reads stay denied.

The gate reads the setting on every write, so a cmux.json edit applies on
the next copy without restarting. Remote exec PTYs that still run ssh
inside a local Ghostty PTY carry no host and stay blocked.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

full-ci EXPENSIVE: full macOS tests/builds; overrides selective PR routing. Not needed for normal checks. merged-unverified A judging check was not green at merge; see the merge receipt comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant