fix(mac): resolve a Nix-provided node when spawning the codeburn CLI by sm0keyyy · Pull Request #1263 · getagentseal/codeburn · GitHub
Skip to content

fix(mac): resolve a Nix-provided node when spawning the codeburn CLI - #1263

Merged
iamtoruk merged 1 commit into
getagentseal:mainfrom
sm0keyyy:fix/mac-nix-cli-path
Sep 6, 2026
Merged

fix(mac): resolve a Nix-provided node when spawning the codeburn CLI#1263
iamtoruk merged 1 commit into
getagentseal:mainfrom
sm0keyyy:fix/mac-nix-cli-path

Conversation

@sm0keyyy

@sm0keyyy sm0keyyy commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Summary

On a Nix-managed machine the menubar app renders no data, because every CLI spawn fails with env: node: No such file or directory and exit status 127.

CodeburnCLI.makeProcess runs the CLI through /usr/bin/env with PATH augmented by additionalPathEntries (/opt/homebrew/bin, /usr/local/bin) plus userNodePaths (volta, .npm-global, asdf, mise, nvm). On a nix-darwin machine node exists in none of those. It lives in the per-user profile at /etc/profiles/per-user/$USER/bin. The published npm launcher dist/cli.js begins #!/usr/bin/env node, so env performs a PATH lookup that cannot succeed, and the child dies before any application code runs.

The failure is latent rather than constant, which is why it presents as a sudden break. GUI applications normally receive those directories from launchctl config user path, which is stored in /var/db/com.apple.xpc.launchd/config/user.plist. A macOS system update deletes that file. Afterwards launchctl getenv PATH returns empty, the app inherits the bare /usr/bin:/bin:/usr/sbin:/sbin, and the app is permanently unable to spawn its CLI. Re-applying the launchd setting is not a durable fix, because the next system update removes it again; the application has to know where Nix puts things.

userNodePaths now also names the Nix locations, in this order:

Directory Installed by
/etc/profiles/per-user/$USER/bin nix-darwin, per-user profile
~/.nix-profile/bin nix profile, classic layout
~/.local/state/nix/profiles/profile/bin nix profile, XDG state layout
/run/current-system/sw/bin nix-darwin, system profile
/nix/var/nix/profiles/default/bin multi-user default profile

The username is derived from the injected homeDirectory rather than read from the process environment, so the function stays pure and remains testable with a fixture home.

~/.local/bin joins the same list. That is the bin directory for the common prefix = ~/.local npm configuration, and installedArgv() previously could not discover a CLI installed there. Reaching it depended entirely on the cached codeburn-cli-path.v1; with that cache absent, discovery fell through to the bare name codeburn, which then hit the same unresolvable PATH lookup.

Because userNodePaths feeds both installedArgv() and augmentedPath(), one addition fixes CLI discovery and interpreter resolution together. Entries are appended, so every currently-matching directory keeps its existing precedence and no machine without Nix changes behavior.

Change evidence

Change Why Receipt
Nix profile directories in userNodePaths Let a GUI-launched app resolve a Nix-provided node without depending on launchd state that macOS updates delete. Ran swift test --filter CodeburnCLIPathTests: 4 tests in 1 suite passed, 0 failed, 0.163 s. Test title: "Spotlight-minimal PATH can launch a CLI whose only node is a Nix profile". The fixture puts the only node in $HOME/.nix-profile/bin, points a #!/bin/sh / exec node "$@" wrapper at it to mirror the real shim's PATH-based interpreter lookup, and runs that wrapper through /usr/bin/env on a PATH filtered to /usr/bin, /bin, and the entries production augmentedPath derived under the fixture home.
nix-darwin per-user profile derivation That path embeds the username and cannot be created inside a temporary fixture, so the derived string is asserted directly. Ran swift test --filter CodeburnCLIPathTests: 4 passed, 0 failed, 0.163 s. Test title: "nix-darwin per-user profile is derived from the home directory". It asserts that augmentedPath for homeDirectory: "/Users/test" contains /etc/profiles/per-user/test/bin and /run/current-system/sw/bin.
Regression proof that the new tests fail before the fix Confirm the tests bind to the reported defect rather than restating the implementation. Reverted mac/Sources/CodeBurnMenubar/Security/CodeburnCLI.swift alone via git stash push on that single path, kept both tests, and re-ran the same command: rc 1, "Test run with 4 tests in 1 suite failed after 0.115 seconds with 4 issues". The failure is the reported symptom: Expectation failed: (process.terminationStatus → 127) == 0, together with Expectation failed: (output → "") == ("nix-node-ok\n" ...). Restored with git stash pop.
No regression for the existing PATH integrations Nix entries are appended, so previously matching directories must keep precedence. In that same pre-fix run, "Spotlight-minimal PATH can launch a mise npm-backend CLI" and "custom mise data directory is added once" both passed, so neither depends on the new entries. Both also pass in the 4/4 run above.
~/.local/bin in userNodePaths installedArgv() could not discover a CLI under the common prefix = ~/.local npm configuration. Observed on the affected machine: npm config get prefix returns /Users/<user>/.local, and npm ls -g --depth=0 reports codeburn@0.9.24 under /Users/<user>/.local/lib, while additionalPathEntries + userNodePaths contained no ~/.local/bin. No dedicated test: this half is filesystem probing inside installedArgv(), and the Nix test above already covers the augmentedPath half.
End-to-end behavior of the built app A unit test cannot prove the shipped bundle resolves the interpreter, because the defect only appears in a GUI-inherited environment. Built the universal release, installed it, and launched CodeBurnMenubar.app/Contents/MacOS/CodeBurnMenubar with PATH=/usr/bin:/bin:/usr/sbin:/sbin and no launchd PATH assistance. pgrep -P <app pid> reported 34818 34811 node /Users/<user>/.local/bin/codeburn serve --stdio, ppid matching the app, and the app stayed alive. The pre-fix binary under the same bare PATH stayed alive but spawned no child within 18 s, measured the same way. The exit-127 status itself was captured at the spawn level rather than from that process: /usr/bin/env -- ~/.local/bin/codeburn --version under a GUI-representative PATH of /opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin returned rc 127 with env: node: No such file or directory, and rc 0 with 0.9.24 once the Nix directories were present.
No impact on the TypeScript surface The change set touches only mac/, so the existing suite must stay green. Ran npm test: 272 test files passed, 2 skipped; 3738 tests passed, 5 skipped, 0 failed; duration 34.30 s. Ran npm run build: rc 0.

Environment

Observed and fixed on macOS 27.0 with nix-darwin, Apple Silicon.

Fact Value
which node /etc/profiles/per-user/<user>/bin/node, realpath /nix/store/...-nodejs-slim-24.14.1/bin/node
node under /opt/homebrew/bin or /usr/local/bin absent in both
launchctl getenv PATH empty after the system update
/var/db/com.apple.xpc.launchd/config/user.plist absent after the system update
CLI shim ~/.local/bin/codeburn, first line #!/usr/bin/env node

Notes

The change is confined to one function. It adds no dependency, alters no public interface, and leaves isSafe, baseArgv, installedArgv's ordering semantics, persistedCLIPath, and the CODEBURN_BIN override untouched. Every new entry satisfies the existing safeArgPattern, which excludes shell metacharacters.

A stronger follow-up is deliberately out of scope: persisting the absolute interpreter path beside codeburn-cli-path.v1 and spawning [node, cli.js] would remove the PATH dependency altogether rather than enumerating more locations. This pull request keeps the existing /usr/bin/env design and only teaches it the Nix layout.

The machine-level remedies used to restore the affected workstation, a login LaunchAgent re-applying the GUI PATH and a post-update verification script, are local configuration and are not part of this change.

Companion pull request #1262 repairs mac/Scripts/build-local.sh, which on a current toolchain exits before compiling and omits the SwiftPM resource bundle from the assembled app. There is no file overlap, but that fix is what makes the local build and the end-to-end verification above possible, so #1262 should land first.

Testing

  • I have tested this locally against real data (not just unit tests)
  • npm test passes
  • npm run build succeeds

Real-data check: the rebuilt bundle was installed to ~/Applications/CodeBurnMenubar.app and run against the live local corpus. codeburn status reported Today $9.77 119 calls / Month $149.97 917 calls, and the menu bar rendered a flame icon with a dollar figure, showing no dash, placeholder, or error state. Its codeburn serve --stdio child was confirmed alive by ppid.

Files (2)

  • mac/Sources/CodeBurnMenubar/Security/CodeburnCLI.swift [MODIFIED] (+27 -1)
  • mac/Tests/CodeBurnMenubarTests/CodeburnCLIPathTests.swift [MODIFIED] (+68 -0)

On a nix-darwin machine node lives only in /etc/profiles/per-user/$USER/bin,
which appears in neither additionalPathEntries nor userNodePaths. The CLI shim
begins '#!/usr/bin/env node', so the spawn dies with
'env: node: No such file or directory' and exit status 127.

GUI apps normally inherit those directories from 'launchctl config user path',
stored in /var/db/com.apple.xpc.launchd/config/user.plist, which a macOS system
update deletes. Re-applying it is not durable, so name the Nix locations in
userNodePaths instead. That feeds both installedArgv() discovery and
augmentedPath() interpreter resolution.

Also adds ~/.local/bin, the bin directory for the common 'prefix = ~/.local'
npm configuration, which installedArgv() could not otherwise discover.

Entries are appended, so existing matches keep their precedence.

@iamtoruk iamtoruk left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Verified locally: 4/4 CodeburnCLIPathTests pass on the branch, the two new tests fail with exit 127 against main's CodeburnCLI.swift. One nit for a follow-up, not blocking: ~/.local/bin lands before the asdf/mise/nvm entries, so it is inserted rather than appended as the description says. Merging.

@iamtoruk
iamtoruk merged commit 4a9d885 into getagentseal:main Sep 6, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants