Projects, Scripts, and the Cost of Isolation

For nvm-sh/nvm, the practical question after activating a runtime is how to carry that choice into project work without making every terminal share one mutable state. This chapter focuses on the boundary between an interactive shell and a child process, then follows the consequence for global npm packages. The reader should be able to choose between changing a shell and launching a command, use .nvmrc without mistaking it for a permanent lock, and plan the migration cost created by version-specific package trees.

The distinction matters because “the project uses Node version X” can describe several different things. It might mean that a developer’s current terminal has selected X, that a command should run under X only for its lifetime, or that a project declares X as its preferred context. Nvm provides different mechanisms for those meanings rather than forcing one environment change to serve all three.

Carry project intent into the right process

nvm use is appropriate when the user wants the current shell to adopt a runtime. It changes the shell’s lookup environment, and commands launched afterward inherit that choice. This is convenient for an interactive session: a developer can inspect a project, run several commands, and keep using the same selected version until they choose another or deactivate it.

That persistent shell change is not always what a script wants. A build command should not silently alter the environment of the shell that launched it, and a CI step may need to run one tool with a selected Node.js version while later steps choose another. For that situation, nvm offers nvm exec, which runs an arbitrary command with a selected version on the child process’s PATH. nvm run is the more focused form for running node with arguments under a selected version.

The implementation makes this separation concrete through nvm-exec. Nvm passes the selected version to that helper through NODE_VERSION. The helper loads nvm without automatically selecting a default, selects the requested version inside the child shell, and finishes by replacing itself with the target command. The parent shell keeps its own environment; the command receives the environment prepared for it. This is ordinary process inheritance used deliberately as a runtime boundary.

That boundary gives each side a clear responsibility. The parent decides what command should run and which selector should describe its runtime. The child verifies or selects the corresponding installed version, updates its own lookup paths, and then becomes the target process. If the requested version is not installed, the child cannot make the project convention true by itself; it reports that the version must be installed. This is a useful failure because a script that silently switches to an unrelated runtime is often harder to diagnose than one that stops at the missing prerequisite.

The distinction also prevents accidental state leakage. A script may invoke one command with a legacy runtime, inspect its result, and then invoke another command under the parent shell’s original selection. The first command’s PATH change belongs to the child process tree. It does not become a hidden profile edit or alter the version a developer’s next interactive command will see. Conversely, if a script deliberately wants a sequence of commands to share a selection, it must arrange for them to run in the same selected shell or repeat the child-process choice explicitly.

A project’s .nvmrc can supply the selector when a command has no version argument. That makes directory context useful for both interactive work and child-process execution. A developer can enter a repository and select the declared context, while a script can ask nvm to interpret the same project file before launching its command.