Skip to content

Node silently runs the wrong ABI, then blames the user for not learning nvm #64815

Description

@AMDphreak

What is the problem this feature will solve?

Hearkens back to #62098 (closed wontfix / wrong repo by @Renegade334).

That closure was not careful engineering judgment. It was ownership refusal. The failure mode did not go away. It happened again today. Calling version mismatch "not the runtime's problem" does not make the problem stop happening at the node entrypoint — it only makes Node the last tool in the chain that gets to shrug while the user eats an ABI crash.

Lack of ownership is the problem. "Not my problem" is how you get an ecosystem that invents engines.node, documents it, ships it in every serious package.json, then pretends the binary named node has no duty when that field is violated.

Concrete failure (today)

Project declares:

  • engines.node: 24.x
  • .node-version: 24

Shell has Node 22 active (system default / leftover PATH). Developer runs the normal path:

cd site-nextjs
pnpm install
pnpm run dev

Result:

module better_sqlite3.node was compiled against a different Node.js version

Nothing in that flow:

  1. noticed the pin,
  2. installed Node 24 if it was missing,
  3. re-executed under 24 for this process tree (global default unchanged),
  4. or even printed a hard, blocking error before compiling native addons against the wrong ABI.

engines was decoration. .node-version was decoration. The runtime executed anyway. The package manager executed anyway. The crash was deferred to the worst possible moment: after install, at app boot, with a native-module error that sends people googling "rebuild better-sqlite3" instead of "why did Node ignore its own engines field."

That is incompetent UX for a platform that claims to own package.json semantics for this ecosystem.

Defeating the #62098 dismissals

1. "It's not the role of the runtime to upgrade and downgrade itself on execution"

Wrong framing. The ask was never "Node core becomes Homebrew." The ask is: the process the user launched notices it is the wrong process, then auto-installs (if needed) and re-execs under the declared requirement — on its own recognizance — without rewriting the machine default.

Roles already claimed by this project:

  • You define engines in the package metadata model this ecosystem treats as canonical.
  • You ship the node binary people actually invoke (directly or via package-manager shims).
  • You own native addon ABI breakage when majors diverge — the exact class of failure above.

If your role includes declaring version requirements and executing user code, your role includes not cheerfully executing the wrong major. "Not our role" here means: we want the prestige of a platform standard without the duty of enforcing it. That is dodge, not architecture.

Also: install + re-exec is not "upgrading itself." It is dispatch. Operating systems, language toolchains, and every competent package manager do dispatch. Pretending dispatch is uniquely illegitimate for Node is special pleading.

Hard refuse is only a degraded fallback when install/re-exec cannot proceed (no network, no registered manager, policy block). The product bar is auto-install and run, not "stop and lecture."

2. "Call arbitrary executables during startup based on a config file… non-starter"

This collapses on contact with reality:

  • Users already install version managers and already trust them.
  • Shell hooks already call those executables based on config files (.nvmrc, .node-version, direnv, fnm, volta).
  • IDEs and docs already tell people to do exactly that.

So the "dangerous" behavior already exists — just in the worst place (per-shell init), with the worst consistency (bypassed by absolute paths, CI images, GUI launchers, task runners), and with zero standard contract.

#62098 asked to register a user-chosen adapter behind a boring CLI contract (install / use / re-exec). That is less chaotic than today's "every shell, every OS, every manager, every hook dialect." Calling the standardized version a non-starter while defending the unstandardized version is jurisdiction theater dressed up as security.

Security claim, steelmanned: "evil-nvm in a config could run as the user."
Reality: the user who put evil-nvm on their PATH and registered it already gave it that power. Shell hooks already give it that power. Node is not creating a new trust boundary by refusing to be the one component that may name the tool the user already trusts.

If the objection is "Node must not download binaries itself," fine — that is why the proposal delegated download/cache to the registered manager. If the objection is "Node must not re-exec," say that plainly and then explain why soft-ignoring engines is acceptable when native modules exist. You cannot have both "we care about security/correctness" and "wrong ABI at runtime is an acceptable default."

