Skip to content

fix: Fixed an issue with files named node.js on Windows - #64

Merged
owlstronaut merged 3 commits into
npm:mainfrom
giovannicalo:main
Jun 19, 2026
Merged

owlstronaut merged 3 commits into
npm:mainfrom
giovannicalo:main

Conversation

@giovannicalo

Copy link
Copy Markdown
Contributor

When a Node module binary is run in a folder containing a file called node.js, on Windows, that file will be opened instead of the binary.

This happens due to Windows' PATHEXT environment variable containing .JS.

The issue was originally fixed in 8b1433c, but it has reappeared in c039646.

Currently, endlocal is called after editing PATHEXT but before invoking Node, so the latter is not affected by it.

My solution edits PATHEXT after calling endlocal, thus affecting the Node invocation and fixing the issue.

The only downside is the Node process will receive the edited PATHEXT, which however must've been the original behaviour anyway.

@giovannicalo
giovannicalo requested a review from a team as a code owner April 18, 2022 02:25
@giovannicalo giovannicalo changed the title Fixed an issue with files named node.js on Windows Fix: Fixed an issue with files named node.js on Windows Apr 22, 2022
kevinoid added a commit to kevinoid/eslint-config-kevinoid that referenced this pull request May 7, 2023
To avoid inadvertently running node.js (using Windows Script Host)
instead of node.exe when .js is present in %PATHEXT% (as it is by
default).  The problem is exacerbated by a regression in .cmd shims
generated by npm (npm/cmd-shim#64
npm/cmd-shim#71) and has already caused
problems in CI (https://github.com/actions/setup-node/issues/720).

Continue to export node and node.js from the package for backward
compatibility.  These may be removed in a future version.

Signed-off-by: Kevin Locke <kevin@kevinlocke.name>
@owlstronaut

Copy link
Copy Markdown

Sorry this got stale. Reopen a pr if it is still good/relevant

@owlstronaut owlstronaut closed this Jun 8, 2026
kevinoid added a commit to kevinoid/setup-node-issue-720 that referenced this pull request Jun 10, 2026
See: npm/cmd-shim#71
See: npm/cmd-shim#64
Signed-off-by: Kevin Locke <kevin@kevinlocke.name>
@kevinoid

kevinoid commented Jun 10, 2026

Copy link
Copy Markdown

@owlstronaut I can still reproduce the issue in main (6ea7b53) by running (in cmd.exe):

echo #!/usr/bin/env node >mycommand.js
echo console.log('Success!') >>mycommand.js
md bin
node -e "require('cmd-shim')('mycommand.js', 'bin/mycommand')"
type nul >bin/node.js
cd bin
mycommand

I can also confirm that this PR still applies cleanly and fixes the issue, as demonstrated by a Github Actions Workflow.

I urge you to consider reopening and merging this PR.

@owlstronaut owlstronaut reopened this Jun 10, 2026
@owlstronaut

Copy link
Copy Markdown

@kevinoid great, I've reopened it. Happy to take a look when CI is happy :)

@giovannicalo

Copy link
Copy Markdown
Contributor Author

It seems in the last 4 years a test has been added which my original commit didn't cover.

I've updated the snapshot. It should pass now.

@owlstronaut owlstronaut changed the title Fix: Fixed an issue with files named node.js on Windows fix: Fixed an issue with files named node.js on Windows Jun 19, 2026
@owlstronaut
owlstronaut merged commit 1c2f94c into npm:main Jun 19, 2026
23 of 24 checks passed
@github-actions github-actions Bot mentioned this pull request Jun 19, 2026
owlstronaut pushed a commit that referenced this pull request Jun 19, 2026
🤖 I have created a release *beep* *boop*
---


## [9.0.2](v9.0.1...v9.0.2)
(2026-06-19)
### Bug Fixes
*
[`1c2f94c`](1c2f94c)
[#64](#64) Fixed an issue with files
named node.js on Windows (#64) (@giovannicalo)
### Chores
*
[`4f2209b`](4f2209b)
[#185](#185) bump
@npmcli/template-oss from 5.1.0 to 5.1.1 (#185) (@dependabot[bot],
@npm-cli-bot)

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
liustack added a commit to liustack/modlens that referenced this pull request Aug 16, 2026
…atch (#43)

npm installs a compiled CLI as a .cmd that runs the .exe beside it, and
handing that .cmd to spawn is the EINVAL that broke the claude-cli
fallback and the opencode reuse probe. The first attempt at this
classified each line against a whitelist; eight review rounds found
eight defects in it, every one the same shape: a line cannot be judged
on its own, because whether it runs at all is a property of the file.
The last was an IF EXIST whose condition is false, where cmd runs
nothing and the reader still produced a plan that spawned the provider.
That trades a loud failure for a silent wrong action, so it was
withdrawn from 3.17.1 rather than shipped.

This does not parse batch. It recognises the shapes the two real
generators emit, each carrying its own closed recipe. No block tree, no
free composition, which is where every defect lived. Anything else is
declined and the caller's own spawn error stands.

npm's Node shim is two templates, because the PATHEXT edit changes
meaning between them. Through 9.0.1 it sits inside SETLOCAL while the
execution line begins with endLocal, so it is undone before the lookup
and before the child starts; honouring it would reproduce a bug npm
fixed in 9.0.2 (npm/cmd-shim#64). From 9.0.2 it rides the execution
line after endLocal, where it applies, on either arm. pnpm writes it
inside the ELSE block and never calls endlocal, so it applies to that
arm alone. npm's native template calls no ENDLOCAL at all, so its child
inherits the dp0 the prologue set, and the plan carries it.

Bare `node` is resolved the way cmd resolves it: the exact name before
any PATHEXT candidate, the working directory before PATH unless
NoDefaultCurrentDirectoryInExePath says otherwise, relative PATH
entries anchored to that directory, and a hit that is itself a batch
file declined. There is no process.execPath fallback, which would run a
different Node than the shell whenever a portable install or an
overridden PATH is involved.

The existence test is the sharp edge. Its operand is never taken from
the file: each recipe tests the one candidate it is about to run, built
from the shim's own directory, and the file has to name exactly that. A
path captured out of an untrusted file and handed to the filesystem is
a file-existence oracle, and a UNC or device path turns a check into
network I/O and authentication that reading a local shim never
authorised. Relative, drive-relative, root-relative, UNC and device
paths are refused before the file is opened, not after. Existence is
tri-state, because existsSync folds a permission error into "absent"
and absent decides which program runs.

Environment handling mirrors what Node actually passes on Windows:
keys folded case-insensitively, PATHEXT substitution case-insensitive
the way cmd does it. analyzer built the child environment after
planning, so the planner read one environment while the child got
another; it is built first now, and the spawn cwd reaches the planner
because cmd's lookup depends on it. The reuse probe and the Pi key
fetch pass the plan's environment through for the same reason.

Fixtures are verbatim generator output with each version installed in
its own directory, after an earlier attempt asked npm for two versions
of one package in a single install and silently got the older.
sobermh pushed a commit to TokensAPI/tokens_DshModelManager_code that referenced this pull request Aug 19, 2026
…atch (#43)

npm installs a compiled CLI as a .cmd that runs the .exe beside it, and
handing that .cmd to spawn is the EINVAL that broke the claude-cli
fallback and the opencode reuse probe. The first attempt at this
classified each line against a whitelist; eight review rounds found
eight defects in it, every one the same shape: a line cannot be judged
on its own, because whether it runs at all is a property of the file.
The last was an IF EXIST whose condition is false, where cmd runs
nothing and the reader still produced a plan that spawned the provider.
That trades a loud failure for a silent wrong action, so it was
withdrawn from 3.17.1 rather than shipped.

This does not parse batch. It recognises the shapes the two real
generators emit, each carrying its own closed recipe. No block tree, no
free composition, which is where every defect lived. Anything else is
declined and the caller's own spawn error stands.

npm's Node shim is two templates, because the PATHEXT edit changes
meaning between them. Through 9.0.1 it sits inside SETLOCAL while the
execution line begins with endLocal, so it is undone before the lookup
and before the child starts; honouring it would reproduce a bug npm
fixed in 9.0.2 (npm/cmd-shim#64). From 9.0.2 it rides the execution
line after endLocal, where it applies, on either arm. pnpm writes it
inside the ELSE block and never calls endlocal, so it applies to that
arm alone. npm's native template calls no ENDLOCAL at all, so its child
inherits the dp0 the prologue set, and the plan carries it.

Bare `node` is resolved the way cmd resolves it: the exact name before
any PATHEXT candidate, the working directory before PATH unless
NoDefaultCurrentDirectoryInExePath says otherwise, relative PATH
entries anchored to that directory, and a hit that is itself a batch
file declined. There is no process.execPath fallback, which would run a
different Node than the shell whenever a portable install or an
overridden PATH is involved.

The existence test is the sharp edge. Its operand is never taken from
the file: each recipe tests the one candidate it is about to run, built
from the shim's own directory, and the file has to name exactly that. A
path captured out of an untrusted file and handed to the filesystem is
a file-existence oracle, and a UNC or device path turns a check into
network I/O and authentication that reading a local shim never
authorised. Relative, drive-relative, root-relative, UNC and device
paths are refused before the file is opened, not after. Existence is
tri-state, because existsSync folds a permission error into "absent"
and absent decides which program runs.

Environment handling mirrors what Node actually passes on Windows:
keys folded case-insensitively, PATHEXT substitution case-insensitive
the way cmd does it. analyzer built the child environment after
planning, so the planner read one environment while the child got
another; it is built first now, and the spawn cwd reaches the planner
because cmd's lookup depends on it. The reuse probe and the Pi key
fetch pass the plan's environment through for the same reason.

Fixtures are verbatim generator output with each version installed in
its own directory, after an earlier attempt asked npm for two versions
of one package in a single install and silently got the older.
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.

3 participants