Skip to content

Fix cmux --version root walk - #1255

Merged
lawrencecchen merged 4 commits into
mainfrom
task-cmux-version-hang-investigation
Mar 12, 2026
Merged

lawrencecchen merged 4 commits into
mainfrom
task-cmux-version-hang-investigation

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Mar 12, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • stop the CLI version lookup from walking past / into /.. when searching for bundle metadata
  • run tests/test_cli_version_memory_guard.py in PR CI and in nightly/release build workflows

Testing

  • ./scripts/download-prebuilt-ghosttykit.sh
  • ./scripts/reload.sh --tag version-hang-fix
  • /Users/tiffanysun/Library/Developer/Xcode/DerivedData/cmux-version-hang-fix/Build/Products/Debug/cmux DEV version-hang-fix.app/Contents/Resources/bin/cmux --version

Issues

  • Related: user report that cmux --version hangs from the packaged app on macOS Sequoia

Summary by cubic

Fixes a hang in cmux --version by stopping bundle metadata lookup at the filesystem root and standardizing paths during traversal. Adds a regression test to CI, nightly, and release; PR CI runs against the newest built CLI.

  • Bug Fixes
    • Use standardizedFileURL and parentSearchURL to canonicalize paths and stop at "/" (prevents walking into "/..").
    • Apply the same root guard to Info.plist search and standardize search roots.
    • Run tests/test_cli_version_memory_guard.py in PR CI (auto-picks newest CLI from Xcode DerivedData), nightly, and release; fail fast if the CLI binary is missing.

Written for commit 85f9ad6. Summary will update on new commits.

Summary by CodeRabbit

  • Testing

    • Added CLI memory-guard regression to CI, nightly, and release workflows and added a large-scale test fixture that simulates many app bundles to exercise memory safeguards.
  • Bug Fixes

    • Improved CLI path traversal canonicalization and added safety checks to prevent edge-case filesystem traversal and related failures.

@vercel

vercel Bot commented Mar 12, 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 Mar 12, 2026 9:36am

@coderabbitai

coderabbitai Bot commented Mar 12, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds CI steps (ci.yml, nightly.yml, release.yml) to run a CLI memory-guard regression that locates/validates the built cmux binary and runs tests/test_cli_version_memory_guard.py. Refactors CLI/cmux.swift to canonicalize URLs and add parentSearchURL(for:) to stop unsafe directory traversal.

Changes

Cohort / File(s) Summary
CI Workflow Regression Tests
.github/workflows/ci.yml, .github/workflows/nightly.yml, .github/workflows/release.yml
Inserted steps to locate the cmux CLI binary (DerivedData path), validate executability, set CMUX_CLI_BIN/CLI_BINARY, and run tests/test_cli_version_memory_guard.py. Steps fail early if binary missing or not executable.
Path Canonicalization & Traversal
CLI/cmux.swift
Replaced ad-hoc parent traversal with parentSearchURL(for:), applied standardizedFileURL to starting/root URLs, and added guards to stop traversal at canonical roots to prevent infinite/root walks.
Memory-guard Test Fixture
tests/test_cli_version_memory_guard.py
Test fixture expanded to create many dummy .app bundles (mass creation) to exercise the CLI memory-guard path during --version testing.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • #1121: Modifies CLI/cmux.swift path canonicalization/traversal and touches the same memory-guard regression test.
  • #447: Related changes to the CI tests job; also adjusted GitHub Actions test execution flow.

Suggested labels

aardvark, codex

Poem

🐰
I hopped through paths both wide and narrow,
Canonicalized each root and burrow.
Tests guard memory, bundles in a throng,
I twitch my nose and hum a bug-free song. 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Fix cmux --version root walk' clearly and concisely describes the main change: fixing a bug in the version command's root directory traversal behavior.
Description check ✅ Passed The PR description includes all required template sections: Summary (what changed and why), Testing (how tested), and covers the main objectives clearly.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch task-cmux-version-hang-investigation

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

@cubic-dev-ai cubic-dev-ai 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.

No issues found across 4 files

@greptile-apps

greptile-apps Bot commented Mar 12, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a macOS-specific infinite loop in cmux --version caused by Foundation's URL.deletingLastPathComponent() producing "/.." (rather than "/") when called on the filesystem root, so the old parent.path == current.path guard never fired and the directory walk spun forever.

Changes:

  • CLI/cmux.swift: Introduces a parentSearchURL(for:) helper that standardizes the URL with standardizedFileURL before each upward step, explicitly returns nil when the path is "/" or empty, and also returns nil when the parent's path is unchanged (secondary safety net). Both versionInfoFromProjectFile() and candidateInfoPlistURLs() now use this helper and initialize current with .standardizedFileURL.
  • tests/test_cli_version_memory_guard.py: New regression test that builds a fixture app bundle containing 40 000 zero-byte .app siblings, runs cmux --version against it, and enforces a 64 MB RSS ceiling and a 10-second wall-clock timeout to guard against re-introduction of the hang.
  • CI/nightly/release workflows: The memory-guard test is wired into all three pipeline stages so the regression cannot silently re-enter main or a release build.

Confidence Score: 4/5

  • Safe to merge; the root-cause fix is correct and well-tested, with only minor style inconsistencies remaining.
  • The parentSearchURL helper correctly addresses the Foundation quirk where deletingLastPathComponent() on "/" produces "/..". The dual guard (explicit "/" check + parent-equals-current check) is robust. The regression test is well-structured and covers the exact hang scenario. The only open items are non-critical: the fallback searchRoots URLs could also be standardized for full consistency, and the CI find command could prefer the most-recently-built binary on non-ephemeral runners.
  • CLI/cmux.swift lines 8329–8332 (un-standardized fallback search roots) and .github/workflows/ci.yml line 158 (non-deterministic find for binary lookup).

Important Files Changed

Filename Overview
CLI/cmux.swift Core fix: replaces fragile parent.path == current.path root guard with a parentSearchURL helper that standardizes URLs and explicitly checks for "/" before each upward step; the fallback searchRoots array on lines 8329–8332 is not standardized but doesn't participate in the root-walk comparison so it is harmless.
tests/test_cli_version_memory_guard.py New regression test: builds a fixture app bundle containing 40 000 junk .app files, runs the CLI binary with --version, and enforces both a 64 MB RSS ceiling and a 10-second timeout; strict EXPECTED_STDOUT comparison provides format-regression coverage.
.github/workflows/ci.yml Adds a post-unit-test step that locates the Debug binary via find … -print -quit and invokes the memory-guard script; find order is non-deterministic but ephemeral CI runners make a stale-binary collision unlikely.
.github/workflows/nightly.yml Adds the memory-guard step after the universal-binary lipo check using a hardcoded Release bundle path; straightforward and consistent with the release workflow pattern.
.github/workflows/release.yml Adds the memory-guard step gated on guard_release_assets skip flag, matching the surrounding release-step pattern correctly.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["resolvedExecutableURL()"] --> B["current = executableURL\n.deletingLastPathComponent()\n.standardizedFileURL"]
    B --> C{Check current dir\nfor Info.plist /\nproject marker}
    C -- found --> D[Append to candidates / return info]
    C -- not found --> E["parentSearchURL(for: current)"]
    E --> F{"standardized.path\n== '/' or empty?"}
    F -- yes --> G[Return nil → break loop]
    F -- no --> H["parent = standardized\n.deletingLastPathComponent()\n.standardizedFileURL"]
    H --> I{"parent.path\n== current.path?"}
    I -- yes --> G
    I -- no --> J[Return parent]
    J --> C

    style G fill:#f96,color:#000
    style D fill:#6c6,color:#000
Loading

Comments Outside Diff (2)

  1. CLI/cmux.swift, line 8329-8332 (link)

    Fallback searchRoots uses non-standardized URLs

    The two fallback searchRoots entries are computed without .standardizedFileURL, inconsistent with every other URL constructed in this function. While this path isn't involved in the root-walk comparison (so it doesn't reproduce the original hang), a path containing .. components or a symlink like /var → /private/var could cause fileManager.enumerator(at:) to open a different directory than expected, or produce entries whose paths don't compare equal to canonicalized siblings tracked in seen.

  2. tests/test_cli_version_memory_guard.py, line 86-88 (link)

    Junk entries are regular files, not directories

    open(os.path.join(resources_path, f"junk-{index:05d}.app"), "wb").close()

    These are created as zero-byte files, whereas a real macOS app bundle is a directory. The fileManager.enumerator in the searchRoots fallback scan does match them by pathExtension == "app", so the test correctly stresses the old code path. However, a reader might assume they are directories, and the intent (simulating many sibling app bundles) is worth a short comment. This is cosmetic but could trip up future maintainers who need to understand why the test uses so many entries.

    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!

Last reviewed commit: f41a831

Comment thread .github/workflows/ci.yml Outdated
Comment on lines +158 to +162
CLI_BIN="$(find "$HOME/Library/Developer/Xcode/DerivedData" -path "*/Build/Products/Debug/cmux" -print -quit)"
if [ -z "${CLI_BIN:-}" ] || [ ! -x "$CLI_BIN" ]; then
echo "cmux CLI binary not found in DerivedData" >&2
exit 1
fi

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.

find order is non-deterministic; stale binary risk on self-hosted runners

CLI_BIN="$(find "$HOME/Library/Developer/Xcode/DerivedData" -path "*/Build/Products/Debug/cmux" -print -quit)"

find … -print -quit stops at the first file found in directory-traversal order, which is filesystem-defined. On ephemeral GitHub-hosted runners this is fine, but on self-hosted or cached macOS runners a previous run's DerivedData directory could persist and cause the test to pick up an older binary rather than the one just built. Consider scoping the search to the project-specific DerivedData folder (e.g., DerivedData/GhosttyTabs-*) or sorting by modification time (e.g., via find … -print0 | xargs -0 ls -t | head -1) to always select the most-recently-built binary.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3152b4ac-5917-4ec1-8d82-5145d59804d5

📥 Commits

Reviewing files that changed from the base of the PR and between 74b0d89 and f41a831.

📒 Files selected for processing (4)
  • .github/workflows/ci.yml
  • .github/workflows/nightly.yml
  • .github/workflows/release.yml
  • CLI/cmux.swift

Comment thread .github/workflows/ci.yml
Comment on lines +154 to +164
- name: Run CLI version memory guard regression
run: |
set -euo pipefail

CLI_BIN="$(find "$HOME/Library/Developer/Xcode/DerivedData" -path "*/Build/Products/Debug/cmux" -print -quit)"
if [ -z "${CLI_BIN:-}" ] || [ ! -x "$CLI_BIN" ]; then
echo "cmux CLI binary not found in DerivedData" >&2
exit 1
fi

CMUX_CLI_BIN="$CLI_BIN" python3 tests/test_cli_version_memory_guard.py

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Locate cmux-unit scheme =="
SCHEME="$(fd 'cmux-unit\.xcscheme$' . | head -n 1)"
if [ -z "${SCHEME:-}" ]; then
  echo "cmux-unit.xcscheme not found" >&2
  exit 1
fi
echo "$SCHEME"

echo
echo "== Build/Test references inside cmux-unit scheme =="
sed -n '/<BuildAction>/,/<\/BuildAction>/p;/<TestAction/,/<\/TestAction>/p' "$SCHEME" \
  | rg -n 'BlueprintName=|BuildableName='

echo
echo "== Current CI lookup =="
rg -n 'cmux-unit|Build/Products/Debug/cmux' .github/workflows/ci.yml

echo
echo "Expected results:"
echo "1) The scheme output should show a BuildableReference for the cmux product/target, or another explicit path proving the CLI is built."
echo "2) The DerivedData search should be scopeable to GhosttyTabs-* instead of every entry under DerivedData."

Repository: manaflow-ai/cmux

Length of output: 1365


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== All Xcode targets in GhosttyTabs.xcodeproj =="
grep -r 'target "' GhosttyTabs.xcodeproj/project.pbxproj | sed 's/.*target "//' | sed 's/".*//' | sort -u

echo
echo "== Check what test_cli_version_memory_guard.py expects =="
if [ -f tests/test_cli_version_memory_guard.py ]; then
  head -n 50 tests/test_cli_version_memory_guard.py | grep -E 'CMUX_CLI_BIN|argv|cmux' || echo "No direct reference found in first 50 lines"
fi

echo
echo "== Search for CLI or command-line target references =="
rg -i 'cli|command.?line|executable' GhosttyTabs.xcodeproj --type xml | head -n 20

echo
echo "== Check if cmux is defined as a separate product (not app) =="
rg 'BuildableReference.*cmux[^.]' GhosttyTabs.xcodeproj --type xml --context 3

Repository: manaflow-ai/cmux

Length of output: 108


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Directory structure of GhosttyTabs.xcodeproj =="
find GhosttyTabs.xcodeproj -type f -name "*.pbxproj" -o -name "*.xcscheme" | head -20

echo
echo "== List all scheme files =="
ls -la GhosttyTabs.xcodeproj/xcshareddata/xcschemes/ 2>/dev/null || echo "No schemes directory found"

echo
echo "== Content of test_cli_version_memory_guard.py =="
if [ -f tests/test_cli_version_memory_guard.py ]; then
  cat tests/test_cli_version_memory_guard.py
else
  echo "File not found at tests/test_cli_version_memory_guard.py"
  echo "Searching for it:"
  find . -name "*test_cli*" -type f 2>/dev/null
fi

echo
echo "== Search for cmux references in all schemes =="
rg -l 'cmux' GhosttyTabs.xcodeproj/xcshareddata/xcschemes/ 2>/dev/null | xargs -I {} sh -c 'echo "File: {}"; rg "BlueprintName|BuildableName" "{}" | head -10'

echo
echo "== Check if cmux (standalone binary) is referenced anywhere in build output =="
rg 'cmux.*product|executable.*cmux' GhosttyTabs.xcodeproj --type xml 2>/dev/null | head -10

Repository: manaflow-ai/cmux

Length of output: 10928


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Full ci.yml content around the regression test (lines 150-170) =="
sed -n '150,170p' .github/workflows/ci.yml

echo
echo "== Check if there are other build steps before this regression test =="
sed -n '90,165p' .github/workflows/ci.yml | grep -E "^\s+-\s+name:|run:" | head -20

echo
echo "== Search for any CLI build or compilation steps =="
rg 'swift build|xcodebuild.*-product|CLI|executable' .github/workflows/ci.yml | head -20

echo
echo "== Check if cmux binary is produced by building the main app =="
rg -A 5 'Build/Products/Debug/cmux' .github/workflows/ci.yml

Repository: manaflow-ai/cmux

Length of output: 2194


The CI workflow will fail because cmux-unit scheme does not produce a standalone CLI binary.

The cmux-unit scheme only builds cmux.app (app bundle) and cmuxTests.xctest (test bundle). The regression test expects a standalone cmux executable at Build/Products/Debug/cmux, which this scheme does not create. The find command at line 158 will not find any matching binary, and the script will exit with code 1 at line 162 before the test even runs.

Either add a separate build step to produce the CLI binary, modify the scheme to build it, or adjust how the binary is resolved.

@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)
tests/test_cli_version_memory_guard.py (1)

76-89: ⚠️ Potential issue | 🟠 Major

This fixture never exercises the / termination path.

Because build_fixture() always creates a valid cmux.app/Contents/Info.plist, --version can resolve bundle metadata before ancestor traversal needs the new root guard. That leaves the actual "/" -> "/.." regression from the PR objective untested, so it could slip back in while this test still passes. Please add a second fixture/run that omits bundle metadata or otherwise forces the lookup to climb all the way to the root boundary under the 40k-entry directory.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_cli_version_memory_guard.py` around lines 76 - 89, The test
currently always creates a valid Info.plist under contents_path so the --version
path resolves bundle metadata early; add a second fixture/run that omits the
Info.plist (or creates a .app without a Contents/Info.plist) so the code must
traverse ancestors up to the root guard (the "/" -> "/..") under the 40k-entry
directory. Concretely, after the existing setup that writes Info.plist, add
another case that either (a) creates a sibling .app entry (using resources_path
and JUNK_APP_COUNT) but does not create contents_path/Info.plist, or (b) deletes
or renames the created Info.plist before invoking the CLI, then invoke the same
--version invocation to ensure ancestor traversal hits the filesystem root
boundary; reference resources_path, contents_path, JUNK_APP_COUNT and the test's
existing invocation to locate where to add the extra run.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@tests/test_cli_version_memory_guard.py`:
- Around line 76-89: The test currently always creates a valid Info.plist under
contents_path so the --version path resolves bundle metadata early; add a second
fixture/run that omits the Info.plist (or creates a .app without a
Contents/Info.plist) so the code must traverse ancestors up to the root guard
(the "/" -> "/..") under the 40k-entry directory. Concretely, after the existing
setup that writes Info.plist, add another case that either (a) creates a sibling
.app entry (using resources_path and JUNK_APP_COUNT) but does not create
contents_path/Info.plist, or (b) deletes or renames the created Info.plist
before invoking the CLI, then invoke the same --version invocation to ensure
ancestor traversal hits the filesystem root boundary; reference resources_path,
contents_path, JUNK_APP_COUNT and the test's existing invocation to locate where
to add the extra run.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: aca85900-f2ea-494d-be53-220e402e7f84

📥 Commits

Reviewing files that changed from the base of the PR and between f41a831 and 85f9ad6.

📒 Files selected for processing (3)
  • .github/workflows/ci.yml
  • CLI/cmux.swift
  • tests/test_cli_version_memory_guard.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • .github/workflows/ci.yml

@lawrencecchen
lawrencecchen merged commit 292359f into main Mar 12, 2026
13 checks passed
@lawrencecchen
lawrencecchen deleted the task-cmux-version-hang-investigation branch March 12, 2026 09:45
bn-l pushed a commit to bn-l/cmux that referenced this pull request Apr 3, 2026
…n-hang-investigation

Fix cmux --version root walk
This was referenced Apr 25, 2026

This branch was successfully deployed

1 active deployment
Preview — 85f9ad6e Deployed Mar 12, 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.

2 participants