3. "wrong repo" / "tell nvm instead"

This is the purest form of the ownership failure.

The user typed a command that ended up in node. The wrong major of node ran. The ABI of node rejected a native module. The metadata field engines.node — in the format this org evangelizes — was ignored.

There is no coherent definition of "right repo" that excludes the repository that ships that binary and that metadata story. "Open it on nvm" means: the platform creates a footgun, then assigns the support ticket to a volunteer shell script.

Third-party managers can help. They cannot own the default path where someone installs Node from the website/winget, clones a repo, and runs the package manager — because that path never enters nvm unless Node (or a shim you bless) forces the issue.

wrong repo is how maintainers close a door without solving a bug.

4. "Already available as a setup step before node is activated"

Translation: already available to people who already knew.

Not available to:

  • the default installer user,
  • anyone whose IDE launches node by absolute path,
  • CI jobs with a stock image,
  • Windows users bouncing between PowerShell / cmd / Windows Terminal profiles with inconsistent hooks,
  • anyone who did the reasonable thing: trust engines / .node-version because the project bothered to write them.

"Solved before node starts" is an admission that node starting is the moment of truth — and you chose not to put a check there. Putting the check everywhere except the entrypoint is how you get today's crash.

5. The meta-argument: "not my problem"

When a maintainer's first response to a systemic UX failure is boundary lawyering (wrong repo, not the role, ask nvm), they are optimizing for inbox closure, not user success.

Ownership would sound like:

  • "Yes, silent wrong-major execution is bad."
  • "Here is what core will guarantee: resolve the pin, install if needed, re-exec for this process tree — or, if that is impossible, fail loudly before wrong-ABI work."
  • "Here is who implements the adapter / cache side."

#62098 got none of that. It got a shrug with labels. That shrug is why this issue exists.

What #62098 actually asked for

A bootstrap contract: when the invoked node does not satisfy the project's declared requirement, Node consults a user-registered version-manager adapter, installs the required version if missing, and re-executes under that version — without mutating the machine's global default Node.

It did not require Node core to become a full version-manager product. It proposed that Node stop being the one binary in the chain that is too proud to notice it is the wrong binary.

The folly, stated plainly

You standardized version declaration and refused version dispatch.

Then when dispatch fails in the wild, maintainers tell users to learn a third tool whose entire job exists because the first tool will not do the obvious thing: run the version the project asked for.

Version managers bolted onto shells are afterthought UX. Users should not discover nvm after a native module ABI crash. System default Node may stay on latest. Opening an older (or newer) pinned project must auto-select that version for the command — install if needed, re-exec, leave the global default alone. Unpinned + older runtime: warn. Pinned + mismatch: do not silently continue on the wrong ABI.

Anything less is cosplay of a platform.

What is the feature you are proposing to solve the problem?

Required outcome (primary)

Auto-install + re-exec under the project pin. When engines.node / .node-version / .nvmrc (pick a documented precedence) is present and the invoked node does not satisfy it:

  1. Resolve the required version.
  2. Ensure it is available (registered manager adapter install, or a first-party fetch/cache you bless).
  3. Re-exec the same command under that version for this process tree only.
  4. Leave the machine global default unchanged.
  5. Print what happened (pin source, download if any, version now running).

This must work for absolute-path and IDE launches, not only interactive shells with hooks. "Use nvm" is not an outcome.

Degraded fallback (only if install/re-exec cannot proceed)

Hard refuse with a one-line fix hint before install/build — not soft EBADENGINE, not a native-module ABI crash after the fact.

Honesty option (if you still refuse duty)

Document in giant letters that engines.node is advisory social fiction and must not be trusted for native modules or CI — and ban closing related UX reports with "wrong repo / use nvm."

Do not close this as wrong repo. The wrong binary is named node. The wrong behavior is executing it. The repo that ships that binary is this one. If you close it anyway, you are restating the ownership failure — not answering it.

What alternatives have you considered?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions