Shell pains

You clone a repo, run pnpm install, and everything looks fine until you hit node or start the dev server. package.json says Node 18 (or there is an .nvmrc with 18). You type node --version and get 22—or the IDE launches its own node from an absolute path and you never even see the mismatch until something crashes. Native addons were built for a different ABI. Errors make it sound like you broke something. Someone on Slack says “just nvm use and rebuild better-sqlite3.” You were not careless. The program that started when you ran the command was simply the wrong one.

That failure is common and predictable. It is also fixable at the moment a shell or CLI turns your keystroke into a running process. This page walks through what should happen there—and how OpenShellOrg’s work sits next to DevCentr’s broader toolchain docs.

The project says one version; your machine runs another

Projects leave hints about which runtime they expect:

  • engines.node in package.json (for example "node": "18")

  • .node-version or .nvmrc

  • SDK channel pins (“use Flutter 3.x”), and similar files

Those files are useful. They tell humans and CI what to install. They do not, by themselves, change which node binary runs when you press Enter, when VS Code’s terminal spawns node, or when a GUI runs a script by full path.

You still get soft warnings (EBADENGINE) after native code already compiled against the wrong major. “Delete node_modules and rebuild” fixes the symptom long after the wrong binary did the damage. A version file that nothing honors is worse than no file—it looks like a promise and behaves like decoration.

Some doc sets call that kind of pin a declaration (what the project asks for). What was missing in the scene above is dispatch: actually installing if needed and starting the right version for this one command, without you manually switching the whole machine.

package.json asks for Node 18 but the shell still runs Node 22
Figure 1. Project pin on disk, wrong node still runs
Mermaid source
%%{init: {"htmlLabels": false, "flowchart": {"htmlLabels": false}, "theme": "base"}}%%
flowchart LR
  Pin[engines / .node-version] -.->|ignored| Wrong[Wrong major on PATH]
  User[User / IDE / CI] --> Wrong
  Wrong --> Native[Compile / load native addon]
  Native --> Boom[ABI crash]
  Boom --> Folklore[Rebuild folklore / install nvm]

What should happen when you press Enter

Picture the same repo. You type node (or pnpm dev, or your SDK’s launcher). Before your script runs, something trustworthy should:

  1. Notice the version the project asked for (engines, .nvmrc, or equivalent).

  2. Install that version if it is not on the machine yet (through a manager or installer you already trust).

  3. Run the same command again under that version—for this process tree only.

  4. Leave your global default alone. Opening an old repo should not mean “switch the whole laptop to Node 18.”

When that loop works, you are not stuck in a ritual of nvm use, hand-edited PATH, or rebuilding native modules because Node 22 ran first.

Docs here sometimes call node, your shell, or an SDK launcher the entrypoint—the program the OS actually starts for that command. The install-and-re-run loop is what those docs mean by dispatch.

Read the project pin
Figure 2. Read the pin, install if needed, run the same command under the right version
Mermaid source
%%{init: {"htmlLabels": false, "sequence": {"useMaxWidth": true}, "theme": "base"}}%%
sequenceDiagram
  participant U as User / IDE / CI
  participant E as Entrypoint (wrong major)
  participant P as Project pin
  participant C as Version cache / adapter
  participant R as Correct major

  U->>E: invoke command
  E->>P: read engines / .node-version
  alt pin unsatisfied
    E->>C: install if missing
    C-->>E: ready
    E->>R: re-exec same argv (this tree only)
    R-->>U: run command
  else pin satisfied
    E-->>U: run command
  end
  Note over E,R: Global default unchanged
How pinning
Figure 3. Toolchain pieces (team lifecycle vs the moment you hit Enter)
Mermaid source
%%{init: {"htmlLabels": false, "flowchart": {"htmlLabels": false}, "theme": "base"}}%%
flowchart TB
  U[Actors: Dev / IDE / CI]
  P[Project declares engines / pin file]
  R[Official entrypoint resolves project pin]
  I[Install if needed]
  X[Re-exec]
  L[Health / repair / upgrade]
  S[Correct runtime / SDK for this tree]
  M[Machine default stays unchanged]
  App[Project / app]

  U --> R
  P --> R
  R --> I
  I --> X
  X --> S
  S --> App
  L -.-> S
  M -.->|left alone| S
DevCentr covers toolchain lifecycle; OpenShellOrg covers behavior when you invoke a command
Figure 4. Sibling focus: DevCentr lifecycle, OpenShellOrg at the shell
Mermaid source
%%{init: {"htmlLabels": false, "flowchart": {"htmlLabels": false}, "theme": "base"}}%%
flowchart LR
  TM[DevCentr: Toolchain Management]
  PP[Pattern / Protocol / TCoP / TCF]
  LC[Pin / health / repair / upgrade]
  LO[Lifecycle ownership]
  TM --> PP --> LC --> LO
  ED[OpenShellOrg: Entrypoint Dispatch]
  RI[Resolve / install / re-exec]
  SB[Shell / CLI process boundary]
  IH[Invocation honesty]
  ED --> RI --> SB --> IH
  LO ~~~ ED

Your system default can stay on “latest.” A hard error should be the last resort—when install or re-run is impossible (no network, policy block, no installer registered). The bar we aim for is install if needed, then run, not “stop and lecture.”

Why “run nvm use when you cd” is not enough

Shell hooks that switch versions on cd help some people, sometimes. They are easy to miss:

  • IDEs and GUIs often launch node by absolute path, bypassing the shell profile that ran nvm use.

  • CI images start clean; they need the same install-and-re-run behavior, not a forgotten hook.

  • New teammates should not have to join a particular shell setup before node --version matches the repo.

That behavior belongs on the command people actually run (node, your wrapper, the SDK shim)—not only in optional shell configuration.

A worked example: wrong shell host

OpenShellOrg’s nu-require uses the same pattern for a different mismatch: the project expects Nushell but you started bash. It can offer to install Nushell and re-launch the same invocation under it. Wrong Node major at the node entrypoint is the same idea with a different pin file.

How this relates to DevCentr

DevCentr writes for teams and environments: how you pin toolchains, check health, repair broken installs, and plan upgrades over time. Their Toolchain Management Pattern is the long-form philosophy; the Toolchain Control Plane essay goes deeper on the control-plane shape.

OpenShellOrg focuses on the moment you hit Enter: the program that starts should match the project’s pin, or it should fetch what is missing and try again—without rewriting your global defaults.

The two doc sets are siblings, not competing rulebooks. Read both when you care about “always the right toolchain”; start here when the pain is “I typed node and the wrong thing ran.”

Who writes what (plain version)

Question Where to look

“What should our team standardize for installs, pins, health, and upgrades?”

DevCentr — Toolchain Management Pattern

“What should happen when someone runs node / our CLI / the SDK launcher?”

OpenShellOrg — this page; nu-require; planned env-refresh and shell-host work (Shell Host and Env Refresh)

“How should CLI flags be spelled and certified?”

open-shell-org (SOS)

If you ship a version pin

Short checklist for CLI and runtime authors:

  • When a user runs your command, the program that starts (node, your binary, or your shim) installs the pinned version if needed and re-runs the same invocation under it.

  • Global defaults may stay on “latest”; one project must not require mutating the whole machine.

  • Absolute-path and IDE launches get the same behavior as an interactive terminal.

  • Show progress: which pin file was read, what is downloading, which version is actually running.

  • If you must fail hard, say which pin blocked you—not “rebuild the native module.”