Skip to content

windows: the installed axiomcode command fails in PowerShell and cmd ("bash.exe" is not recognized), because npm's shim runs bash from PATH #1229

Description

@Whua689

What

On Windows, the command that npm link or npm i -g @axiomcode/code-graph installs does not run from PowerShell or cmd:

PS> axiomcode --help
& : The term 'bash.exe' is not recognized as the name of a cmdlet, function, script file, or operable program.
At C:\Users\<user>\AppData\Roaming\npm\axiomcode.ps1:24 char:7
+     & "bash$exe"  "$basedir/node_modules/@axiomcode/code-graph/bin/axiomcode" ...

The same command works from Git Bash. Verified at f60e4d4f on Windows 11, Node 24.11.1, npm 11.6.2, with a default Git for Windows install.

Why

bin.axiomcode points at bin/axiomcode, a bash script. npm generates the Windows axiomcode.cmd and axiomcode.ps1 wrappers from the script's shebang, so both run a bare bash.exe looked up on PATH. A default Git for Windows install adds Git\cmd to PATH but not Git\bin, so no bash is found.

Adding Git\bin to PATH is not a durable workaround. Once WSL is installed, C:\Windows\System32\bash.exe is WSL's bash, and it sits on the machine PATH ahead of anything in the user PATH. The wrapper would then run this Windows install inside Linux, where neither the Windows node nor a C:/ path exists. That part is inferred and not reproduced, because WSL wasn't installed on the test machine.

It also affects every agent that starts the MCP server on Windows with npx -y @axiomcode/code-graph mcp, since npx goes through the same wrapper.

Generic repro

  1. On Windows, with Git for Windows installed with default options: clone, then npm install && npm link.
  2. Open PowerShell or cmd and run axiomcode --help.

Proposed fix (tested locally)

No separate cmd/ or Windows folder is needed, because npm writes the Windows wrappers itself. The fix is to point bin at a small Node launcher, bin/axiomcode.js. npm then generates wrappers that run node, and the launcher picks the bash:

  • $AXIOMCODE_BASH if set; a set-but-missing path is an error.
  • On Windows: Git's own bin\bash.exe, found from git --exec-path, then %ProgramFiles%, %ProgramFiles(x86)% and %LOCALAPPDATA%\Programs \Git\bin\bash.exe. PATH is never searched, so WSL's bash is never used. Git\bin\bash.exe is the wrapper that puts mingw64/bin and usr/bin on PATH; usr\bin\bash.exe alone does not.
  • Elsewhere: plain bash, as before.
  • If none is found, exit 127 with a message naming Git for Windows and AXIOMCODE_BASH.
  • Pass the exit status and any signal through.

bin/axiomcode stays unchanged, since every test suite and doc calls it directly.

Two related changes were needed:

  • .gitignore:131: [Bb]in/ (added for the C# dotnet build output) also ignores the root bin/. A new file there is silently left out of a commit, which is presumably why bin/axiomcode had to be force-added. A !bin/axiomcode.js exception cannot work, because git won't re-include a file under an excluded directory, so the fix is !/bin/ directly after it. graph/test/csharp/**/bin/ stays ignored (checked with git check-ignore).
  • graph/test/tools/package-contents-test.sh: also assert that bin/axiomcode.js ships.

Results on the test machine with the patch applied and npm link re-run:

where axiomcode --help exit code passthrough parse run (Java case 01) axiomcode mcp initialize + tools/list
PowerShell ✅ exit 0 ✅ bad arg → 2 ✅ IR written, then the expected no engine for java@… (#454) ✅
cmd ✅ exit 0 ✅ — ✅
Git Bash ✅ exit 0 ✅ ✅ ✅

AXIOMCODE_BASH=C:\nope\bash.exe → ❌ AXIOMCODE_BASH is set to C:\nope\bash.exe, which does not exist., exit 127. npm pack --dry-run ships both bin/axiomcode and bin/axiomcode.js. package-contents-test passes, 23 of 23 checks (needs the separate test fix filed alongside this issue).

Not verified: macOS and Linux. The POSIX path is spawnSync('bash', [script, …]), and __dirname resolves through the .bin symlink, so #886's root walk should be unaffected, but that is untested here.

Patch (against f60e4d4)
diff --git a/.gitignore b/.gitignore
index d53e24ef..f70e4558 100644
--- a/.gitignore
+++ b/.gitignore
@@ -129,6 +129,9 @@ oracles/
 # four binaries into the tree.
 [Oo]bj/
 [Bb]in/
+# ...except the repository's own bin/, which is the CLI and not build output: without this
+# every new file there, bin/axiomcode.js included, is silently left out of a commit.
+!/bin/
 *.user
 
 reports/
diff --git a/graph/test/tools/package-contents-test.sh b/graph/test/tools/package-contents-test.sh
index 68f18ac7..2937fa3e 100755
--- a/graph/test/tools/package-contents-test.sh
+++ b/graph/test/tools/package-contents-test.sh
@@ -56,12 +56,13 @@ try{
 let files;
 try{
   files=JSON.parse(execFileSync("npm",["pack","--dry-run","--json","--ignore-scripts"],
-    {cwd:root, encoding:"utf8", stdio:["ignore","pipe","ignore"], maxBuffer:64*1024*1024}))[0].files.map(f=>f.path);
+    {cwd:root, encoding:"utf8", stdio:["ignore","pipe","ignore"], maxBuffer:64*1024*1024, shell:process.platform==="win32"}))[0].files.map(f=>f.path);
 }catch(e){ console.log("  FAIL  npm pack --dry-run failed: "+e.message.split("\n")[0]); process.exit(1); }
 const has=(p)=>files.includes(p);
 const count=(re)=>files.filter(f=>re.test(f)).length;
 
 has("bin/axiomcode") ? ok("ships bin/axiomcode") : bad("does NOT ship bin/axiomcode");
+has("bin/axiomcode.js") ? ok("ships bin/axiomcode.js") : bad("does NOT ship bin/axiomcode.js, the command npm links");
 // The corpora are the bulk of the repository and run nothing for a consumer.
 count(/^graph\/test\//)===0 ? ok("does not ship graph/test") : bad("ships "+count(/^graph\/test\//)+" files under graph/test");
 count(/^parser\/src\/test-data\//)===0 ? ok("does not ship parser test-data") : bad("ships "+count(/^parser\/src\/test-data\//)+" files under parser/src/test-data");
diff --git a/package.json b/package.json
index bfb73b7a..0d59e6d3 100644
--- a/package.json
+++ b/package.json
@@ -36,7 +36,7 @@
     "@axiomcode/engine-win32-x64": "0.1.0"
   },
   "bin": {
-    "axiomcode": "bin/axiomcode"
+    "axiomcode": "bin/axiomcode.js"
   },
   "files": [
     "bin/",
diff --git a/bin/axiomcode.js b/bin/axiomcode.js
new file mode 100644
index 00000000..2ebd2358
--- /dev/null
+++ b/bin/axiomcode.js
@@ -0,0 +1,65 @@
+#!/usr/bin/env node
+// ─────────────────────────────────────────────────────────────────────────────
+// The installed `axiomcode` command. It only finds a bash and hands everything to
+// bin/axiomcode, which is the CLI.
+//
+// WHY A NODE LAUNCHER. npm writes the command's wrappers itself from the `bin` entry: a
+// symlink on POSIX, and on Windows an axiomcode.cmd and an axiomcode.ps1 that run whatever
+// the script's shebang names. For bin/axiomcode that is `bash`, so both wrappers run a bare
+// `bash.exe` from PATH -- and a default Git for Windows install puts Git\cmd on PATH, not
+// Git\bin, so PowerShell and cmd fail with "'bash.exe' is not recognized". Adding Git\bin to
+// PATH is not a fix either: once WSL is installed, C:\Windows\System32\bash.exe comes first on
+// the machine PATH, and it is WSL's bash, which runs this Windows install inside Linux where
+// neither the Windows node nor a C:/ path exists. A Node shebang makes npm's wrappers run
+// node, which is always there, and the choice of bash is made here instead of by PATH.
+//
+// On Windows, in order: $AXIOMCODE_BASH, Git's own bash located from `git --exec-path`, then
+// the default install locations. PATH is never searched for bash, so WSL's is never taken.
+// Everywhere else it is plain `bash`, as the shebang of bin/axiomcode already assumes.
+// ─────────────────────────────────────────────────────────────────────────────
+'use strict';
+const { spawnSync, execFileSync } = require('child_process');
+const fs = require('fs');
+const path = require('path');
+
+const script = path.join(__dirname, 'axiomcode');
+
+function gitBash() {
+  const candidates = [];
+  try {
+    // <git>/mingw64/libexec/git-core, or <git>/libexec/git-core on some layouts. Git\bin\bash.exe
+    // is the wrapper that puts mingw64/bin and usr/bin on PATH, which the pipeline needs;
+    // usr\bin\bash.exe alone does not.
+    const exec = execFileSync('git', ['--exec-path'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true }).trim();
+    for (let d = path.resolve(exec), i = 0; i < 4; i++, d = path.dirname(d)) candidates.push(path.join(d, 'bin', 'bash.exe'));
+  } catch { /* no git on PATH: fall through to the default locations */ }
+  for (const base of [process.env.ProgramFiles, process.env['ProgramFiles(x86)'], process.env.LOCALAPPDATA && path.join(process.env.LOCALAPPDATA, 'Programs')]) {
+    if (base) candidates.push(path.join(base, 'Git', 'bin', 'bash.exe'));
+  }
+  return candidates.find((p) => fs.existsSync(p));
+}
+
+function resolveBash() {
+  const override = process.env.AXIOMCODE_BASH;
+  if (override) {
+    if (fs.existsSync(override)) return override;
+    fail(`AXIOMCODE_BASH is set to ${override}, which does not exist.`);
+  }
+  if (process.platform !== 'win32') return 'bash';
+  const found = gitBash();
+  if (found) return found;
+  fail('axiomcode needs the bash that comes with Git for Windows, and none was found.\n' +
+       '   • install Git for Windows (https://git-scm.com/download/win), or\n' +
+       '   • set AXIOMCODE_BASH to the full path of a Git Bash bash.exe (not C:\Windows\System32\bash.exe, which is WSL).');
+}
+
+function fail(msg) {
+  process.stderr.write(`❌ ${msg}\n`);
+  process.exit(127);
+}
+
+const r = spawnSync(resolveBash(), [script, ...process.argv.slice(2)], { stdio: 'inherit', windowsHide: false });
+if (r.error) fail(`could not start bash: ${r.error.message}`);
+// Die of the same signal the CLI died of, so a caller sees what really happened.
+if (r.signal) process.kill(process.pid, r.signal);
+process.exit(r.status ?? 1);

Related: #16 (Windows engine pipeline; the comment there that first reported this now points here), #886 (the bin entry and root walk this builds on), #454 / #904 (prebuilt engines).

Activity

  1. added
    bugSomething isn't working
    platformOS / toolchain portability
    buildBuild, packaging and developer setup
    windowsMicrosoft Windows support
    on Sep 24, 2026
  2. added theissue type on Sep 24, 2026
  3. Whua689 commented on Sep 24, 2026

    @Whua689
    CollaboratorAuthor

    The WSL case above is now verified, no longer inferred: with WSL 2.7.14 and Ubuntu installed, a bare bash from the Windows PATH resolves to %LOCALAPPDATA%\Microsoft\WindowsApps\bash.exe, which runs WSL's /bin/bash. Given a C:/… script path, it fails with No such file or directory and exit 127. The plugin's MCP manifests hit the same lookup; see #1233.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingbuildBuild, packaging and developer setupplatformOS / toolchain portabilitywindowsMicrosoft Windows support

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions