Skip to content

Fix Claude NODE_OPTIONS restore cache path - #3541

Closed
austinywang wants to merge 43 commits into
mainfrom
fix/node-options-restore-cache-dir
Closed

austinywang wants to merge 43 commits into
mainfrom
fix/node-options-restore-cache-dir

Conversation

@austinywang

@austinywang austinywang commented May 5, 2026 •

Copy link
Copy Markdown
Contributor

Based on #3278, with maintainer-side conflict resolution against current origin/main.

Summary

  • Move the Claude NODE_OPTIONS restore shim to a persistent cache path instead of $TMPDIR.
  • Align macOS Swift CLI, bash wrapper, and Go daemon on ~/Library/Caches/com.cmuxterm.app/cmux-claude-node-options/restore-node-options.cjs.
  • Guard cache roots so NODE_OPTIONS --require=<path> never receives paths containing whitespace.
  • Keep Linux/non-mac behavior on absolute, whitespace-free XDG_CACHE_HOME, otherwise fall back to ~/.cache/cmux.

Validation

  • git pull origin main completed and conflicts are resolved on this branch.
  • git diff --check passed.
  • ./scripts/reload.sh --tag fix-node-options-restore-cache-dir --launch passed.

Notes


Note

Medium Risk
Changes how NODE_OPTIONS is built and where preload files live across Claude launch paths; mistakes could break Node startup or weaken cache permissions, but behavior is heavily tested and scoped to restore-module placement.

Overview
Fixes long-running Claude sessions breaking when macOS reaps the NODE_OPTIONS restore preload from temp dirs by writing restore-node-options.cjs to a persistent per-user cache instead of $TMPDIR / NSTemporaryDirectory() / os.TempDir().

Swift, the Resources/bin/claude wrapper, and cmuxd-remote now pick the same cache roots (macOS ~/Library/Caches/com.cmuxterm.app/…, Linux absolute safe XDG_CACHE_HOME or ~/.cache/cmux, else /var/tmp/cmux-<uid>/…), reject paths with whitespace or quotes for unquoted --require=<path>, and harden fallbacks with 0700 dirs, 0600 module files, ownership checks, and symlink rejection. Logic moves into CMUXCLI+ClaudeNodeOptions.swift with shared CMUXCLIShimWriter; a broken $TMPDIR no longer skips injection when the cache path still works.

Regression tests cover cache selection, unsafe env fallbacks, and permission re-hardening on cache hits.

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

Summary by CodeRabbit

  • Chores

    • Restore modules now persist in platform-aware per-user cache locations (macOS Library/Caches, XDG or ~/.cache, with a secure /var/tmp per-UID fallback). Paths containing whitespace/newlines or quotes are rejected; fallbacks enforce ownership, non-symlink checks, and stricter private permissions for guard files and modules.
  • Tests

    • Added/updated tests covering cache-path selection, whitespace/quote handling, relative-XDG behavior, fallback to safe per-user locations, and macOS/Linux scenarios.

Summary by cubic

Moves the Claude NODE_OPTIONS restore preload from temp dirs to a persistent, per-user cache with whitespace‑safe paths and strict permissions across Swift, bash, and Go to stop long‑running session crashes. Aligns fallback validation across launchers with owner checks and symlink rejection before chmod.

  • Bug Fixes
    • Cache roots: macOS ~/Library/Caches/com.cmuxterm.app/cmux-claude-node-options; Linux uses absolute, safe XDG_CACHE_HOME/cmux/…, else ~/.cache/cmux/…; empty/unsafe HOME/XDG fall back to per‑UID /var/tmp/cmux-<uid>/… (Swift may use the system caches root if HOME is unsafe).
    • Safety: document and enforce that --require=<path> must be free of whitespace/quotes; validate fallback base and directory chain ownership; deny symlinks; create dirs 0700 and module 0600 from the start; re‑apply modes on cache hits; guard heredoc writes; owner checks support GNU/BSD stat; avoid $TMPDIR.
    • Consistency: restore module bytes are identical across Swift/bash/Go with one trailing newline; injected --require=<path> is always whitespace‑free; helpers moved to CMUXCLI+ClaudeNodeOptions.swift; shim writers accept explicit modes; tests cover path selection, fallbacks, and permission re‑hardening.

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

Review in cubic

ejjcc and others added 4 commits April 29, 2026 11:54
The restore-node-options.cjs guard module is written under
${TMPDIR}/cmux-claude-node-options/ in three call sites (the bash
wrapper, the Swift main app, and the cmuxd-remote daemon). On macOS
the launchd job com.apple.periodic-daily reaps /var/folders/.../T/
after ~3 days of no access, but NODE_OPTIONS=--require=<that path>
lives for the entire Claude session. Once the file is reaped, every
node child process spawned by Claude hooks crashes with
MODULE_NOT_FOUND, breaking long-running sessions.

The previous attempt at a fix (#2983) moved the guard to
~/Library/Application Support/cmux/node-options/, but Greptile flagged
that path contains a space, and node splits NODE_OPTIONS on whitespace,
so the --require=<path> flag is truncated at the first space and
fails immediately on every macOS launch.

This change picks a no-whitespace cache path on each platform:

  - $XDG_CACHE_HOME/cmux/cmux-claude-node-options/  (when set)
  - ~/Library/Caches/com.cmuxterm.app/cmux-claude-node-options/  (macOS)
  - ~/.cache/cmux/cmux-claude-node-options/  (Linux fallback)

Library/Caches is the Apple-recommended location for regenerable
runtime data and is not periodically swept. The Go daemon uses
os.UserCacheDir() with a TempDir fallback. The bash wrapper's cache
selection respects XDG_CACHE_HOME first so callers (including tests)
can override the path without setting HOME.

Existing snapshot-resume tests in cmuxTests/SessionPersistenceTests.swift
intentionally still hardcode /tmp/... paths -- they verify that legacy
on-disk session snapshots get their stale --require=/tmp/... flags
stripped on resume, which remains correct.
…etic tests

- daemon/remote/cmd/cmuxd-remote/agent_launch.go: on Darwin, use
  com.cmuxterm.app/cmux-claude-node-options to match the bash wrapper
  and Swift launcher so all three share one cache tree (Greptile P2,
  CodeRabbit nit).

- Resources/bin/claude: refuse to use XDG_CACHE_HOME if it contains
  whitespace, falling back to the OS default. Otherwise a user's
  XDG_CACHE_HOME with a space would re-trigger the same node
  split-on-whitespace failure that sank #2983 (Greptile P2).

- tests/test_claude_wrapper_hooks.py: always pin XDG_CACHE_HOME to a
  per-test path so tests are hermetic instead of touching the host's
  real ~/.cache or ~/Library/Caches when no override is provided
  (CodeRabbit nit).
Merged current origin/main into the PR branch and resolved the wrapper test conflict by keeping both the XDG-cache override coverage and the hook-disable coverage. The restore-module cache path is now an explicit cross-launcher contract instead of each launcher independently trusting its platform cache helper.

Constraint: Node splits NODE_OPTIONS on whitespace, so --require paths must be absolute and whitespace-free.

Constraint: macOS launchers must converge on ~/Library/Caches/com.cmuxterm.app/cmux-claude-node-options.

Rejected: Keep honoring XDG_CACHE_HOME on macOS | it diverges from the Swift launcher and makes launch order decide the shim location.

Rejected: Accept relative XDG_CACHE_HOME values | Node preload resolution would become cwd-dependent.

Confidence: high

Scope-risk: narrow

Tested: git diff --check

Tested: ./scripts/reload.sh --tag fix-node-options-restore-cache-dir --launch

Not-tested: Local test suites per repo policy; CI pending after push.

Co-authored-by: OmX <omx@oh-my-codex.dev>
@vercel

vercel Bot commented May 5, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Jun 6, 2026 9:15am
cmux-staging Building Building Preview, Comment Jun 6, 2026 9:15am

@coderabbitai

coderabbitai Bot commented May 5, 2026 •

Copy link
Copy Markdown

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

Moves the Claude restore-node-options.cjs shim from ephemeral temp dirs into a persistent, per-user cache root with OS/env-aware selection, Unicode-whitespace/quote safety checks, /var/tmp per-UID fallback with ownership/symlink validation, and tighter filesystem permissions; updates Swift, Go, shell, and Python test harnesses accordingly.

Changes

Persistent Claude NODE_OPTIONS restore cache

Layer / File(s) Summary
Error & safety helpers
CLI/cmux.swift
Adds ClaudeNodeOptionsCachePathError and helpers that detect unsafe paths for Node --require (Unicode whitespace/newline or quote characters).
Cache root selection logic
CLI/cmux.swift, daemon/remote/cmd/cmuxd-remote/agent_launch.go, Resources/bin/claude
Introduce claudeNodeOptionsRestoreModuleRoot / claudeNodeOptionsCacheDir that pick cache roots by OS/env: macOS → ~/Library/Caches/...; non-darwin → $XDG_CACHE_HOME/cmux if absolute/safe, else ~/.cache/cmux; always append cmux-claude-node-options.
/var/tmp per-UID fallback & validation
CLI/cmux.swift, daemon/remote/cmd/cmuxd-remote/agent_launch.go, Resources/bin/claude
Add per-UID /var/tmp/cmux-<uid> fallback base; ensurePrivateNodeOptionsCacheBase rejects symlink destinations, creates directory with 0700, verifies owner UID matches current user, and optionally appends app scope.
Path safety enforcement
daemon/remote/cmd/cmuxd-remote/agent_launch.go, CLI/cmux.swift, Resources/bin/claude
Reject candidate cache roots that contain Unicode whitespace/newlines or quote characters; if unsafe, fall back and warn/throw via ClaudeNodeOptionsCachePathError or shell-side warnings.
Module writing & permissions
CLI/cmux.swift, daemon/remote/cmd/cmuxd-remote/agent_launch.go, Resources/bin/claude
Write restore-node-options.cjs into computed cache root, ensure directories are 0700, write temp guard with 0600, use cmp/mv semantics and set final module file to 0600.
Implementation imports / helpers
daemon/remote/cmd/cmuxd-remote/agent_launch.go, Resources/bin/claude
Add runtime and unicode (Go); add shell helpers: node_options_fallback_base, node_options_path_is_unsafe, UID/owner checks, and ensure_private_node_options_cache_base.
Tests / Harness
daemon/remote/cmd/cmuxd-remote/tmux_compat_test.go, tests/test_claude_wrapper_hooks.py
Add TestClaudeNodeOptionsCacheDir; extend Python harness to control HOME/XDG_CACHE_HOME, add require_path_from_node_options, assert whitespace-free require paths, and add multiple fallback/regression tests; remove obsolete tmpdir-failure test and update stale mktemp test.

Sequence Diagram(s)

sequenceDiagram
  actor TestHarness
  participant Wrapper
  participant CLI
  participant Daemon
  participant FS as Filesystem
  TestHarness->>Wrapper: start wrapper (HOME/XDG overrides)
  Wrapper->>CLI: request restore-module path
  CLI->>Daemon: compute cache root via claudeNodeOptionsCacheDir()
  Daemon->>FS: validate candidate path (whitespace/quotes, symlink, owner)
  alt safe path
    Daemon->>FS: mkdir (0700), write shim temp (0600), mv -> restore-node-options.cjs (0600)
  else fallback needed
    Daemon->>FS: ensure /var/tmp/cmux-<uid> base (no symlink, owned by uid), mkdir, write shim
  end
  Daemon-->>CLI: return safe require-path
  CLI-->>Wrapper: inject --require=<safe path> into NODE_OPTIONS
  Wrapper->>Wrapper: launch Node with injected --require
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • manaflow-ai/cmux#2238: Modifications touching shim/restore-module setup and shim-writing logic in agent_launch.go.
  • manaflow-ai/cmux#2462: Prior changes around Claude NODE_OPTIONS restore-module placement and injection behavior.

Poem

🐰

I hopped your shim from tmp to cache so neat,
No sneaky spaces where preload hooks meet,
XDG, Library, or a safe /var/tmp seat,
Permissions snug, the preload path is sweet,
Now Node wakes gentle — and my carrots are a treat! 🥕

🚥 Pre-merge checks | ✅ 12 | ❌ 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 (12 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: moving the Claude NODE_OPTIONS restore cache path to a persistent location instead of temporary directories.
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 Swift additions are private utility functions/error type using only value types. No implicit MainActor, Sendable violations, or isolation boundary crossings. Compliant with Swift 6 rules.
Cmux Swift Blocking Runtime ✅ Passed No blocking sync introduced. Swift adds FileManager and path checks only. Go changes safe. time.After in unrelated agent relay is async pattern.
Cmux Swift Concurrency ✅ Passed PR introduces only synchronous Swift functions for cache path handling. No DispatchQueue, Task, Combine, completion handlers, or fire-and-forget patterns added.
Cmux Swift @Concurrent ✅ Passed Swift changes introduce synchronous file I/O functions for cache management. No async functions added, no invalid @concurrent annotations. Complies with rules.
Cmux Swift File And Package Boundaries ✅ Passed +106 lines to CLI/cmux.swift (under budget). NODE_OPTIONS cache feature is cohesive, focused hardening with no mixed UI/state/network/parsing. Allowed as bug fix.
Cmux Swift Logging ✅ Passed No logging violations found. New cache functions in CLI/cmux.swift use no print, debugPrint, dump, or NSLog. Error handling via LocalizedError only, exposing only safe cache paths without secrets.
Cmux Swiftui State Layout ✅ Passed No SwiftUI changes present in PR. All Swift changes are pure utility functions for NODE_OPTIONS cache path management in the non-SwiftUI CMUXCLI struct. Check not applicable.
Cmux Architecture Rethink ✅ Passed Swift functions for cache path selection show no prohibited patterns: no timing/dispatch, mutable state, side channels, or lifecycle issues. Clear owner and invariants for path safety.
Description check ✅ Passed PR description includes Summary, Testing, validation steps, and implementation notes aligned with the template, though Demo Video section is absent.

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

✨ 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/node-options-restore-cache-dir

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

❤️ Share

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

@greptile-apps

greptile-apps Bot commented May 5, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR moves the Claude NODE_OPTIONS restore preload (restore-node-options.cjs) from $TMPDIR to durable per-user cache paths, aligning Swift, bash, and Go on identical location logic and rejecting whitespace/quote characters that would break unquoted --require=<path> injection.

  • Cache roots: macOS uses ~/Library/Caches/com.cmuxterm.app/… (Swift also tries the system caches root if HOME is unsafe); Linux uses an absolute, whitespace-free XDG_CACHE_HOME/cmux/…, then ~/.cache/cmux/…; both fall back to /var/tmp/cmux-<uid>/… with ownership and symlink guards.
  • Hardening: restore module written 0600, cache dirs 0700; modes re-applied on cache hits; CMUXCLIShimWriter centralises atomic temp-write logic with explicit mode parameter; fallback base validated with lstat/owner checks before and after mkdir -p.
  • Tests: nine new Python integration tests cover whitespace/quoted HOME, bad TMPDIR, relative/quoted XDG paths, and cache-hit mode re-hardening; Go unit tests cover path-selection matrix and symlink rejection.

Confidence Score: 5/5

Safe to merge; all three runtimes produce the same cache root, path-safety boundary is enforced at every exit point, and the previous round's findings have been addressed.

The cache-path selection logic is consistent across Swift, bash, and Go: identical fallback hierarchy, same whitespace/quote rejection predicate, and the same final defensive check before interpolating the path into NODE_OPTIONS. Symlink and ownership validation in the fallback path is correct. The shim writer now re-applies the requested mode on every code path including cache hits and the replaceItemAt branch. Test coverage spans the full platform matrix and permission re-hardening. No new logic defects were found beyond what prior review rounds already caught and the author addressed.

No files require special attention at this point; CLI/CMUXCLI+ClaudeNodeOptions.swift and Resources/bin/claude carry the most security-sensitive logic but were reviewed in depth across multiple rounds.

Important Files Changed

Filename Overview
CLI/CMUXCLI+ClaudeNodeOptions.swift New 327-line file: cache-path selection, CMUXCLIShimWriter, fallback hardening for restore module; well-structured, previous-round issues addressed.
CLI/cmux.swift Removes old inline implementations, delegates to CMUXCLIShimWriter and the new extension file; writeShimIfChanged now internal (intentional for delegation).
Resources/bin/claude Replaces TMPDIR-based guard dir with platform-aware cache selection; adds whitespace/quote rejection, symlink checks, and ownership validation in ensure_private_node_options_cache_base.
daemon/remote/cmd/cmuxd-remote/agent_launch.go Go daemon now mirrors Swift/bash cache-path logic: darwin uses Library/Caches, Linux uses XDG or ~/.cache or /var/tmp fallback; writeShimIfChanged extended with explicit mode parameter.
daemon/remote/cmd/cmuxd-remote/tmux_compat_test.go New unit tests for claudeNodeOptionsCacheDir path selection, HOME lookup edge cases, symlink rejection, and writeShimIfChanged mode enforcement; comprehensive coverage.
tests/test_claude_wrapper_hooks.py Adds 9 new integration tests covering whitespace/quoted HOME, bad TMPDIR fallback, XDG rejection, cache-hit rehardening, and fake uname/stat scaffolding; existing test renamed to match new behavior.
cmux.xcodeproj/project.pbxproj Adds CMUXCLI+ClaudeNodeOptions.swift to both the file references group and the Sources build phase; mechanical Xcode bookkeeping only.

Reviews (28): Last reviewed commit: "fix: align node options fallback validat..." | Re-trigger Greptile

Comment thread CLI/cmux.swift Outdated
Comment thread daemon/remote/cmd/cmuxd-remote/agent_launch.go Outdated

@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: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CLI/cmux.swift`:
- Around line 11633-11637: The code currently reads environment["HOME"] into
homePath and builds cacheRoot directly, which causes a hard-fail when HOME
contains whitespace; change the logic so if the resolved homePath contains any
whitespace you deterministically use the system caches directory instead (e.g.
FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first) and
then append "com.cmuxterm.app" to that URL, leaving the original behavior when
HOME is whitespace-free; apply the same fix for the other occurrence referenced
(the block around the symbols assigning cacheRoot at the second site).

In `@Resources/bin/claude`:
- Line 177: The guard that checks for whitespace in the guard_dir variable
currently does a silent "return 1" when whitespace is found; update that check
to print a short diagnostic to stderr (e.g., using printf or echo to >&2) before
returning so callers can see why NODE_OPTIONS injection was skipped; modify the
line with [[ "$guard_dir" != *[[:space:]]* ]] || return 1 to instead emit a
warning like "echo 'warning: guard_dir contains whitespace; skipping
NODE_OPTIONS injection' >&2" and then return 1, referencing the guard_dir
variable in the message for clarity.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: cb546bf1-dcd5-4384-ac4a-d485a0d7b266

📥 Commits

Reviewing files that changed from the base of the PR and between 8f63ca9 and 259e244.

📒 Files selected for processing (5)
  • CLI/cmux.swift
  • Resources/bin/claude
  • daemon/remote/cmd/cmuxd-remote/agent_launch.go
  • daemon/remote/cmd/cmuxd-remote/tmux_compat_test.go
  • tests/test_claude_wrapper_hooks.py

Comment thread Resources/bin/claude Outdated
Reviewer feedback exposed a remaining representable bad state: the preferred persistent cache root could still inherit whitespace from HOME, making Node split the --require path. This keeps the preferred platform cache path when it is safe, but gives all launchers the same deterministic /var/tmp/cmux fallback when the preferred path would be unsafe.

Constraint: NODE_OPTIONS --require paths are split on whitespace by Node and cannot be quoted through this environment variable.

Constraint: Normal macOS homes must still use ~/Library/Caches/com.cmuxterm.app/cmux-claude-node-options.

Rejected: Use FileManager user caches as the fallback for spaced HOME | it resolves under the same spaced home and does not remove the unsafe state.

Confidence: high

Scope-risk: narrow

Directive: Keep Swift, bash, and Go cache-root fallback rules aligned whenever this path contract changes.

Tested: git diff --check

Tested: ./scripts/reload.sh --tag fix-node-options-restore-cache-dir --launch

Not-tested: Local test suites per repo policy; CI will validate pushed branch.

Co-authored-by: OmX <omx@oh-my-codex.dev>
Comment thread tests/test_claude_wrapper_hooks.py Outdated
Comment thread daemon/remote/cmd/cmuxd-remote/agent_launch.go Outdated
Comment thread tests/test_claude_wrapper_hooks.py Outdated
Comment thread daemon/remote/cmd/cmuxd-remote/agent_launch.go Outdated

@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: 8

Caution

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

⚠️ Outside diff range comments (3)
Resources/bin/claude (2)

190-204: 🧹 Nitpick | 🔵 Trivial | 💤 Low value

Mode 0644 on the restore module is correct, but consider 0600 to harden against tampering on the /var/tmp fallback.

restore-node-options.cjs is --require-loaded into the Claude process, so any user that can write to it gains code execution in Claude. On the per-user cache (~/Library/Caches/..., ~/.cache/...) the parent dir already restricts access, so 0644 is fine. On the shared /var/tmp/cmux/... fallback the wider read perms are unnecessary (only the current user invokes claude) and 0600 would shrink the surface a bit. Combine with the /var/tmp ownership concern flagged separately.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Resources/bin/claude` around lines 190 - 204, Change the file-permission bit
used when creating the restore module from 0644 to 0600 to reduce tampering risk
(update the chmod invocation that currently reads chmod 0644 "$temp_path" || {
... } to chmod 0600 "$temp_path" || { ... }); keep the existing failure cleanup
(rm -f "$temp_path") and return behavior unchanged so the temporary
restore-node-options.cjs remains writable/readable only by the creating user,
mitigating the /var/tmp fallback exposure.

188-190: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

/var/tmp/cmux fallback creates unmitigated RCE risk — both shell wrapper and Go daemon lack ownership validation.

Because NODE_OPTIONS=--require=$guard_path causes Node to execute whatever sits at that path, an attacker can pre-create /var/tmp/cmux (world-writable on Linux with sticky bit) owned by themselves before this script runs. Both implementations will then:

  • Succeed silently with mkdir -p (no error if dir already exists, even if owned by another user)
  • Write the restore module to the attacker-controlled directory
  • Have Claude load and execute the attacker's code

Neither the shell wrapper (Resources/bin/claude, lines 188–190) nor the Go daemon (agent_launch.go, claudeNodeOptionsCacheRoot(), lines 354–375) validates directory ownership before using the fallback.

Fix by one of:

  1. Check ownership: refuse the fallback if /var/tmp/cmux exists and is not owned by the current user
  2. Namespace per-user: use /var/tmp/cmux-$(id -u)/... and enforce 0700 permissions
  3. Fail safely: skip the fallback (return error) rather than write to a potentially shared location
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Resources/bin/claude` around lines 188 - 190, The fallback directory usage in
Resources/bin/claude (the mkdir -p "$guard_dir" and temp_path="$(mktemp
"$guard_dir/restore-node-options.cjs.XXXXXX")" logic that writes
restore-node-options.cjs) is unsafe; update the script to either validate
ownership or use a per-user, restricted directory: check if "$guard_dir" exists
and ensure it is owned by the current UID (refuse/return error if not), or
create a per-user path like "$guard_dir-$(id -u)" with mode 0700 before calling
mktemp, and in all cases avoid silently writing to an existing
world-writable/shared directory (i.e., if ownership check fails, return a
non-zero error instead of proceeding to set NODE_OPTIONS).
daemon/remote/cmd/cmuxd-remote/agent_launch.go (1)

318-352: 🧹 Nitpick | 🔵 Trivial | 💤 Low value

Dead error-handling branch and swallowed homeDirErr.

claudeNodeOptionsCacheRoot never returns a non-nil error (every branch returns a path with nil), so the if err != nil block at lines 330–335 is unreachable. The practical consequence is that when os.UserHomeDir() fails, homeDirErr is silently dropped: the function falls through to /var/tmp/cmux, writes the module there, and the user gets no diagnostic about why their per-user cache wasn't used. Either change claudeNodeOptionsCacheRoot to return (string, error) meaningfully (propagating homeDirErr when it had to fall back), or drop the error return entirely and surface homeDirErr here as a warning.

♻️ Proposed simplification
-	homeDir, homeDirErr := os.UserHomeDir()
-	if homeDirErr != nil {
-		homeDir = ""
-	}
-	cacheRoot, err := claudeNodeOptionsCacheRoot(runtime.GOOS, os.Getenv("XDG_CACHE_HOME"), homeDir)
-	if err != nil {
-		if homeDirErr != nil {
-			return "", fmt.Errorf("could not determine home directory for NODE_OPTIONS cache: %w", homeDirErr)
-		}
-		return "", err
-	}
+	homeDir, homeDirErr := os.UserHomeDir()
+	if homeDirErr != nil {
+		homeDir = ""
+		fmt.Fprintf(os.Stderr, "cmux: warning: could not determine home directory for NODE_OPTIONS cache: %v; using /var/tmp fallback\n", homeDirErr)
+	}
+	cacheRoot := claudeNodeOptionsCacheRoot(runtime.GOOS, os.Getenv("XDG_CACHE_HOME"), homeDir)

And drop the error return on claudeNodeOptionsCacheRoot (update test expectations accordingly).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@daemon/remote/cmd/cmuxd-remote/agent_launch.go` around lines 318 - 352, The
function ensureClaudeNodeOptionsRestoreModule currently swallows homeDirErr
because claudeNodeOptionsCacheRoot always returns a nil error; fix by making
claudeNodeOptionsCacheRoot return only (string) (remove the error return) and
then explicitly check homeDirErr in ensureClaudeNodeOptionsRestoreModule: if
homeDirErr != nil return "", fmt.Errorf("could not determine home directory for
NODE_OPTIONS cache: %w", homeDirErr) before proceeding to use cacheRoot; update
any callers/tests of claudeNodeOptionsCacheRoot accordingly so the dead
error-handling branch is removed and the original homeDirErr is surfaced.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CLI/cmux.swift`:
- Around line 11543-11547: ClaudeNodeOptionsCachePathError currently returns a
static errorDescription which hides which path failed; update the
ClaudeNodeOptionsCachePathError struct to accept the offending path (e.g., a
String or URL) and include that path in errorDescription, then update each throw
site (the places that currently throw ClaudeNodeOptionsCachePathError when
validating HOME, XDG_CACHE_HOME, or the /var/tmp fallback) to instantiate
ClaudeNodeOptionsCachePathError with the actual root.path/fallback string so the
error message surfaces the exact rejected path.
- Around line 11673-11683: The fallback cache root in
claudeNodeOptionsFallbackCacheRoot currently uses a shared /var/tmp/cmux path
which allows cross-user race/symlink attacks; change
claudeNodeOptionsFallbackCacheRoot to append a per-user unique directory (e.g.,
based on getuid() or the current user's UID) so the path becomes
/var/tmp/cmux/<uid>/... instead of a global directory, create that per-user
directory with restrictive permissions (mode 0700), and before reusing it verify
it is owned by the current UID and is not a symlink; apply the identical per-UID
directory scheme and ownership/permission checks in the corresponding writers in
Resources/bin/claude and daemon/remote/cmd/cmuxd-remote/agent_launch.go so all
three components agree on the safe path.

In `@daemon/remote/cmd/cmuxd-remote/agent_launch.go`:
- Around line 340-343: The whitespace check for dir duplicates the guard already
performed by claudeNodeOptionsCacheRoot; either remove this unreachable
duplicate (the pathContainsWhitespace check around dir) or move/document the
responsibility to a single place: keep the validation inside
claudeNodeOptionsCacheRoot and delete the extra check here, or retain it but add
a clear comment stating it is defensive redundancy; update references to the dir
variable and the pathContainsWhitespace call in agent_launch.go accordingly so
only one canonical validation exists.
- Around line 354-375: The Linux/non-darwin fallback in
claudeNodeOptionsCacheRoot currently returns "/var/tmp/cmux", which causes the
caller (which appends "cmux" + "cmux-claude-node-options") to produce a doubled
"cmux/cmux" segment; change the non-darwin fallback return to "/var/tmp" (i.e.,
return the parent temp dir) so that subsequent joins yield
"/var/tmp/cmux/cmux-claude-node-options" and align with the bash wrapper and
Swift CLI; keep the existing whitespace checks and use the same helpers
(pathContainsWhitespace) and branch structure, only adjust the final return
value for the non-darwin case.

In `@daemon/remote/cmd/cmuxd-remote/tmux_compat_test.go`:
- Around line 416-464: Add a test case to TestClaudeNodeOptionsCacheRoot that
covers the degenerate input claudeNodeOptionsCacheRoot("linux", "", "") (both
XDG_CACHE_HOME empty and homeDir empty) and assert it returns the fallback
filepath.Join("/var", "tmp", "cmux"); locate the test function
TestClaudeNodeOptionsCacheRoot and append a block creating linuxEmptyRoot, err
:= claudeNodeOptionsCacheRoot("linux", "", "") with the usual err check and a
comparison expecting "/var/tmp/cmux" to lock in current fallback behavior.
- Around line 416-464: The TestClaudeNodeOptionsCacheRoot contains six
sequential checks that should be converted into a table-driven set of subtests
to isolate failures and make output clearer: create a slice of test cases (name,
goos, xdg, home, want, expectError) and iterate with for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) { got, err :=
claudeNodeOptionsCacheRoot(tc.goos, tc.xdg, tc.home); check error vs
tc.expectError; compare got to tc.want with t.Fatalf on mismatch }) }; keep the
existing expected strings and reuse the same claudeNodeOptionsCacheRoot
reference so each scenario (mac, mac fallback, linux XDG, linux fallback, linux
relative fallback, linux unsafe home fallback) is a separate subtest and can be
run/filtered individually.

In `@Resources/bin/claude`:
- Around line 165-186: The final whitespace re-check (the if test matching
"$guard_dir" == *[[:space:]]*) is effectively unreachable because every branch
that sets guard_dir already normalizes or falls back to a whitespace-free path;
add a short comment immediately above that if-statement explaining it's a
defensive sanity check (defense-in-depth) to catch future branches that might
forget whitespace handling, referencing the guard_dir variable and the
"*[[:space:]]*" pattern so readers understand why it exists and that current
branches guarantee no hit.

In `@tests/test_claude_wrapper_hooks.py`:
- Line 771: The test runner calls an undefined function
test_live_socket_whitespace_cache_path_skips_node_options_injection in main();
replace that call with the correct existing test name
test_live_socket_whitespace_home_uses_safe_cache_fallback (or define the missing
function if intended) so main() invokes a defined test; update the invocation in
the main() test list to refer to
test_live_socket_whitespace_home_uses_safe_cache_fallback (or add a new test
function with the expected name) to eliminate the NameError.

---

Outside diff comments:
In `@daemon/remote/cmd/cmuxd-remote/agent_launch.go`:
- Around line 318-352: The function ensureClaudeNodeOptionsRestoreModule
currently swallows homeDirErr because claudeNodeOptionsCacheRoot always returns
a nil error; fix by making claudeNodeOptionsCacheRoot return only (string)
(remove the error return) and then explicitly check homeDirErr in
ensureClaudeNodeOptionsRestoreModule: if homeDirErr != nil return "",
fmt.Errorf("could not determine home directory for NODE_OPTIONS cache: %w",
homeDirErr) before proceeding to use cacheRoot; update any callers/tests of
claudeNodeOptionsCacheRoot accordingly so the dead error-handling branch is
removed and the original homeDirErr is surfaced.

In `@Resources/bin/claude`:
- Around line 190-204: Change the file-permission bit used when creating the
restore module from 0644 to 0600 to reduce tampering risk (update the chmod
invocation that currently reads chmod 0644 "$temp_path" || { ... } to chmod 0600
"$temp_path" || { ... }); keep the existing failure cleanup (rm -f "$temp_path")
and return behavior unchanged so the temporary restore-node-options.cjs remains
writable/readable only by the creating user, mitigating the /var/tmp fallback
exposure.
- Around line 188-190: The fallback directory usage in Resources/bin/claude (the
mkdir -p "$guard_dir" and temp_path="$(mktemp
"$guard_dir/restore-node-options.cjs.XXXXXX")" logic that writes
restore-node-options.cjs) is unsafe; update the script to either validate
ownership or use a per-user, restricted directory: check if "$guard_dir" exists
and ensure it is owned by the current UID (refuse/return error if not), or
create a per-user path like "$guard_dir-$(id -u)" with mode 0700 before calling
mktemp, and in all cases avoid silently writing to an existing
world-writable/shared directory (i.e., if ownership check fails, return a
non-zero error instead of proceeding to set NODE_OPTIONS).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8dabb062-fa0e-4911-b43b-268d9dce8db5

📥 Commits

Reviewing files that changed from the base of the PR and between 259e244 and e02899b.

📒 Files selected for processing (5)
  • CLI/cmux.swift
  • Resources/bin/claude
  • daemon/remote/cmd/cmuxd-remote/agent_launch.go
  • daemon/remote/cmd/cmuxd-remote/tmux_compat_test.go
  • tests/test_claude_wrapper_hooks.py

Comment thread CLI/cmux.swift Outdated
Comment thread CLI/cmux.swift Outdated
Comment thread daemon/remote/cmd/cmuxd-remote/agent_launch.go Outdated
Comment thread daemon/remote/cmd/cmuxd-remote/agent_launch.go Outdated
Comment thread daemon/remote/cmd/cmuxd-remote/tmux_compat_test.go Outdated
Comment thread Resources/bin/claude Outdated
Comment thread tests/test_claude_wrapper_hooks.py Outdated
austinywang and others added 2 commits May 5, 2026 02:01
The whitespace-safe /var/tmp fallback must not become a shared writable code-loading location. This changes the fallback contract to a per-UID private directory, validates ownership before use, fixes the Go full-dir assembly, and wires the renamed wrapper regression into the test runner.

Constraint: NODE_OPTIONS --require executes the restore module in Claude subprocesses, so fallback directories must not be attacker-owned or shared.

Constraint: Swift, bash, and Go must continue to agree on the final restore-module path shape for each platform.

Rejected: Keep /var/tmp/cmux as a global fallback | allows pre-created shared directories to influence a code-loaded path.

Confidence: high

Scope-risk: narrow

Directive: Future fallback changes must preserve per-user ownership checks and avoid root-plus-subdir recomposition drift.

Tested: git diff --check

Tested: ./scripts/reload.sh --tag fix-node-options-restore-cache-dir --launch

Not-tested: Local test suites per repo policy; CI will validate pushed branch.

Co-authored-by: OmX <omx@oh-my-codex.dev>
The per-user fallback refactor used named return values in claudeNodeOptionsCacheDir, so reassignment must use = instead of :=. This keeps the path contract unchanged while restoring the remote daemon test build.

Constraint: Keep the prior security fix intact; this is only a compile correction.

Confidence: high

Scope-risk: narrow

Tested: git diff --check

Tested: ./scripts/reload.sh --tag fix-node-options-restore-cache-dir --launch

Not-tested: Local test suites per repo policy; CI remote-daemon-tests will validate.

Co-authored-by: OmX <omx@oh-my-codex.dev>
@austinywang

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 5, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@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: 4

Caution

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

⚠️ Outside diff range comments (1)
Resources/bin/claude (1)

232-254: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Unhandled cat write failure can silently overwrite the guard module with an empty file.

If the heredoc write fails (e.g., disk-full), cat exits non-zero but there is no error check, so execution continues. chmod 0600 "$temp_path" succeeds on the empty file, cmp -s sees a size mismatch with the existing $guard_path, and mv -f "$temp_path" "$guard_path" replaces the valid module with an empty file. Claude then runs with a broken --require=<path> for the rest of the session.

🐛 Proposed fix
-    cat >"$temp_path" <<'EOF'
+    if ! cat >"$temp_path" <<'EOF'
 const hadOriginalNodeOptions = process.env.CMUX_ORIGINAL_NODE_OPTIONS_PRESENT === "1";
 if (hadOriginalNodeOptions) {
   process.env.NODE_OPTIONS = process.env.CMUX_ORIGINAL_NODE_OPTIONS ?? "";
 } else {
   delete process.env.NODE_OPTIONS;
 }
 delete process.env.CMUX_ORIGINAL_NODE_OPTIONS;
 delete process.env.CMUX_ORIGINAL_NODE_OPTIONS_PRESENT;
 EOF
+    then
+        rm -f "$temp_path"
+        return 1
+    fi
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Resources/bin/claude` around lines 232 - 254, The heredoc write to temp_path
can fail and leave an empty file that gets moved over guard_path; fix this by
checking the exit status of the cat heredoc write (the block that writes to
"$temp_path") and if it fails remove "$temp_path" and return 1 before
continuing; keep the existing cleanup pattern used after chmod (i.e., on failure
rm -f "$temp_path" and return 1) and apply it immediately after the heredoc
write so the subsequent chmod/cmp/mv steps never run on a failed write involving
temp_path.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CLI/cmux.swift`:
- Around line 11645-11649: The code uses environment["HOME"] ??
NSHomeDirectory() which treats an empty HOME as set; change the logic to treat
an empty string as unset so the fallback NSHomeDirectory() is used (e.g., check
environment["HOME"] for nil or empty before using it) and apply the same change
where the same pattern appears (the other branch around the second occurrence).
Update the code that computes homePath and any uses that derive preferredRoot so
they only use environment["HOME"] when it's non-empty.

In `@daemon/remote/cmd/cmuxd-remote/tmux_compat_test.go`:
- Around line 416-491: Add a table case to TestClaudeNodeOptionsCacheDir to
cover the darwin branch when homeDir == "": update the tests slice in
TestClaudeNodeOptionsCacheDir by inserting a case named like "mac empty home
fallback" with goos "darwin", xdgCacheHome "", homeDir "", wantDir
filepath.Join("/var/tmp", "cmux-501", "com.cmuxterm.app",
"cmux-claude-node-options") and wantFallback filepath.Join("/var/tmp",
"cmux-501"); this will explicitly exercise the fallback path in
claudeNodeOptionsCacheDir for mac when no HOME is available.

In `@Resources/bin/claude`:
- Around line 164-166: The ownership check in node_options_path_owner_uid() must
try GNU-format first then BSD-format; change the command to run stat -c '%u'
"$1" 2>/dev/null || stat -f '%u' "$1" 2>/dev/null so that on Linux the -c format
is used and on BSD/macOS the -f fallback runs; ensure stderr is still redirected
to /dev/null for both calls.

In `@tests/test_claude_wrapper_hooks.py`:
- Around line 582-593: The linter flags S108 for the two assertions that compare
require_path to a literal "/var/tmp/cmux-..." even though they are not
file-creation calls; update the two equality-check lines in
tests/test_claude_wrapper_hooks.py (the expect(...) calls that compare
require_path for macOS and non-macOS) to append a noqa suppression comment (#
noqa: S108) to each literal-containing expression so Ruff will ignore the false
positive for those require_path comparisons.

---

Outside diff comments:
In `@Resources/bin/claude`:
- Around line 232-254: The heredoc write to temp_path can fail and leave an
empty file that gets moved over guard_path; fix this by checking the exit status
of the cat heredoc write (the block that writes to "$temp_path") and if it fails
remove "$temp_path" and return 1 before continuing; keep the existing cleanup
pattern used after chmod (i.e., on failure rm -f "$temp_path" and return 1) and
apply it immediately after the heredoc write so the subsequent chmod/cmp/mv
steps never run on a failed write involving temp_path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8c29a0b6-1443-49c0-ae3b-e5ddc9cfd427

📥 Commits

Reviewing files that changed from the base of the PR and between e02899b and c6e9407.

📒 Files selected for processing (5)
  • CLI/cmux.swift
  • Resources/bin/claude
  • daemon/remote/cmd/cmuxd-remote/agent_launch.go
  • daemon/remote/cmd/cmuxd-remote/tmux_compat_test.go
  • tests/test_claude_wrapper_hooks.py

Comment thread CLI/cmux.swift Outdated
Comment thread daemon/remote/cmd/cmuxd-remote/tmux_compat_test.go
Comment thread Resources/bin/claude
Comment thread tests/test_claude_wrapper_hooks.py
The restore preload is injected through NODE_OPTIONS without shell quoting, so cache directory selection must reject every path character that Node treats as option syntax. The fallback owner probe also now tries GNU stat first so Linux fallback ownership checks do not capture failed BSD-probe stdout.

Constraint: NODE_OPTIONS --require paths are assembled as a single unquoted option value.

Rejected: Quote the --require path in NODE_OPTIONS | Node parses NODE_OPTIONS itself and quote handling is the source of this failure class.

Confidence: high

Scope-risk: narrow

Tested: ./scripts/reload.sh --tag fix-node-options-restore-cache-dir; bash -n Resources/bin/claude; python3 -m py_compile tests/test_claude_wrapper_hooks.py; git diff --check

Not-tested: Local regression tests were not executed per repository testing policy; CI should run the Python wrapper and remote daemon coverage.
CodeRabbit caught two follow-up issues on the parser-safe path change: Swift treated an exported empty HOME as a real cache root input, and the Python assertions now intentionally contain /var/tmp literals that Ruff flags as S108.

Constraint: Swift, bash, and Go cache-root selection should agree for empty or unsafe HOME/XDG inputs.

Rejected: Remove the /var/tmp assertions | they lock the per-UID fallback path that prevents shared-directory preload attacks.

Confidence: high

Scope-risk: narrow

Tested: ./scripts/reload.sh --tag fix-node-options-restore-cache-dir; python3 -m py_compile tests/test_claude_wrapper_hooks.py; git diff --check

Not-tested: Local test execution was skipped per repository testing policy; CI will run the relevant lint/test jobs.

@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)
Resources/bin/claude (1)

251-258: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Cached guard_path permissions are not re-tightened to 0600 on the cmp-match path.

When guard_path already exists from a prior install and cmp -s matches, we just rm -f "$temp_path" and return — the existing file keeps whatever mode it had (e.g., a 0644 file written by an earlier version of this wrapper before the 0600 hardening). The Go counterpart in agent_launch.go always runs os.Chmod(restoreModulePath, 0600) after writeShimIfChanged, so bash diverges from Go on cache hits.

🔒 Proposed fix: enforce 0600 on the cached file too
     if [[ -f "$guard_path" ]] && cmp -s "$temp_path" "$guard_path"; then
         rm -f "$temp_path" || return 1
+        chmod 0600 "$guard_path" || return 1
     else
         mv -f "$temp_path" "$guard_path" || {
             rm -f "$temp_path"
             return 1
         }
     fi
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Resources/bin/claude` around lines 251 - 258, When the existing cached file
at guard_path matches temp_path (the cmp -s branch) we currently just rm -f
temp_path and leave guard_path's mode as-is; change that to enforce chmod 0600
on guard_path before removing temp_path and return a non-zero status if the
chmod fails. In other words, in the cmp-match branch (the if [[ -f "$guard_path"
]] && cmp -s "$temp_path" "$guard_path" block) run chmod 0600 "$guard_path" and
check its exit code (return 1 on failure) prior to rm -f "$temp_path", mirroring
the hardening behavior applied after the mv -f path.
♻️ Duplicate comments (2)
CLI/cmux.swift (1)

11652-11652: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Treat HOME="" as unset in both cache-root branches.

Line 11652 and Line 11670 still use environment["HOME"] ?? NSHomeDirectory(). An exported empty HOME slips through here, and URL(fileURLWithPath: "") can resolve relative to the current working directory instead of the user cache. That makes the Swift path selection diverge from the Go/bash implementations this PR is trying to align with.

Suggested fix
-        let homePath = environment["HOME"] ?? NSHomeDirectory()
+        let homePath = environment["HOME"].flatMap { $0.isEmpty ? nil : $0 } ?? NSHomeDirectory()
@@
-            let homePath = environment["HOME"] ?? NSHomeDirectory()
+            let homePath = environment["HOME"].flatMap { $0.isEmpty ? nil : $0 } ?? NSHomeDirectory()

Also applies to: 11670-11670

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CLI/cmux.swift` at line 11652, The code uses environment["HOME"] ??
NSHomeDirectory() which treats an empty HOME as set; change the logic in both
cache-root branches where homePath is computed (the variable named homePath and
the code paths handling cache root selection) to treat an empty string as unset
— i.e., if environment["HOME"] is nil or empty, fall back to NSHomeDirectory();
otherwise use the non-empty environment value to construct the URL so
URL(fileURLWithPath: "") is never created from an empty HOME.
tests/test_claude_wrapper_hooks.py (1)

614-614: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Suppress Ruff S108 false positives on /var/tmp assertion literals.

Line 614, Line 620, and Line 648 are equality assertions (not temp-dir creation), but Ruff still reports S108 and can fail lint/CI.

🔧 Proposed fix
-            require_path == f"/var/tmp/cmux-{os.getuid()}/com.cmuxterm.app/cmux-claude-node-options/restore-node-options.cjs",
+            require_path == f"/var/tmp/cmux-{os.getuid()}/com.cmuxterm.app/cmux-claude-node-options/restore-node-options.cjs",  # noqa: S108
             f"whitespace HOME fallback: expected macOS fallback cache path, got {require_path!r}",
             failures,
         )
@@
-            require_path == f"/var/tmp/cmux-{os.getuid()}/cmux-claude-node-options/restore-node-options.cjs",
+            require_path == f"/var/tmp/cmux-{os.getuid()}/cmux-claude-node-options/restore-node-options.cjs",  # noqa: S108
             f"whitespace HOME fallback: expected Linux fallback cache path, got {require_path!r}",
             failures,
         )
@@
-        require_path == f"/var/tmp/cmux-{os.getuid()}/cmux-claude-node-options/restore-node-options.cjs",
+        require_path == f"/var/tmp/cmux-{os.getuid()}/cmux-claude-node-options/restore-node-options.cjs",  # noqa: S108
         f"quoted HOME fallback: expected Linux fallback cache path, got {require_path!r}",
         failures,
     )

Also applies to: 620-620, 648-648

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_claude_wrapper_hooks.py` at line 614, Ruff is flagging S108 on
literal "/var/tmp/..." equality assertions in the tests even though they are not
creating temp dirs; update the three assertion lines that compare test variables
(e.g., require_path ==
f"/var/tmp/cmux-{os.getuid()}/com.cmuxterm.app/cmux-claude-node-options/restore-node-options.cjs"
and the two similar equality assertions) to append a per-line suppression
comment ("# noqa: S108") so Ruff ignores these false positives while keeping the
assertions intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@Resources/bin/claude`:
- Around line 251-258: When the existing cached file at guard_path matches
temp_path (the cmp -s branch) we currently just rm -f temp_path and leave
guard_path's mode as-is; change that to enforce chmod 0600 on guard_path before
removing temp_path and return a non-zero status if the chmod fails. In other
words, in the cmp-match branch (the if [[ -f "$guard_path" ]] && cmp -s
"$temp_path" "$guard_path" block) run chmod 0600 "$guard_path" and check its
exit code (return 1 on failure) prior to rm -f "$temp_path", mirroring the
hardening behavior applied after the mv -f path.

---

Duplicate comments:
In `@CLI/cmux.swift`:
- Line 11652: The code uses environment["HOME"] ?? NSHomeDirectory() which
treats an empty HOME as set; change the logic in both cache-root branches where
homePath is computed (the variable named homePath and the code paths handling
cache root selection) to treat an empty string as unset — i.e., if
environment["HOME"] is nil or empty, fall back to NSHomeDirectory(); otherwise
use the non-empty environment value to construct the URL so URL(fileURLWithPath:
"") is never created from an empty HOME.

In `@tests/test_claude_wrapper_hooks.py`:
- Line 614: Ruff is flagging S108 on literal "/var/tmp/..." equality assertions
in the tests even though they are not creating temp dirs; update the three
assertion lines that compare test variables (e.g., require_path ==
f"/var/tmp/cmux-{os.getuid()}/com.cmuxterm.app/cmux-claude-node-options/restore-node-options.cjs"
and the two similar equality assertions) to append a per-line suppression
comment ("# noqa: S108") so Ruff ignores these false positives while keeping the
assertions intact.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: d16c39bf-d549-4889-8b85-d6719528a6b2

📥 Commits

Reviewing files that changed from the base of the PR and between c6e9407 and cc2b8f4.

📒 Files selected for processing (5)
  • CLI/cmux.swift
  • Resources/bin/claude
  • daemon/remote/cmd/cmuxd-remote/agent_launch.go
  • daemon/remote/cmd/cmuxd-remote/tmux_compat_test.go
  • tests/test_claude_wrapper_hooks.py

@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: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CLI/cmux.swift`:
- Around line 11543-11548: ClaudeNodeOptionsCachePathError is used for multiple
failure causes but its errorDescription always says "unsafe for --require",
which misleads triage; update the type and its use in
claudeNodeOptionsFallbackCacheRoot/pathIsUnsafeForNodeOptions so the error
message reflects the actual rejection reason (e.g., "unsafe for --require" for
pathIsUnsafeForNodeOptions, "fallback base is a symlink" for symlink check, and
"owned by different UID" for ownership check). Add a small enum or string reason
field to ClaudeNodeOptionsCachePathError (or distinct error cases) and set it at
each call site (claudeNodeOptionsFallbackCacheRoot and
pathIsUnsafeForNodeOptions) so errorDescription returns the correct, specific
message for each failure mode.

In `@tests/test_claude_wrapper_hooks.py`:
- Around line 605-625: Add assertions that the restored shim file and its parent
directories have hardened permissions: use os.stat and stat.S_IMODE on
require_path to assert file mode == 0o600 and on its containing directories
(os.path.dirname(require_path) and its parent) to assert dir mode == 0o700; add
these checks next to the existing expect(...) calls that validate require_path,
runtime_node_options, and child_node_options so failures surface in the same
test, and mirror the same assertions in the other fallback test block around the
640-653 region.
- Around line 210-213: The harness currently always sets env["XDG_CACHE_HOME"],
preventing tests from exercising the "unset" branch; modify the assignment in
the test setup that references env, xdg_cache_home and tmp so that when
xdg_cache_home is a special sentinel value (e.g. a constant UNSET or a specific
string marker) the code leaves env without the "XDG_CACHE_HOME" key, when
xdg_cache_home is None it sets the deterministic tmp / "xdg-cache" path, and
otherwise it sets env["XDG_CACHE_HOME"] to the provided xdg_cache_home value;
update any tests to pass that sentinel to simulate an actually missing
environment variable.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4d75f3ae-b9cc-4d3f-bf3a-25cf43c39914

📥 Commits

Reviewing files that changed from the base of the PR and between cc2b8f4 and f087a5b.

📒 Files selected for processing (2)
  • CLI/cmux.swift
  • tests/test_claude_wrapper_hooks.py

Comment thread CLI/cmux.swift Outdated
Comment thread tests/test_claude_wrapper_hooks.py Outdated
Comment thread tests/test_claude_wrapper_hooks.py
CodeRabbit flagged that the fallback restore-module path still had ambiguous diagnostics, an unchecked heredoc write, and missing permission coverage. This keeps the per-user fallback path while making each rejection reason specific, hardening fallback intermediate directories, and locking the wrapper expectations into executable coverage.

Constraint: PR review feedback requires high and medium findings to be addressed before merge
Constraint: Repo policy forbids local test execution; use syntax checks locally and CI for tests
Rejected: Skip older CodeRabbit summary findings wholesale | still-valid current findings were present in inline medium feedback
Confidence: medium
Scope-risk: moderate
Tested: bash -n Resources/bin/claude; python3 -m py_compile tests/test_claude_wrapper_hooks.py; git diff --check; gofmt on changed Go files
Not-tested: Local test suites per repo policy; pending GitHub/CircleCI checks
Comment thread CLI/cmux.swift Outdated

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

1 issue found across 3 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread CLI/CMUXCLI+ClaudeNodeOptions.swift
Comment thread CLI/CMUXCLI+ClaudeNodeOptions.swift
Comment thread CLI/CMUXCLI+ClaudeNodeOptions.swift Outdated
Comment thread Resources/bin/claude
@austinywang

Copy link
Copy Markdown
Contributor Author

Addressed the Greptile fallback hardening finding in 2be7750: Swift now validates the /var/tmp fallback base with lstat before any chmod, and the bash wrapper now performs the post-mkdir symlink/owner checks before chmod as well. I did not run local tests per handoff instructions; CI is the validation 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 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 32d60d8. Configure here.

Comment thread daemon/remote/cmd/cmuxd-remote/agent_launch.go
Comment thread daemon/remote/cmd/cmuxd-remote/agent_launch.go

This branch was successfully deployed

1 active deployment
Preview – cmux — db7a8777 Deployed Jun 6, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants