The ENOENT Trap
A developer encountered a baffling error while committing a fix to a fork of vercel/eve (issue #412). After staging changes in packages/eve/src/harness/tool-loop.ts, running git commit produced:
Error: spawnSync git ENOENT
at ChildProcess.spawnSync (...)
Yet git status, git diff, and git add all worked fine in the same shell. which git returned the correct path. Switching from git-bash to PowerShell didn't help. The developer wasted 20 minutes chasing a phantom "git is missing" problem.
Where the Error Actually Came From
The repository uses simple-git-hooks with a pre-commit script at scripts/pre-commit-fmt.mjs. This script runs formatters and linters, and internally shells out to git via Node's spawnSync:
spawnSync("git", ["diff", "--staged", "--name-only"], { cwd: REPO_ROOT });
Node's spawnSync throws ENOENT in exactly two cases: the command isn't on PATH, OR the cwd doesn't exist. The error message doesn't distinguish between the two — both read "Error: spawnSync git ENOENT". The developer had been reading it as "the OS couldn't find git", but the real meaning was "the working directory doesn't exist, so nothing can run there."
The Bug: A Leading Slash Windows Can't Ignore
REPO_ROOT was computed as:
const REPO_ROOT = path.resolve(new URL("..", import.meta.url).pathname);
import.meta.url returns a file URL like file:///D:/codes/eve/scripts/pre-commit-fmt.mjs on Windows. .pathname strips the file:// prefix but keeps the leading slash before the drive letter: /D:/codes/eve/scripts/pre-commit-fmt.mjs. That leading / is meaningless on Windows — there's no root-level /D: directory. But path.resolve() treats the string as already-absolute, producing something like D:\D:\codes\eve\scripts — a path that doubles the drive letter. It doesn't exist.
Passing that as cwd to spawnSync causes Node to fail immediately, reporting ENOENT on git. On macOS or Linux, this works fine because POSIX paths don't have a drive letter to double — file:///Users/me/eve/... has a .pathname that's already correct. That's why the bug shipped unnoticed: the author tested on a Unix machine.
The Correct Fix
Node provides fileURLToPath() to reliably convert file URLs to filesystem paths across all OSes:
import { fileURLToPath } from "node:url";
const REPO_ROOT = fileURLToPath(new URL("..", import.meta.url));
fileURLToPath handles the Windows drive-letter case, stripping the extra slash correctly. .pathname is a URL-spec accessor with no OS awareness.
The Developer's Actual Workaround
Rewriting a third-party pre-commit script wasn't in scope for a PR about an unrelated bug in the harness. Instead, the developer ran the tools manually (oxfmt and oxlint) and committed using the repo's documented escape hatch:
SKIP_SIMPLE_GIT_HOOKS=1 git commit -m "..."
They chose this over git commit --no-verify because --no-verify skips all hooks, while SKIP_SIMPLE_GIT_HOOKS bypasses only the broken tool. The narrowest escape hatch preserves working checks.
The Transferable Lesson
ENOENT from child_process.spawnSync is ambiguous: "command not found" and "cwd not found" produce identical error strings, with the command name right there. If you hit ENOENT for a binary you can independently verify is on PATH, don't chase PATH — check the cwd passed to the spawn call. If that cwd was derived from import.meta.url, verify it went through fileURLToPath() instead of .pathname. The latter quietly breaks on Windows and passes code review clean on a Mac every time.
Why This Matters
Cross-platform compatibility remains a persistent pain point in Node.js tooling. As more developers work on Windows (especially with WSL), relying on Unix-only path logic in build scripts and hooks can cause cryptic failures. Using fileURLToPath() is a simple, standards-compliant fix that prevents hours of debugging. Understanding that spawnSync ENOENT can mean a missing cwd, not a missing command, is a debugging pattern every Node developer should know.
What to Do Now
- Audit any use of
.pathnameonimport.meta.urlin your projects — especially in scripts that setcwdfor child processes. - Replace with
fileURLToPath()fromnode:url. - If you hit
ENOENTon a command you know exists, immediately inspect thecwdargument rather than the command path.





