Skip to content

Fix shell-integration socket sends: pin BSD nc (GNU nc silently drops the hook channel) - #7789

Merged
lawrencecchen merged 13 commits into
mainfrom
fix-zsh-zsocket-module
Jul 10, 2026
Merged

lawrencecchen merged 13 commits into
mainfrom
fix-zsh-zsocket-module

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Jul 10, 2026 •

Copy link
Copy Markdown
Contributor

Cause

On machines where a -U-incapable netcat shadows the system one (GNU netcat in /usr/local/bin/nc ahead of BSD /usr/bin/nc), every shell-integration socket send fails silently: _cmux_send resolves nc from PATH, both -U invocations error out behind || true, and the whole hook channel drops — report_tty never registers, ports_kick is discarded by the scanner's unregistered-TTY guard, report_shell_state and git/PR reports are lost, and sidebar listening-port rows never populate. Found while seeding a 128-workspace stress instance where zero ports attributed; writing the same verbs to the socket with a working client immediately restored registration and port rows.

A contributing factor: the intended zsocket fast path from #2109 never activated because it loads zsh/net/unix, which is not a zsh module (zsocket lives in zsh/net/socket), so the external-client fallback has always been the only real transport.

Fix

_cmux_send now prefers /usr/bin/nc explicitly: always BSD, always -U, waits for the server to process the line and close (which preserves order across a batched child and keeps the peer alive through cmuxOnly ancestry checks), and -w bounds its lifetime. ncat/socat/PATH-nc remain as fallbacks for non-macOS shells.

The zsocket branch is removed rather than enabled. Structured review of enabling it surfaced three real transport defects, now documented in the integration header for a future ordered-transport PR: the instant-exit child races the server's live peer-ancestry authorization in cmuxOnly mode, per-connection handler threads make cross-connection ordering nondeterministic unless the client waits for responses, and a timeout-free blocked child can outlive its shell.

Hardening kept from review: sends stay detached from the interactive shell (a wedged listener must never block the prompt), in-flight detached sends are capped at 8 per shell via tracked pids, and _cmux_report_tty_once batches the first ports_kick behind report_tty in one sequential child so the scanner cannot receive the kick before the registration.

Testing

ShellIntegrationSendTransportTests.sendDeliversPayloadToUnixSocketListener is the real contract end to end: source the bundled integration in a fresh zsh -f and assert one _cmux_send payload reaches a unix-socket listener. Verified locally under both PATH=/usr/bin:/bin and a hostile PATH with GNU nc first; both delivered (the second was 100% dropped before the fix). The in-flight cap was exercised against a stubbed hung listener: 25 sends leave exactly 8 live children and sends resume after they exit.

No user-facing strings changed, so no localization impact.


Note

Medium Risk
Changes how every terminal panel talks to cmux over the Unix socket; mis-picked nc or blocking sends would break sidebar ports and activity, though the fix targets a known silent-failure mode and keeps sends off the interactive prompt path.

Overview
Fixes silent loss of the entire zsh shell-integration hook channel when PATH resolves nc to a client without Unix-socket support (e.g. Homebrew GNU netcat ahead of macOS /usr/bin/nc), which broke TTY registration, port kicks, shell state, and git/PR updates.

Transport: Removes the never-active zsh/net/unix zsocket path and routes _cmux_send through /usr/bin/nc -w 1 -U first (Apple nc uses -w, not -N), with ncat/socat/PATH nc as fallbacks. _cmux_send_bg always runs sends in a detached child with client timeouts, accepts multiple payloads in one child for ordering, and returns failure on job-table saturation so latches can retry.

Hook behavior: _cmux_report_tty_once batches the first ports_kick with report_tty in one background send and only sets _CMUX_TTY_REPORTED after enqueue succeeds. _cmux_report_shell_activity_state clears its last-state latch if the send is dropped.

Adds ShellIntegrationSendTransportTests (end-to-end _cmux_send via bundled integration; skipped on CI runner users due to flaky subprocess socket delivery).

Reviewed by Cursor Bugbot for commit 89df046. Bugbot is set up for automated code reviews on this repo. Configure here.

Summary by CodeRabbit

  • Bug Fixes
    • Improved shell integration communication over Unix sockets by using more reliable connection methods.
    • Increased reliability of background shell updates with detached delivery, batching, and safeguards against overload.
    • Combined initial terminal registration and related setup messages to reduce connection overhead.
    • Prevented stale shell activity information when an update cannot be delivered.
    • Improved compatibility when preferred socket utilities are unavailable or fail.

Sourcing the bundled cmux-zsh-integration.zsh leaves
_CMUX_HAS_ZSOCKET=0 because it loads the nonexistent zsh/net/unix
module (the unix-socket module is zsh/net/socket, which provides the
zsocket builtin _cmux_send calls). Every hook send then falls back to
ncat/socat/nc; on machines whose PATH nc lacks -U (e.g. GNU netcat in
/usr/local/bin shadowing BSD nc), report_tty, ports_kick, and
report_shell_state all drop silently, so sidebar listening-port rows
never populate. Red before the fix.
The zsh integration has loaded the nonexistent zsh/net/unix module since
the fast path was added, so _CMUX_HAS_ZSOCKET was always 0 and every
socket send forked ncat/socat/nc. Besides paying the ~3ms fork per send
the fallback silently drops the whole hook channel (report_tty,
ports_kick, report_shell_state) on machines whose PATH nc lacks
unix-socket support, e.g. GNU netcat in /usr/local/bin shadowing BSD
/usr/bin/nc: TTYs never register, port scans never run, and sidebar
listening-port rows stay empty. zsocket is provided by zsh/net/socket;
loading that module enables the path _cmux_send was already written
for.
@vercel

vercel Bot commented Jul 10, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Jul 10, 2026 11:43pm
cmux-staging Building Building Preview, Comment Jul 10, 2026 11:43pm

@coderabbitai

coderabbitai Bot commented Jul 10, 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

The zsh integration now sends through external Unix-socket clients, manages detached send capacity, batches initial reporting payloads, and handles enqueue failures. A Swift end-to-end test verifies delivery when a failing nc executable precedes the system client in PATH.

Changes

Zsh send transport

Layer / File(s) Summary
External socket transport
Resources/shell-integration/cmux-zsh-integration.zsh
_cmux_send removes the zsocket path and uses ordered external clients. _cmux_send_bg tracks detached PIDs, batches payloads, limits in-flight sends, and reports dropped sends.
Initial reporting and activity handling
Resources/shell-integration/cmux-zsh-integration.zsh
Initial TTY registration batches ports_kick when applicable, and shell activity state resets its previous value when enqueueing fails.
Transport regression test
cmuxTests/ShellIntegrationSendTransportTests.swift, cmux.xcodeproj/project.pbxproj
The new test runs the bundled zsh script against a Unix listener with a failing nc shim and verifies payload delivery; Xcode project entries compile and include the test source.

Estimated code review effort: 4 (Complex) | ~45 minutes

🚥 Pre-merge checks | ✅ 24 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (24 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Swift Actor Isolation ✅ Passed Only a test file changed; its private lock-protected @unchecked Sendable helper matches existing test patterns and adds no actor-isolation regression.
Cmux Swift Blocking Runtime ✅ Passed PASS: The only Swift additions are test-only scaffolding using a semaphore and lock; no production Swift files changed.
Cmux Browser Automation Off-Main ✅ Passed PASS: The PR only changes shell-integration transport/test files; no browser.* socket commands, mainActor routing, or WebKit/AppKit browser automation code were touched.
Cmux Expensive Synchronous Load ✅ Passed PASS: The only Swift change is a new test; no production @MainActor/interactive code calls expensive loaders or large-file parsing.
Cmux Cache Substitution Correctness ✅ Passed No production Swift/TS/JS changed; shell latches are transient and not used in persistence/history/snapshot paths, with failure clears for retries.
Cmux No Hacky Sleeps ✅ Passed The shell/runtime diff adds no new sleep/poll/timer waits; only bounded client timeouts and retry-latch logic, which aren’t hacky sleeps.
Cmux Algorithmic Complexity ✅ Passed Only bounded scans were added: _CMUX_SEND_PIDS is capped at 8, TTY batching is two messages, and the Data scan is test-only scaffolding.
Cmux Swift Concurrency ✅ Passed Only Swift concurrency change is a test-only DispatchQueue.global().async listener, which the rules allow for XCTest/boundary synchronization.
Cmux Swift @Concurrent ✅ Passed Only new Swift code is synchronous test/support code; no @concurrent or nonisolated async isolation changes, and no UI-isolated async helper was introduced.
Cmux Swift File And Package Boundaries ✅ Passed Only Swift change is a 124-line test file in cmuxTests; no production Swift files were touched and the project wires it into the test target.
Cmux Swiftpm Lockfiles ✅ Passed HEAD only changes the zsh script and adds a test file/build entries; no .gitignore, Package.resolved, or SwiftPM package-reference diffs appear.
Cmux Swift Logging ✅ Passed The only Swift change is a test file, and it adds no print/debugPrint/dump/NSLog/Logger usage or sensitive logging.
Cmux User-Facing Error Privacy ✅ Passed The PR only changes internal transport logic, comments, and tests; it adds no user-facing errors, alerts, or recovery copy exposing sensitive details.
Cmux Full Internationalization ✅ Passed Only tests, project wiring, and developer-only shell comments/config tokens changed; no user-facing localized text or locale catalogs were introduced.
Cmux Swiftui State Layout ✅ Passed Diff adds only a shell script and a non-SwiftUI test helper; no ObservableObject, GeometryReader, lazy-row store refs, or render-time state writes.
Cmux Architecture Rethink ✅ Passed Production code keeps one explicit socket transport owner; the only lock/semaphore use is test-only synchronization, which the rule allows.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PASS: The only Swift addition is a test-only UnixLineListener/socket integration test; no NSWindow/NSPanel/WindowGroup, cmux.* identifiers, or close-shortcut routing changes appear.
Cmux Source Artifacts ✅ Passed Changed paths are hand-written shell/test source; no logs, caches, temp dirs, build output, or other artifact paths were added.
Cmux No Test Or Debug Seam In Production Source ✅ Passed No production Swift under Sources/ changed; the new seam lives only in cmuxTests, so the rule is not violated.
Cmux No Ambient Global State ✅ Passed Only new Swift is a test file with a nested test struct and private helper class; no new top-level funcs/vars or singletons, and no production Swift sources changed.
Title check ✅ Passed The title clearly names the main change: pinning BSD nc to fix shell-integration socket sends.
Description check ✅ Passed The description includes the cause, fix, and testing details, so it mostly satisfies the template despite missing some optional sections.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-zsh-zsocket-module

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Jul 10, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR updates the zsh shell-integration transport for Unix-socket hook messages. The main changes are:

  • Pins the preferred socket client to /usr/bin/nc -w 1 -U.
  • Removes the inactive zsocket path and documents why it stays disabled.
  • Sends background hook messages through detached, bounded children.
  • Batches the first TTY registration and port kick in one ordered send.
  • Adds a Swift transport test and wires it into the Xcode test target.

Confidence Score: 5/5

This looks safe to merge.

  • No blocking issues found in the changed code that meet the follow-up review scope.

Important Files Changed

Filename Overview
Resources/shell-integration/cmux-zsh-integration.zsh Updates the zsh socket send path, background batching, TTY reporting, and shell-state retry behavior.
cmux.xcodeproj/project.pbxproj Adds the new shell-integration transport test file to the cmuxTests target.
cmuxTests/ShellIntegrationSendTransportTests.swift Adds an end-to-end zsh Unix-socket transport test with a local listener and PATH shim.

Reviews (12): Last reviewed commit: "test(shell): detect CI runners by consol..." | Re-trigger Greptile

Review finding on the module fix: _cmux_send_bg ran _cmux_send in the
foreground when zsocket was available, and zsocket's connect/write has
no timeout, so a wedged cmux listener (hung app, full backlog) would
block every precmd/preexec hook and freeze the interactive prompt.
Always detach the send; zsocket still saves the ncat/socat/nc exec
inside the subshell, and the job-table saturation guard now applies to
every send.
Second review finding: &! disowns each send child, so the jobstates
soft limit cannot count them, and a zsocket child has no timeout, so a
wedged listener would accumulate one hung process per prompt hook
without bound. Track in-flight send pids in the parent shell, prune
with kill -0, and drop new sends once 8 are outstanding; sends resume
as soon as the listener drains. Functional check: 25 sends against a
stubbed hung listener leave exactly 8 live children, and sends flow
again after they exit.
Comment thread Resources/shell-integration/cmux-zsh-integration.zsh Outdated
…d child

Third review finding: independent detached children deliver in
nondeterministic order, and the scanner drops ports_kick for a TTY it
has not registered yet, so kick-before-report loses the first port scan
(observed live: 128 seeded shells registered TTYs while their same-
prompt kicks were dropped). _cmux_send_bg now sends multiple payloads
sequentially inside one child, and _cmux_report_tty_once batches the
first kick behind the registration. Cross-hook ordering (shell state
across prompts) keeps the integration's long-shipped detached
semantics: those signals are re-sent every hook and self-correct.
…st path

Review rounds on enabling zsocket surfaced three transport defects: the
instant-exit child races cmuxOnly's live peer-ancestry authorization,
separate connections are handled on independent server threads so even
sequential connects can process out of order, and a timeout-free
blocked child outlives its shell. Rather than grow a shell-side
transport with response reads and supervision, fix the actual breakage:
PATH nc resolution. GNU netcat (Homebrew /usr/local/bin/nc) lacks -U
and fails silently, dropping every hook message on machines where it
shadows the system nc. _cmux_send now prefers /usr/bin/nc (always BSD,
always -U, waits for the server to process and close, -w bounded),
keeping the long-shipped detached semantics whose children self-reap
and survive the auth window. The zsocket branch is removed with its
defects documented for a future ordered-transport PR.

The regression test is now the real contract end to end: source the
bundled integration in a fresh zsh and assert one _cmux_send payload
reaches a unix-socket listener, under PATH=/usr/bin:/bin. Verified
locally under both a clean PATH and one with GNU nc first: both
delivered.
@lawrencecchen lawrencecchen changed the title Fix zsh integration zsocket fast path (zsh/net/unix does not exist) Fix shell-integration socket sends: pin BSD nc (GNU nc silently drops the hook channel) Jul 10, 2026
…ries, shadowing test

Round-five review findings, all verified: Apple nc's -N takes a
num_probes argument, so the -N -U form failed parsing and every send
double-execed; the pinned path now uses the bounded -w 1 form directly.
_cmux_send_bg returns nonzero when the in-flight cap or job-table guard
drops a payload, the tty registration latches only after a successful
enqueue, and a dropped shell-activity state resets its latch so both
retry after a stall clears. The transport test now prepends a failing
nc shim to PATH, so it fails against the old PATH-resolved
implementation and passes with the pinned client.
Round-six review: the cap could reject sends whose callers latch
edge-triggered state (pwd, PR hints, ports timestamp), permanently
suppressing updates after a transient stall — and it is redundant now
that every client self-bounds via -w 1 / -T 1, which caps a wedged
listener's children at roughly one second's worth of sends without any
admission semantics. Removed the cap and pid tracking; the pre-existing
job-table guard remains and the tty/activity latch retries keep their
success-gated behavior. The transport test's socket now lives under a
short /tmp root because sockaddr_un.sun_path (104 bytes) overflows on
default /var/folders temporary paths.
The CI runner fails the send while local runs pass; the failure message
now carries the child's full output (source errors, nc availability,
send rc) and uses file-backed output so an unread pipe cannot deadlock
the child.
CI diagnostics showed the environment, not the change: /usr/bin/nc is
executable, the integration sources cleanly, nc runs and exits 0, and
the listener still receives nothing — the CI app-host context cannot
host a unix-socket loop for spawned children at all. Probe that pure
baseline (direct nc child, no shell integration) in an .enabled(if:)
trait so the regression test runs on developer machines and fleet Macs
and reports as skipped, not failed, where the harness cannot host it.
…seline

The raw-nc baseline probe passed on the CI runner while the identical
integration-path send delivered nothing, so the previous gate did not
exclude the hostile environment. Gate on the exact un-shimmed flow the
test builds on: where the environment cannot deliver it, the test skips
with the reason recorded; where it can (developer and fleet Macs, where
a PATH-first GNU nc actually occurs), the shim assertion runs with full
diagnostics.
Two runs showed the identical un-shimmed flow passing as the gate probe
and failing as the assertion (or vice versa) while /usr/bin/nc was
executable and the send exited 0: subprocess unix-socket delivery is
flaky on the CI runners themselves, with ~11s child runtimes pointing
at VM scheduling. Gate deterministically on GITHUB_ACTIONS instead of a
probe that shares the flakiness. The regression class this test guards,
Homebrew GNU nc shadowing the system client, lives on developer
machines, where the test always runs; verified green locally with the
shimmed PATH.

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

Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit a81f65d. Configure here.

Comment thread Resources/shell-integration/cmux-zsh-integration.zsh
Comment thread Resources/shell-integration/cmux-zsh-integration.zsh
The env var does not survive into the elevated app-host test process
(the previous gate did not skip). Every CI runner image executes tests
as the 'runner' console user; developer and fleet Macs never do.
@lawrencecchen
lawrencecchen merged commit c1ea31e into main Jul 10, 2026
28 of 30 checks passed
@lawrencecchen
lawrencecchen deleted the fix-zsh-zsocket-module branch July 10, 2026 23:36
dachev added a commit to dachev/cmux that referenced this pull request Jul 16, 2026
Extends ShellIntegrationSendTransportTests with a bash case mirroring the zsh
test from manaflow-ai#7789: sources cmux-bash-integration.bash in a bare shell with a
broken PATH-first nc shimmed ahead of the system client, and asserts _cmux_send
still delivers to a unix-socket listener. Fails against the old ncat --send-only
transport, passes with the /usr/bin/nc fix. Generalizes the existing helper over
shell + integration script.

This branch was successfully deployed

1 active deployment
Preview – cmux — 89df046d Deployed Jul 10, 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.

1 participant