NPM Version Upgrade Paths on Linux and macOS
Understand which npm upgrade path fits your setup to avoid permissions errors and version drift.

A developer runs npm install -g npm@latest, watches the install log scroll by, and finds a broken global install, a permissions error, or a command not found where npm used to work. The command looks like a one-liner, but what it touches depends entirely on how npm got onto the machine in the first place. npm ships bundled with Node.js, so most developers inherit whatever npm version came down with their Node install. That inherited version sits on top of a permission structure the developer never set up and usually can't see until an upgrade command runs into it. The official npm docs even flag, in a parenthetical, that the standard upgrade command may need sudo on Linux and macOS: a quiet admission that the easy path carries risk baked into its foundation.
How the installation origin shapes the permission landscape
There is no single safe upgrade command, because there are three different permission landscapes a given machine could be running, and the wrong command applied to the wrong one is what causes the breakage.
The first landscape comes from the official Node.js installer, which places binaries in protected system directories, /usr/local/ on macOS among them. Global npm writes under this setup require elevated access, which makes sudo npm install -g npm@latest the path most people reach for. It works, but it isn't the safest long-term posture, since every future global install now depends on remembering to escalate privileges correctly and consistently.
The second landscape comes from a system package manager, such as apt on Linux. The system package manager still considers itself the owner of that installation, and its next update can quietly overwrite whatever changes were made outside of it.
The third landscape comes from a version manager such as nvm, which installs everything under ~/.nvm, inside the user's own home directory. No system directories are involved, so no sudo is needed, and global writes succeed without any privilege escalation. This is the structural reason npm's own documentation recommends version managers as the installation method: it sidesteps the permission problem instead of leaving it for the developer to work around.
Wherever that path points determines which permission regime is in play, and which of the upgrade approaches below is appropriate.
The direct CLI upgrade's narrow window of safety
The direct CLI upgrade is appropriate for a narrow set of conditions and can cause real damage outside them, a distinction the docs acknowledge without putting much emphasis on it.
npm's documentation lists npm install -g npm@latest as the standard upgrade command for most operating systems, adding only a note that sudo may be required. That note undersells what sudo actually does in this context. Once that happens, later npm commands run without sudo can fail outright, because the files they need to touch are now owned by root instead of by the current user. A developer who ran this command last week and saw it succeed may already be carrying that ownership mismatch, which surfaces the next time a package needs a plain, unprivileged install.
The command itself isn't the danger; the directory it writes to is.
The direct CLI upgrade is safe when the prefix already points to a directory the current user owns, which is the case on a version-manager-managed install. Confirm that first with npm config get prefix. A common mistake is checking the version before the terminal's PATH has had a chance to refresh, which can make a successful upgrade look like it failed, or a failed one look like it worked.
NVM as the structurally safer upgrade path on Linux and macOS
nvm removes the permission conflict entirely: the whole Node and npm installation moves into user-space, so there's never a privileged directory for an upgrade command to collide with. That's why it functions as the right default for any developer who has full control over their own machine.
Every global npm operation under this setup writes to a directory the current user already owns, so there's nothing for sudo to do and nothing for it to break.
Two separate commands cover npm upgrades under nvm, and they're not interchangeable. nvm install-latest-npm upgrades npm within whichever Node version is currently selected, without touching the Node runtime itself, which is the right choice when only the package manager needs to move forward. nvm install node installs the newest available Node.js release, which comes bundled with whatever npm version ships at that release, and is the right choice when both the runtime and the package manager should advance together.
nvm's switching command, nvm use <version>, lets a developer try a new npm version against a project before committing to it, and roll back to the previous Node version just as easily if something breaks. A .nvmrc file at the project root extends that further: it pins a Node (and therefore npm) version directly in the repository, so any developer who runs nvm use from that directory is on the same runtime as everyone else.
nvm's usefulness has a boundary, however. A .nvmrc file sitting in the repository does nothing for CI unless the CI configuration is explicitly told to read it. That gap, between what a developer's laptop knows and what the build pipeline knows, is where the next problem starts.
Why local version control doesn't prevent drift once CI and teammates enter the picture
Any upgrade approach that lives only on individual machines creates a category of failure that stays invisible until code reaches CI or lands on a colleague's laptop. Five developers managing their own Node and npm versions independently, with no shared pin anywhere in the repository, amounts to five slightly different build environments running side by side. The divergence sits there quietly until something exposes it, and which build happens to trip over it first is mostly a matter of chance.
CI runners are configured separately from any individual's machine, and they'll use whatever Node and npm version their image or runner ships with, unless the repository tells them otherwise. Nothing about the code changed; only the toolchain running it did.
Committing package-lock.json is necessary for reproducibility across machines, but it only pins the dependency graph, not the npm version doing the installing. Two different npm versions can resolve the identical lockfile and still produce different behavior at install time. Unpinned transitive dependencies make this worse: developers typically specify their top-level libraries precisely but leave sub-dependencies to resolve however the installer decides, so a change anywhere deeper in that graph can alter the effective build without anyone touching a line of their own code.
Declarative environment management as the answer to the team-scale drift problem
Moving the Node and npm version declaration into the repository itself, as a machine-readable file every developer, CI runner, and deployment target reads from, closes off the class of drift problem that per-machine tools can't touch by design.
The underlying principle is straightforward: version truth belongs in the repository, not scattered across individual shell profiles or a setup wiki nobody updates. When the environment definition is code, it goes through code review, it can be rolled back, and it can be enforced automatically.
One documented migration shows what that looks like in practice. A team replaced its .nvmrc file with a mise.toml file as the single source of truth, removing both nvm and Corepack entirely, so that Node and pnpm versions are now declared in exactly one place. Enforcement happens at two points: a pre-push hook checks the developer's running versions against the declaration before code leaves their machine, and CI installs its own toolchain directly from mise.toml, so what reaches production matches what the repository actually declares.
That setup also makes the "one command to onboard" experience real. A new developer clones the repository, runs the setup command, and the full declared toolchain installs on its own. There's no sixteen-step README to follow and no step that says "make sure you have npm version X installed first" before anything else can run.
The Corepack Removal From Node 25+
Node removed its built-in toolchain manager starting with Node 25 and later. Any team migrating to Node 26, active LTS as of October 2026, now has to make a decision about package manager toolchain management that Node used to handle on its own.
Corepack still exists and is maintained as a standalone package, but it no longer arrives bundled with Node. Teams that relied on Corepack to automatically pin pnpm or Yarn versions need to replicate that same constraint through some other mechanism, and a declarative tool like mise.toml is exactly the kind of mechanism built for that job.
The effect compounds. An LTS upgrade changes the Node runtime, which removes Corepack, which changes how pnpm or Yarn versions get enforced, which leaves the npm version itself as a variable with nothing anchoring it. What used to be one decision, upgrade Node and move on, is now four separate decisions that have to be made explicitly.
npm's Install-Time Code Execution as a Supply Chain Decision
npm install runs code; it doesn't just fetch files. Packages can declare preinstall, install, and postinstall scripts in package.json that execute automatically the moment npm install runs, and that execution isn't limited to a project's direct dependencies. Every package anywhere in the dependency tree gets the same privilege to run code on the installing machine.
The spring 2026 "Mini Shai-Hulud" campaign, which targeted the SAP developer ecosystem through compromised npm packages, shows what that exposure looks like in practice. Neither vector required a flaw in npm itself; both turned legitimate CI/CD tooling into an attack surface.
The May 2026 "Mini Shai-Hulud Is Back" wave adds a more counterintuitive wrinkle. SLSA provenance confirms which pipeline produced a package. It says nothing about whether that pipeline's internal state was clean at the time.
None of this means older npm versions are inherently compromised. The upgrade decision and the security posture aren't separate questions.
SBOMs and dependency provenance as the audit layer npm's lockfile alone can't provide
A committed lockfile records what was resolved. A machine-readable bill of materials tells auditors, security teams, and regulators what was actually built and where each piece came from, and that distinction is turning into a requirement.
A lockfile is an internal build artifact, meant for the installer to read. An SBOM is a signed, machine-readable document that exposes the full component graph to anyone outside the build process who needs to inspect it, whether for an audit, a compliance review, or an incident response. CISA's 2026 guidance on minimum elements for SBOMs points to SPDX and CycloneDX as the two widely used formats, and recommends organizations accept any format that is widely used, interoperable, and machine-processable, without mandating those two exclusively. That guidance also notes that the earlier federal enforcement mechanism, mandatory attestation as a condition of doing business with the government, was removed following the rescission of prior policy memos. The expectation of transparency remains even where the enforcement lever has weakened.
An SBOM without attestation is a list of components. An attested SBOM ties that list to a specific, verifiable build output. That distinction matters most in the moment a security team needs an answer to "are we exposed to this CVE?" within hours rather than days, and a list of components with no link to how they were actually built doesn't get them there.
Choosing the right upgrade path for your situation
The right npm upgrade path comes down to three questions asked in order: who controls the machine, how many runtimes need to coexist on it, and whether the environment needs to be shared across a team or stood up for audit. Working through them in sequence heads off the most common mistakes described above.
For a single developer on a single project with no team coordination to worry about, start by checking the installation origin with npm config get prefix. If that prefix points to a system-owned directory, migrate to nvm first and upgrade npm from inside it, rather than patching around permission errors with sudo. If the prefix already points to a user-owned directory, under nvm or an equivalent, npm install -g npm@latest is safe to run directly, and pinning the result in .nvmrc keeps it reproducible going forward.
For a machine juggling multiple projects with different Node and npm requirements, nvm with a per-project .nvmrc file is the right tool, with nvm alias default setting a sensible fallback for any new shell.
For team environments that need consistency across developers and CI, the version declaration belongs in the repository itself, whether as .nvmrc, mise.toml, or an equivalent, not scattered across individual shell configs. Pre-push hooks that check a developer's running Node and npm versions against the declared ones catch drift at the local boundary, before it has a chance to reach the pipeline.
For teams migrating to Node 26 and later, Corepack is no longer bundled, so any team that relied on it for pnpm or Yarn pinning needs to move that constraint into its declarative toolchain file before the Node runtime itself gets upgraded. Treat the migration as a toolchain audit: list out everything Corepack used to provide implicitly, and make each one explicit in the new configuration.
For teams facing supply chain audit or compliance requirements, pair the upgrade path with a lockfile discipline review: commit package-lock.json, enforce npm ci in CI rather than npm install, and evaluate --ignore-scripts for any dependency categories that don't actually need install-time hooks to run. A declarative environment definition is also the prerequisite for generating a reliable SBOM. An environment that gets assembled differently on every machine can't produce a bill of materials anyone can trust.


