Skip to content

fix: use process.execPath and probe ~/.bun/bin for snap/PATH-limited envs#190

Merged
mksglu merged 4 commits into
mksglu:nextfrom
luispabon:fix/snap-node-bun-path
Mar 28, 2026
Merged

fix: use process.execPath and probe ~/.bun/bin for snap/PATH-limited envs#190
mksglu merged 4 commits into
mksglu:nextfrom
luispabon:fix/snap-node-bun-path

Conversation

@luispabon

@luispabon luispabon commented Mar 27, 2026

Copy link
Copy Markdown
Contributor

tl;dr

This fixes execution on Ubuntu systems where Node is installed as a snap, and also access to bun when the user is using a different shell than bash (fish in my case).

Before

image

After

image

Summary

  • Snap Node.js server test failure: When Node.js is installed via snap, spawning node as a child process re-invokes the snap wrapper, which exits 0 with no output — causing all JS execution (including the doctor server test) to silently fail. Fixed by using process.execPath as the javascript runtime so the actual binary path is passed directly to spawn().
  • Bun not detected in MCP environments: MCP servers inherit a stripped PATH that misses ~/.bun/bin (added by fish/shell profile scripts). Fixed by probing ~/.bun/bin/bun as a fallback in bunExists().

Test plan

  • Run /ctx-doctor with snap Node.js — server test should PASS
  • Run /ctx-doctor with bun installed via its installer (not in system PATH) — bun should be detected and performance warning should clear

🤖 Generated with Claude Code

@luispabon

Copy link
Copy Markdown
Contributor Author

I'll re-check those failures.

luispabon and others added 4 commits March 28, 2026 18:35
…envs

In snap Node.js environments, spawning `node` as a child process invokes
the snap wrapper again which exits 0 with no output, causing the server
test and all JS execution to silently fail. Fix by storing process.execPath
as the javascript runtime so the actual binary is used directly.

Also probe ~/.bun/bin/bun as a fallback when bun is not in PATH, which
happens in MCP server environments that inherit a stripped PATH and miss
the fish/shell profile additions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Keep original "bun or node" test alongside new stricter check
- Add test asserting javascript runtime is never bare "node" string
- Add test for full-path bun runtime using "run" subcommand
- Fix PATH-dependent shell test to use process.execPath instead of "node"

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@luispabon luispabon force-pushed the fix/snap-node-bun-path branch from 3081bd5 to cc2060d Compare March 28, 2026 18:35
@mksglu mksglu changed the base branch from main to next March 28, 2026 18:35
@mksglu

mksglu commented Mar 28, 2026

Copy link
Copy Markdown
Owner

Architecture Review — 3 Specialist Agents

Thanks @luispabon — this is a valuable fix for snap Node.js users and PATH-limited environments. The architecture is sound: process.execPath correctly avoids snap sandbox re-entry, and ~/.bun/bin/bun probing is properly guarded.

Bug Found: getRuntimeSummary() broken with full bun path

getRuntimeSummary() (around line 153 in src/runtime.ts) checks:

runtimes.javascript === "bun"

But after your change, runtimes.javascript can now be a full path like /home/user/.bun/bin/bun. This breaks:

  • The lightning bolt indicator in doctor output
  • The "Performance: FAST (Bun)" message
  • Triggers a false "Install Bun for 3-5x faster execution" tip

Fix:

- const bunPreferred = runtimes.javascript === "bun";
+ const bunPreferred = runtimes.javascript?.endsWith("bun") ?? false;

This matches how buildCommand() was already updated to handle full paths.

Duplicate Test

The first two tests in the "Runtime Detection" describe block appear near-identical. One should be removed.

Security: APPROVE

process.execPath is OS-controlled (not user input) — strictly better than bare "node" which is PATH-hijackable. ~/.bun/bin probe is same-user scope. Net security improvement.

Please push the getRuntimeSummary() fix and remove the duplicate test. I'll merge after.

@mksglu

mksglu commented Mar 28, 2026

Copy link
Copy Markdown
Owner

Additional Finding: Electron process.execPath Blocker

The Adaptors Architect found a critical issue I missed in the first review:

On Cursor, VS Code Copilot, and Windsurf, process.execPath returns the Electron binary (e.g., /path/to/Cursor.app/.../electron), not Node.js. Using it to spawn JS files will fail on these platforms.

Fix: Guard with an Electron check:

const jsRuntime = process.versions.electron
  ? "node"  // Electron: fall back to PATH-resolved node
  : process.execPath;  // Native Node.js: use actual binary

This is the highest priority fix — it would break context-mode on every Electron-based platform.

Also noted:

  • bunExists() and bunCommand() both call commandExists("bun") independently — merge into single resolveBun()
  • Bundle files (cli.bundle.mjs, server.bundle.mjs) should be removed from the PR — CI auto-generates them

@mksglu mksglu merged commit f69b0d2 into mksglu:next Mar 28, 2026
5 checks passed
mksglu added a commit that referenced this pull request Mar 28, 2026
PR #183: Restrict sessionKey regex to [a-zA-Z0-9_-]+ and add
startsWith containment check to prevent path traversal in
writeRoutingInstructions workspace derivation.

PR #190: Fix getRuntimeSummary() bun detection — use endsWith("bun")
instead of === "bun" to handle full paths like ~/.bun/bin/bun.

PR #192: Remove trailing whitespace from AGENTS.md blank separator lines.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@mksglu

mksglu commented Mar 28, 2026

Copy link
Copy Markdown
Owner

Merged and post-merge fix applied: 2b0a8c2

Fixed the getRuntimeSummary() bug:

- const bunPreferred = runtimes.javascript === "bun";
+ const bunPreferred = runtimes.javascript?.endsWith("bun") ?? false;

This ensures the lightning bolt indicator and "Performance: FAST (Bun)" message work correctly when bun is detected via ~/.bun/bin/bun full path.

Note: I reviewed the Electron process.execPath concern raised during review — it's a false positive. Context-mode always runs as a separate Node.js process (spawned by the platform), never inside Electron. process.execPath is always the real Node.js binary in our context.

Once the next version ships, can you test on your Ubuntu snap setup? Both the snap Node.js fix and bun detection should work correctly now.

Thanks for the valuable fix @luispabon.

@luispabon luispabon deleted the fix/snap-node-bun-path branch March 28, 2026 19:09
@luispabon

Copy link
Copy Markdown
Contributor Author

Thank you @mksglu

mksglu added a commit that referenced this pull request May 31, 2026
#731)

ctx_execute(language: "javascript") failed when context-mode runs
in-process inside a bun-compiled self-contained binary host
(OpenCode opencode.exe / Kilo). PolyglotExecutor spawned the host
binary itself with a script.js argument, triggering yargs's
"Failed to change directory" error and a silent exit 0 with no
stdout.

Root cause: detectRuntimes() returned `bun ?? process.execPath` for
the javascript runtime (src/runtime.ts:253). PR #190 (f69b0d2)
introduced the execPath default to avoid the snap-Node wrapper
re-invocation problem, but assumed execPath always points at a JS
runtime — false for in-process plugin hosts whose execPath is the
host binary itself.

Fix: extract resolveJavascriptRuntime() helper that gates execPath
on the existing JS_RUNTIMES allowlist (src/adapters/types.ts:421 —
the same single-source-of-truth set PR #708 introduced for
buildNodeCommand). When execPath basename is not in {node, bun,
deno}, fall back to commandExists("node") ? "node" : null. The
allowlist is cross-OS by design — strips optional .exe and splits
on both separators, so the bug reproduces and fixes identically on
macOS, Linux, and Windows (the reporter only saw the Windows shape
but the cross-OS check is correct).

Preserves PR #190 snap-Node behavior: snap wrapper's binary is
literally named `node` → in JS_RUNTIMES → execPath returned
verbatim. Covered by regression test "snap-node host".

When neither bun nor PATH node is reachable, runtimes.javascript
is null. RuntimeMap.javascript is widened to string | null;
getRuntimeSummary surfaces an actionable "not available" line
instead of literal "null", and buildCommand throws a clear error
before spawning the wrong binary. server.ts:3895 jsRuntime
forwarding is null-guarded (downstream buildNodeCommand already
defaults to "node" when jsRuntime is omitted).

Tests added (tests/runtime.test.ts):
- Windows opencode.exe host falls back to PATH node
- POSIX opencode binary host falls back to PATH node (cross-OS)
- null when host is non-JS and node missing
- snap-node host preserves execPath (regression for #190)
- bun host preserves execPath
- doctor summary surfaces actionable error when javascript is null

Archaeology:
- f69b0d2 (#190) — introduced bun ?? process.execPath default
- PR #708 — added JS_RUNTIMES + IN_PROCESS_PLUGIN_PLATFORMS
mksglu pushed a commit that referenced this pull request Jun 9, 2026
…brew Cellar ENOENT (#800) (#803)

* fix(runtime): add liveness guard to resolveJavascriptRuntime for Homebrew Cellar ENOENT (#800)

When process.execPath is a Homebrew Cellar path
(/opt/homebrew/Cellar/node/VERSION/bin/node), brew upgrade
plus brew cleanup deletes the old Cellar directory, leaving the
in-process execPath dangling.  The liveness guard falls back to
PATH-resolved node when the execPath no longer exists on disk.

Preserves the snap-node fix (#190): the snap wrapper binary
literally exists at its execPath, so the guard passes and the
path is returned verbatim (not collapsed to bare node).

- Add existsSync(execPath) check before returning execPath
- Existing snap-node + bun regression tests updated to mock
  existsSync returning true for their respective exec paths
- Two new tests covering:
  1. Stale Cellar path falls back to PATH node
  2. Stale Cellar + no PATH node returns null

* fix(test): cross-platform bun path mock in #800 Cellar ENOENT test

The existsSync mock only blocked Unix-style bun paths
(/.bun/bin/bun), but on Windows bunFallbackPaths returns
paths with backslashes (C:\Users\...\.bun\bin\bun.exe).
This caused bunExists() to find a real bun on the Windows CI
runner, making resolveJavascriptRuntime return the bun path
instead of 'node'.

Replace the single-platform .includes() check with a
cross-platform regex that matches both file separators
and the optional dot prefix on ~/.bun.
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