Skip to content

browse: compiled binary's Bun.spawn PATH lookup fails on macOS even when bun is on PATH #931

@austinhirsch

Description

@austinhirsch

Summary

On macOS Apple Silicon, ~/.claude/skills/gstack/browse/dist/browse fails on every invocation with:

[browse] Starting server...
[browse] Executable not found in $PATH: "bun"

…even though bun is installed and reachable from the shell. The bare Bun.spawn(['bun', 'run', SERVER_SCRIPT]) call at browse/src/cli.ts:243 cannot find bun from inside the compiled binary, but the same Bun.spawnSync(['bun', '--version']) call from the bun runtime in the same shell environment works fine. The bug is specific to bun build --compile'd binaries, not the bun runtime.

Environment

  • macOS Apple Silicon (arm64)
  • bun 1.3.10 (also reproduced on 1.3.11)
  • gstack: tested on a7593d70 (HEAD when filed) and 1652f22 — same symptom on both
  • bun installed via official installer at ~/.bun/bin/bun, also copied to /usr/local/bin/bun (root-owned, mode 755) which IS in the launchd-default PATH
  • No Homebrew (/opt/homebrew does not exist)

The isolating test

This is the key reproduction. With identical minimal env:

$ env -i HOME=$HOME PATH="$HOME/.bun/bin:/usr/local/bin:/usr/bin:/bin" \
    ~/.bun/bin/bun -e "
      console.log('PATH inside bun:', process.env.PATH);
      const r = Bun.spawnSync(['bun', '--version']);
      console.log('bun spawn:', r.exitCode, r.stdout?.toString());
    "
PATH inside bun: /Users/x/.bun/bin:/usr/local/bin:/usr/bin:/bin
bun spawn: 0 1.3.10

✅ Bun runtime: Bun.spawnSync(['bun', '--version']) succeeds, returns 1.3.10.

$ env -i HOME=$HOME PATH="$HOME/.bun/bin:/usr/local/bin:/usr/bin:/bin" \
    ~/.claude/skills/gstack/browse/dist/browse status
[browse] Starting server...
[browse] Executable not found in $PATH: "bun"

❌ Compiled browse binary: same Bun.spawn(['bun', ...]) fails to find bun with the exact same PATH.

So the issue is not "bun is not on PATH" — it's that the compiled binary's PATH-lookup primitive is broken in a way that the bun runtime's is not.

Things that don't fix it

Tried, none work:

  1. Default shell PATH with ~/.bun/bin first
  2. Inline PATH="$HOME/.bun/bin:$PATH" override
  3. Symlink ~/.bun/bin/bun~/.local/bin/bun (which IS in $PATH)
  4. launchctl setenv PATH "$HOME/.bun/bin:..." (user-level launchd PATH)
  5. env -i HOME=$HOME PATH="$HOME/.bun/bin:..." browse status
  6. BUN_INSTALL="$HOME/.bun" PATH="$HOME/.bun/bin:$PATH" browse status
  7. cp ~/.bun/bin/bun /usr/local/bin/bun (real file, root-owned, mode 755, in launchd-default PATH)
  8. Downgrading bun from 1.3.11 to 1.3.10
  9. Checking out gstack commit 1652f22 (older commit known to work elsewhere)
  10. Clearing com.apple.quarantine xattr on browse + find-browse + bun
  11. Full clean reinstall (rm -rf ~/.claude/skills/gstack && git clone … && setup)

Side issue: build-node-server.sh fails on bun ≥ 1.3.10

During gstack/setup, this prints:

Building Node-compatible server bundle...
error: cannot write multiple output files without an output directory

…from browse/scripts/build-node-server.sh's bun build --target=node --outfile … call. Looks like --outfile semantics for --target=node changed in recent bun versions and now require --outdir when the build produces multiple chunks. Setup continues past the error and the fallback server-node.mjs is never written. On the gstack 1652f22 commit this error does NOT appear and server-node.mjs is built correctly, so something between 1652f22 and HEAD also pushed the build-script over the edge — but this is a separate issue from the spawn bug above.

Workaround that fixes it for me

Patched browse/src/cli.ts:243 to use an absolute path to bun resolved at runtime, skipping PATH lookup entirely:

-    proc = Bun.spawn(['bun', 'run', SERVER_SCRIPT], {
+    // Use absolute path to bun. Bun.spawn's PATH lookup is broken in
+    // bun build --compile'd binaries on some macOS machines (see issue link).
+    const bunPath = (process.env.BUN_INSTALL ? `${process.env.BUN_INSTALL}/bin/bun` : null)
+      || (fs.existsSync('/usr/local/bin/bun') ? '/usr/local/bin/bun' : null)
+      || (process.env.HOME ? `${process.env.HOME}/.bun/bin/bun` : null)
+      || 'bun';
+    proc = Bun.spawn([bunPath, 'run', SERVER_SCRIPT], {
       stdio: ['ignore', 'pipe', 'pipe'],
       env: { ...process.env, BROWSE_STATE_FILE: config.stateFile, ...extraEnv },
     });
     proc.unref();

Recompile, retest:

$ ~/.claude/skills/gstack/browse/dist/browse status
[browse] Starting server...
Status: healthy
Mode: launched
URL: about:blank
Tabs: 1
PID: 21867

browse goto, browse snapshot, etc. all work end-to-end after this patch.

What might be the underlying bun bug

I am not equipped to fix this in bun itself, but the symptoms suggest:

  • bun build --compile produces a single-file executable
  • Inside that executable, Bun.spawn's PATH-lookup primitive (posix_spawnp / execvp / Bun's own which-equivalent) does not see process.env.PATH correctly, OR uses a hardcoded fallback that doesn't include ~/.bun/bin or /usr/local/bin
  • This is specific to --compile'd binaries because the same Bun.spawn(['bun', ...]) call from a non-compiled bun -e "…" script in the same env works

Could be a --compile flag baking in compile-time process.env instead of using runtime env, or a Bun.spawn code path that diverges between embedded vs runtime mode. I'd be happy to test additional repro variants if a Bun maintainer wants to chase it.

What I'd suggest gstack do regardless of the bun fix

The absolute-path workaround above is bulletproof — even on a working machine, it removes a moving part (PATH lookup) for no real cost. If BUN_INSTALL is set (the normal case for bun-installed-via-installer machines), use ${BUN_INSTALL}/bin/bun directly. If not, fall through to /usr/local/bin/bun, then ~/.bun/bin/bun, then bare 'bun' as the last resort. Same approach for find-browse and any other compiled binary that spawns bun by name.

Happy to send a PR if you'd like.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions