Skip to content

Fix #3463: store NODE_OPTIONS guard file under $HOME instead of $TMPDIR - #3699

Closed
psh4607 wants to merge 9 commits into
manaflow-ai:mainfrom
psh4607:fix/3463-node-options-tmpdir
Closed

psh4607 wants to merge 9 commits into
manaflow-ai:mainfrom
psh4607:fix/3463-node-options-tmpdir

Conversation

@psh4607

@psh4607 psh4607 commented May 7, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Store the Claude NODE_OPTIONS restore module under persistent user storage so it survives macOS cleanup of $TMPDIR between login sessions or reboots.

Both current Claude launch entrypoints now prefer $HOME/.claude/cmux/cmux-claude-node-options and fall back to ${TMPDIR:-/tmp} only when HOME is unavailable.

Current implementation

  • Resources/bin/cmux-claude-wrapper — uses the persistent HOME-based guard directory.
  • CLI/cmux.swift — applies the same location in the claude-teams path, keeping both entrypoints consistent.
  • tests/test_claude_wrapper_hooks.py — sandboxes HOME and covers guard placement, invalid HOME, and TMPDIR cleanup.
  • tests/test_cli_claude_teams_env.py — verifies the Swift CLI environment uses the same persistent location and fails open for unusable HOME.

The obsolete Resources/bin/claude path was deleted on main and was not restored during conflict resolution.

Verification

  • Tagged Debug app build succeeded with Zig 0.15.2.
  • Both Python regression files parse successfully.
  • GitHub reports the refreshed head as mergeable.
  • Tests are left to CI per repository policy.

Closes #3463

Copilot AI review requested due to automatic review settings May 7, 2026 11:44
@vercel

vercel Bot commented May 7, 2026

Copy link
Copy Markdown

@psh4607 is attempting to deploy a commit to the Manaflow Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented May 7, 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 wrapper now prefers storing restore-node-options.cjs under $HOME/.claude/cmux when HOME is set, falling back to $TMPDIR otherwise. Tests add a HOME override, validate guard-file placement under HOME, exercise bad-HOME behavior, and verify NODE_OPTIONS survives tmpdir cleanup.

Changes

Node Options Guard File Relocation

Layer / File(s) Summary
Guard Directory Selection Logic
Resources/bin/claude
ensure_node_options_restore_module computes guard_dir using $HOME/.claude/cmux when HOME is set; otherwise defaults to ${TMPDIR:-/tmp}/cmux-claude-node-options.
Test Harness HOME Override
tests/test_claude_wrapper_hooks.py
run_wrapper gains home: str | None = None; resolved sandbox HOME is created and injected into the wrapper subprocess environment.
Test Harness HOME Injection Line
tests/test_claude_wrapper_hooks.py
run_wrapper sets env["HOME"] unconditionally to the resolved sandboxed home so the wrapper subprocess always sees the intended HOME.
Auth Env Helper HOME Sandbox
tests/test_claude_wrapper_hooks.py
run_wrapper_auth_env creates a sandboxed fake HOME under the test temp dir and sets env["HOME"] for its subprocess.
Bad HOME Failure Behavior
tests/test_claude_wrapper_hooks.py
test_live_socket_home_failure_skips_node_options_injection asserts invalid HOME causes NODE_OPTIONS injection to be skipped while wrapper args and cmux ping still occur.
Guard File Location Validation
tests/test_claude_wrapper_hooks.py
test_issue_3463_guard_file_lives_under_home_not_tmpdir verifies the restore-node-options guard file and NODE_OPTIONS --require path are under $HOME/.claude/... and not under the tmpdir guard directory.
TMPDIR Cleanup Resilience
tests/test_claude_wrapper_hooks.py
test_issue_3463_node_options_survives_tmpdir_cleanup runs a Node subprocess after deleting the sandboxed tmpdir to confirm injected NODE_OPTIONS continues to work and does not emit “Cannot find module”.
Test Execution List Update
tests/test_claude_wrapper_hooks.py
main() updated to run the new home-failure and #3463 tests and remove the old tmpdir-failure test.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 12 | ❌ 2

❌ Failed checks (2 warnings)

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.
Description check ⚠️ Warning The description covers summary and verification, but it omits required template sections like Testing, Demo Video, Review Trigger, and Checklist. Add the missing template sections: Testing, Demo Video (or N/A), Review Trigger block, and Checklist items; align headings with the repository template.
✅ Passed checks (12 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR successfully addresses all requirements from issue #3463: moves guard file to $HOME/.claude/cmux, preserves atomic operations and cmp -s fast path, adds regression tests, and maintains fallback to $TMPDIR when HOME is unset.
Out of Scope Changes check ✅ Passed All code changes are directly related to fixing #3463 and improving test coverage. The wrapper logic change, test file updates, and test infrastructure modifications are all in scope.
Cmux Swift Actor Isolation ✅ Passed PR contains no Swift code changes. Custom check for Swift 6 actor isolation is inapplicable. PR modifies only bash script (Resources/bin/claude) and Python tests (tests/test_claude_wrapper_hooks.py).
Cmux Swift Blocking Runtime ✅ Passed This PR contains no Swift code. Changes are only to bash script (Resources/bin/claude) and Python test file. The check explicitly applies to production Swift changes, so it is not applicable.
Cmux No Hacky Sleeps ✅ Passed No hacky sleeps or timing workarounds found. Uses proper synchronization: mkdir -p, mktemp+mv, cmp -s. Tests are deterministic. Structurally fixes root cause by moving guard file to persistent HOME.
Cmux Swift Concurrency ✅ Passed No Swift code is modified in this PR. Changes are only to a Bash script and Python tests. The custom check targets cmux-owned Swift code and is not applicable.
Cmux Swift @Concurrent ✅ Passed PR contains only bash and Python changes (Resources/bin/claude and tests/test_claude_wrapper_hooks.py). No Swift files modified. Custom check for Swift @concurrent annotations not applicable.
Cmux Swift File And Package Boundaries ✅ Passed This PR contains no Swift file changes (only bash script and Python test modifications), so the Swift file and package boundaries check does not apply.
Cmux Swift Logging ✅ Passed No Swift code changes in this PR. Modified files are bash (Resources/bin/claude) and Python test (tests/test_claude_wrapper_hooks.py). Check only applies to production Swift changes.
Cmux Swiftui State Layout ✅ Passed This custom check is designed for SwiftUI changes, but the PR modifies only a bash script and Python test file with no SwiftUI code. The check is not applicable to this PR.
Cmux Architecture Rethink ✅ Passed PR changes only bash and Python, not Swift. The swift-architectural-rethink rule applies to Swift code only. Bash fix is a simple, clear correctness improvement with no architectural violations.
Title check ✅ Passed The title accurately summarizes the main change: moving the NODE_OPTIONS guard file from TMPDIR to HOME.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR stores the Claude NODE_OPTIONS restore module in persistent home-directory storage. The main changes are:

  • Prefer $HOME/.claude/cmux/cmux-claude-node-options in the Swift CLI and shell wrapper.
  • Fall back to temporary storage when HOME is unavailable.
  • Sandbox HOME in the test helpers.
  • Add tests for guard-file placement and survival after TMPDIR cleanup.

Confidence Score: 5/5

This looks safe to merge.

  • The Swift and shell entry points use the same persistent directory layout.
  • The temporary-directory fallback remains available when HOME is unset.
  • Tests cover the reported cleanup failure and the updated failure path.
  • No blocking issues were found in the changed code.

Important Files Changed

Filename Overview
CLI/cmux.swift Moves the Swift CLI restore module to persistent storage under HOME with a temporary-directory fallback.
Resources/bin/cmux-claude-wrapper Moves the shell wrapper restore module under HOME while preserving its existing write and failure behavior.
tests/test_claude_wrapper_hooks.py Sandboxes HOME and tests that the restore module remains usable after TMPDIR is removed.
tests/test_cli_claude_teams_env.py Updates Claude Teams tests for the HOME-backed restore-module path and invalid-HOME behavior.

Reviews (5): Last reviewed commit: "Merge remote-tracking branch 'origin/mai..." | Re-trigger Greptile

Copilot AI 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.

Pull request overview

This PR fixes cmux issue #3463 where the claude wrapper’s NODE_OPTIONS=--require <guard file> can become stale on macOS because the guard file was written under $TMPDIR, which may be cleared between sessions/reboots. The guard file location is moved to a persistent path under $HOME, and regression tests are updated/added to validate the new behavior end-to-end.

Changes:

  • Move the NODE_OPTIONS restore guard directory from ${TMPDIR}/cmux-claude-node-options to ${HOME}/.claude/cmux/cmux-claude-node-options (fallback to ${TMPDIR:-/tmp} only if $HOME is unset).
  • Sandbox HOME by default in the wrapper test harness and update TMPDIR-specific tests to align with the new guard file location.
  • Add two new behavioral regression tests covering guard file placement and survival across simulated $TMPDIR cleanup.

Reviewed changes

Copilot reviewed 1 out of 2 changed files in this pull request and generated 1 comment.

File Description
Resources/bin/claude Writes the NODE_OPTIONS restore guard file under $HOME to avoid macOS $TMPDIR cleanup issues, with fallback when $HOME is unset.
tests/test_claude_wrapper_hooks.py Sandboxes HOME, updates existing tests for the new guard path, and adds #3463 regression tests.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread tests/test_claude_wrapper_hooks.py 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: 1

🤖 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 `@tests/test_claude_wrapper_hooks.py`:
- Around line 716-766: The test only rmtree's tmp_dir /
"cmux-claude-node-options" which is a no-op for the new wrapper; to faithfully
reproduce `#3463` wipe everything under the temporary $TMPDIR after calling
run_wrapper. In the test function
test_issue_3463_node_options_survives_tmpdir_cleanup, replace the single-path
cleanup using guard_dir_tmpdir with logic that iterates tmp_dir.iterdir() and
removes each entry (file or directory) so the entire tmp_dir contents are
deleted before launching the child Node process, ensuring the guard relocation
under $TMPDIR would be caught.
🪄 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: a480bf7c-7631-4d84-856f-d898c2a10956

📥 Commits

Reviewing files that changed from the base of the PR and between 226b7a5 and 042d797.

📒 Files selected for processing (2)
  • Resources/bin/claude
  • tests/test_claude_wrapper_hooks.py

Comment thread 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: 1

🤖 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 `@tests/test_claude_wrapper_hooks.py`:
- Around line 318-323: The test helper sets a sandboxed HOME but then calls
env.update(inherited_env) which can restore the real HOME; filter out "HOME"
from inherited_env before updating (e.g., build inherited_env_no_home = {k: v
for k, v in inherited_env.items() if k != "HOME"}) and call
env.update(inherited_env_no_home); keep the current order so setup_env(tmp) may
intentionally override HOME but prevent inherited_env from undoing the sandbox;
reference sandbox_home, env, setup_env, and inherited_env when making this
change.
🪄 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: 954dad70-5009-4c30-a272-523d1f333a6d

📥 Commits

Reviewing files that changed from the base of the PR and between 042d797 and 61badb6.

📒 Files selected for processing (1)
  • tests/test_claude_wrapper_hooks.py

Comment thread tests/test_claude_wrapper_hooks.py 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.

Caution

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

⚠️ Outside diff range comments (1)
tests/test_claude_wrapper_hooks.py (1)

47-67: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Keep a way to run the wrapper with HOME truly unset.

home=None now means “create a fake home”, so this helper can no longer exercise the production fallback path where HOME is absent and the guard should land under $TMPDIR. An explicit sentinel or unset_home flag would let you add one regression test for that branch instead of leaving it unreachable from this harness.

Also applies to: 184-188

🤖 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` around lines 47 - 67, The helper
currently treats home=None as “create a fake home” which prevents testing the
production fallback where HOME is truly unset; add an explicit flag (e.g.
unset_home: bool = False) to the helper signature and change the resolution
logic so that if unset_home is True you leave HOME unset (set resolved_home to
None / do not set sandbox_home), otherwise preserve the existing behavior (when
home is provided use it, when home is None and unset_home is False create
sandbox_home as before). Update uses of resolved_home/sandbox_home and any tests
(including the branch referenced at lines ~184-188) to pass unset_home=True when
you need to exercise the absent-HOME path. Ensure callers that relied on
home=None keep the current fake-home behavior unless they opt into unset_home.
♻️ Duplicate comments (1)
tests/test_claude_wrapper_hooks.py (1)

318-323: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Don't let inherited_env overwrite the sandboxed HOME.

env.update(inherited_env) can restore the caller's real HOME after the sandbox is set, which breaks hermeticity and can pull real ~/.claude state into these auth-env tests.

💡 Minimal fix
             sandbox_home = tmp / "fake-home"
             sandbox_home.mkdir(parents=True, exist_ok=True)
             env["HOME"] = str(sandbox_home)
             if setup_env is not None:
                 env.update(setup_env(tmp))
-            env.update(inherited_env)
+            env.update({key: value for key, value in inherited_env.items() if key != "HOME"})
🤖 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` around lines 318 - 323, The test
currently sets env["HOME"] = str(sandbox_home) and then does
env.update(inherited_env), which can overwrite the sandboxed HOME; to fix,
ensure the sandbox HOME wins by either applying inherited_env before setting
env["HOME"] or by stripping the HOME key from inherited_env (e.g. pop("HOME",
None)) before env.update; adjust the code around sandbox_home, env, and
setup_env so the sandboxed HOME is preserved when updating env with
inherited_env.
🤖 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 `@tests/test_claude_wrapper_hooks.py`:
- Around line 47-67: The helper currently treats home=None as “create a fake
home” which prevents testing the production fallback where HOME is truly unset;
add an explicit flag (e.g. unset_home: bool = False) to the helper signature and
change the resolution logic so that if unset_home is True you leave HOME unset
(set resolved_home to None / do not set sandbox_home), otherwise preserve the
existing behavior (when home is provided use it, when home is None and
unset_home is False create sandbox_home as before). Update uses of
resolved_home/sandbox_home and any tests (including the branch referenced at
lines ~184-188) to pass unset_home=True when you need to exercise the
absent-HOME path. Ensure callers that relied on home=None keep the current
fake-home behavior unless they opt into unset_home.

---

Duplicate comments:
In `@tests/test_claude_wrapper_hooks.py`:
- Around line 318-323: The test currently sets env["HOME"] = str(sandbox_home)
and then does env.update(inherited_env), which can overwrite the sandboxed HOME;
to fix, ensure the sandbox HOME wins by either applying inherited_env before
setting env["HOME"] or by stripping the HOME key from inherited_env (e.g.
pop("HOME", None)) before env.update; adjust the code around sandbox_home, env,
and setup_env so the sandboxed HOME is preserved when updating env with
inherited_env.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: f2f9a13c-7bbc-425d-b02d-34f589ed6b67

📥 Commits

Reviewing files that changed from the base of the PR and between 61badb6 and 52b4599.

📒 Files selected for processing (1)
  • 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: 1

🤖 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 `@tests/test_claude_wrapper_hooks.py`:
- Around line 706-715: The TMPDIR guard-file check is too narrow: change the
glob call on guard_dir_tmpdir (where stale_under_tmpdir is built) from
glob("restore-node-options.cjs") to a wider pattern (e.g. glob("*.cjs") or
glob("restore-node-options*.cjs")) so the expect(...) assertion will catch both
the final committed file and any mktemp-style temp artifacts; update the
stale_under_tmpdir construction accordingly so expect(not stale_under_tmpdir,
...) still reports leaked files.
🪄 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: 1718f88d-781a-4c01-95f9-e59563aa0294

📥 Commits

Reviewing files that changed from the base of the PR and between 52b4599 and e542129.

📒 Files selected for processing (1)
  • tests/test_claude_wrapper_hooks.py

Comment thread tests/test_claude_wrapper_hooks.py
psh4607

This comment was marked as off-topic.

@psh4607

psh4607 commented Jul 13, 2026 •

Copy link
Copy Markdown
Contributor Author

@lawrencecchen @austinywang Conflicts are resolved and this PR is mergeable. Could you review and merge?

@debedb

debedb commented Jul 14, 2026

Copy link
Copy Markdown

This is the right approach and currently the only conflict-free PR of the several open against #3463 — #3541, #3966, #3526, #3278 are all the same durable-location fix, just rotting on merge conflicts. Consolidating on this one and merging would close the issue.

Two notes for reviewers:

  • The 2 failing checks are just Vercel deploy-authorize (a fork can't trigger a preview deploy), not code failures — the real CI is green.
  • Please confirm this covers the live-session case, not only reboot/relogin. macOS's periodic cleaner reaps $TMPDIR files by access time (~3 days), and the APFS Data volume doesn't update atime on read — so a shim that's --required on every node spawn still looks idle and gets purged mid-session (repro in the issue: a session up continuously for ~5 days lost the file with no reboot). A durable location like this PR fixes that; a launch-time-only recreate would still leave long-lived sessions broken. Worth a quick check that the new path is genuinely outside any GC'd dir.

This has sat unreviewed while the same fix keeps getting reproposed. Could a maintainer review and merge? Happy to help rebase if it drifts.

…R staleness

The claude wrapper writes restore-node-options.cjs under $TMPDIR/cmux-claude-node-options/
and exports NODE_OPTIONS=--require <that path>. macOS clears $TMPDIR (/var/folders/...)
between sessions and reboots, but child Node processes spawned later still inherit
NODE_OPTIONS referencing the now-deleted path, producing 'Cannot find module' errors.

Add two behavioral tests against the real wrapper (not source-text greps):

- test_issue_3463_guard_file_lives_under_home_not_tmpdir: asserts the guard file
  lands under $HOME/.claude/cmux/cmux-claude-node-options/, not under $TMPDIR.
- test_issue_3463_node_options_survives_tmpdir_cleanup: simulates macOS clearing
  $TMPDIR after the wrapper sets NODE_OPTIONS, then spawns a fresh node child
  with that NODE_OPTIONS and asserts no 'Cannot find module' error.

Both tests fail today against the unpatched wrapper, with the 'tmpdir wipe' test
reproducing the exact stack trace the issue reporter saw.

Also sandbox HOME to a fresh temp dir inside run_wrapper by default, so the
upcoming fix (which writes under $HOME) does not pollute the real ~/.claude tree
during test runs.

Refs manaflow-ai#3463
…ad of $TMPDIR

The claude wrapper writes restore-node-options.cjs and exports
NODE_OPTIONS=--require <that path>. macOS clears $TMPDIR (/var/folders/...)
between sessions and reboots, but child Node processes spawned later still
inherit NODE_OPTIONS pointing at the now-deleted file, producing 'Cannot
find module' errors on session stop.

Move guard_dir from $TMPDIR/cmux-claude-node-options to
$HOME/.claude/cmux/cmux-claude-node-options, which is persistent across
login sessions and reboots, and co-located with the existing ~/.claude/
data the wrapper already reads. Keep the existing atomic temp-then-mv
pattern and cmp -s reuse-if-identical fast path.

Fall back to the historical $TMPDIR location only if $HOME is somehow
unset, to keep the wrapper non-fatal in unusual environments.

Update two existing tests that asserted TMPDIR-specific behaviour:
- Rename test_live_socket_tmpdir_failure_skips_node_options_injection to
  test_live_socket_home_failure_skips_node_options_injection and simulate
  HOME pointing at a regular file so mkdir fails (same fail-open intent).
- Move the stale-mktemp-literal test fixture from $TMPDIR to $HOME so it
  still exercises the wrapper's mktemp path under the new layout.

Closes manaflow-ai#3463
run_wrapper already auto-sandboxes $HOME so the wrapper's NODE_OPTIONS
guard file ($HOME/.claude/cmux/cmux-claude-node-options/) cannot land in
the developer's real home dir during test runs. The auth-env helper
missed the same treatment, so the four tests that go through
run_wrapper_auth_env (preserves_third_party_claude_auth_for_fresh_launch,
preserves_claude_auth_for_resume_launch,
preserves_only_listed_claude_auth_keys, and the subrouter-config-dir
normalisation case) executed the real wrapper against the developer's
real HOME and silently materialised that guard file there.

Apply the same default sandbox in run_wrapper_auth_env. setup_env still
overrides HOME for tests that intentionally set up a specific home tree
(e.g. the subrouter normalisation case), because env.update(setup_env)
runs after the default is set.

Refs manaflow-ai#3463
The 'survives_tmpdir_cleanup' test only rmtree'd
$TMPDIR/cmux-claude-node-options before. After the fix the wrapper
never writes that path, so the rmtree is a no-op and the test only
proves 'node spawns successfully with the wrapper's NODE_OPTIONS'.

Wipe the whole sandboxed $TMPDIR instead, mirroring how macOS clears
/var/folders/... between login sessions. If the wrapper ever regresses
to writing the guard file under $TMPDIR, the --require path will be
gone after the wipe and node will fail with the same 'Cannot find
module' the issue reporter saw, catching the regression.

Refs manaflow-ai#3463
The previous commit set HOME to a sandboxed temp dir, but
env.update(inherited_env) immediately afterwards could clobber it back
to the caller's real HOME. None of today's call sites pass HOME through
inherited_env, but a future auth-env regression test that does would
silently re-leak the wrapper's NODE_OPTIONS guard file into the dev's
real ~/.claude/cmux. setup_env stays as the intentional override path
(used by the subrouter normalisation case).

Refs manaflow-ai#3463
The previous glob('restore-node-options.cjs') only catches the final
committed filename. Use glob('*.cjs') so the assertion also catches
any leaked mktemp temp files (restore-node-options.cjs.XXXXXX) that a
partial wrapper regression could leave under $TMPDIR/cmux-claude-node-options/.

The TMPDIR subdir is sandboxed and only the wrapper writes \.cjs files
there, so the wider glob has no false-positive risk.

Refs manaflow-ai#3463
@psh4607
psh4607 force-pushed the fix/3463-node-options-tmpdir branch from 7c5ad9e to a88aa58 Compare August 18, 2026 06:40
@cursor

cursor Bot commented Aug 18, 2026

Copy link
Copy Markdown

Bugbot is paused — on-demand spend limit reached

Bugbot uses usage-based billing for this team and has hit its on-demand spend limit.

A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue.

@psh4607

psh4607 commented Aug 18, 2026 •

Copy link
Copy Markdown
Contributor Author

@debedb Thanks for pointing out the live-session case.

I rebased this PR onto the latest main. The restore module is stored under $HOME/.claude/cmux/..., outside $TMPDIR.

The regression test models the live-session scenario by launching the wrapper, deleting the entire sandboxed $TMPDIR, and then starting a new Node process with the inherited NODE_OPTIONS. This specifically covers mid-session temp cleanup, not only reboot/relogin.

@debedb

debedb commented Aug 22, 2026

Copy link
Copy Markdown

@psh4607 Thanks — that answers it, and it answers it in the right place.

$HOME/.claude/cmux/... is outside every directory the periodic cleaner touches, and the regression test that deletes the entire sandboxed $TMPDIR and then spawns a fresh node with the inherited NODE_OPTIONS is exactly the mid-session repro rather than the reboot/relogin one. That was the failure I hit: a session up continuously for ~5 days lost the shim with no reboot at all, because the cleaner reaps by access time and the APFS Data volume doesn't update atime on read — so a file that is --required on every single node spawn still looks untouched and gets purged underneath a live session. A launch-time-only recreate would not have fixed that; a durable location does.

For maintainers, the state of this one:

@lawrencecchen @austinywang — could one of you take a look? This has now been open across three months and rebased twice while the same fix keeps getting reproposed by different contributors. Happy to help rebase again if it drifts.

debedb added a commit to voitta-ai/skillz that referenced this pull request Sep 22, 2026
…30.0) (#333)

The launchd-env-sync-session-leak branch carried a "Related" pointer to
cmux-node-options-tmpdir-guard that never landed, because the target was
only ever a personal skill under ~/.claude/skills - a shadow copy nobody
else has. Adding the pointer alone would have shipped a dangling reference
in a public repo, so the target comes with it.

cmux launches Claude Code with a --require shim whose .cjs lives under
$TMPDIR. macOS reaps files there once they go untouched for about three
days, which is exactly the window a reboot-surviving cmux session lives
past. The file disappears while NODE_OPTIONS still names it, and from then
on every node/claude invocation in that session dies on a missing --require
target - a failure that reads as a broken toolchain and arrives days after
anything changed.

The skill carries the stopgap for builds predating manaflow-ai/cmux#3699: a
persistent copy under $HOME, and a LaunchAgent that recreates the $TMPDIR
file and refreshes its mtime daily, well inside the reap window. The subtle
part is why the LaunchAgent can find the right file at all - it runs in the
same per-user launchd context as the GUI app, so getconf DARWIN_USER_TEMP_DIR
returns the same per-boot temp base cmux used, and nothing has to be
hardcoded.

The personal copy's closing reference pointed at a memory note with no
public counterpart; it is replaced by a pointer back to
launchd-env-sync-session-leak, which is the same class of failure from the
other direction - a stale value synced into launchd, rather than a guard
file reaped out from under one.

Versions: bundle 1.130.0; launchd-env-sync-session-leak skill 1.1.0 ->
1.2.0, and its standalone plugin 1.0.0 -> 1.2.0, which had been left behind
at 1.0.0 by an earlier skill bump.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
teamleaderleo and others added 2 commits September 25, 2026 07:35
# Conflicts:
#	tests/test_cli_claude_teams_env.py
Per review, keep the manaflow-ai#3463 restore module out of Claude's own ~/.claude tree
and under a cmux-owned directory, matching ~/.cmux/hooks and
~/.cmux/local-tmux.

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

Copy link
Copy Markdown
Contributor


Thank you for your submission, we really appreciate it. Like many open-source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution. You can sign the CLA by just posting a Pull Request Comment same as the below format.


I have read the CLA Document v2.2 and I hereby sign the CLA


1 out of 2 committers have signed the CLA.
✅ teamleaderleo
❌ @psh4607
You can retrigger this bot by commenting recheck in this Pull Request. Posted by the CLA Assistant Lite bot.

@teamleaderleo

Copy link
Copy Markdown
Collaborator

Thanks for working on this. #12022 is now fixed on main by #14814, which moves the Claude NODE_OPTIONS restore preload to ~/.cmuxterm/cmux-claude-node-options/; #14851 does the same for the remote daemon. That covers what this PR set out to fix, so a maintainer will likely close it. If you see a case those two miss, please say so here.

@teamleaderleo

Copy link
Copy Markdown
Collaborator

Closing as superseded by #14814 (and #14851 for the remote daemon). Thanks again.

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.

NODE_OPTIONS --require path in $TMPDIR becomes stale after macOS temp cleanup

4 participants