devEngines is the one that enforces tooling for the people working on your code. The npm docs say it checks the environment before install, ci and run, and fails by default. engines is advisory unless engine-strict is set, and it mostly speaks to people who install your package as a dependency.
What the npm docs say
engines declares which Node.js and npm versions a package works with. Unless the user sets engine-strict, the field is advisory: npm prints warnings when the package is installed as a dependency. Omitting a version or using "*" accepts any version. The config reference describes engine-strict (default false) as making npm refuse to install packages that declare incompatibility with the current Node.js version, and --force overrides it.
devEngines is for contributors, not consumers. It checks the environment before install, ci and run commands. Supported keys are cpu, os, libc, runtime and packageManager. Each entry takes a name, and optionally a version and an onFail value of warn, error or ignore. If onFail is omitted it behaves as error. The docs add that setting runtime or packageManager to an unsupported name fails inside the npm CLI.
Comparison
| Question | engines | devEngines |
|---|---|---|
| Who is it for | People installing your package | People working on your source |
| Default behavior | Warns only | Errors unless onFail says otherwise |
| Can pin a package manager | No | Yes, through packageManager |
| Can pin the runtime | By version range | Yes, through runtime |
| Can be made strict | With engine-strict | Through onFail |
Why this is a security matter
Running an unexpected Node.js version can mean running without a fix. Runtimes age out of support, and a laptop or CI image left on an old version silently loses security patches. Pinning the runtime for contributors makes the intended version visible in the repository and makes a mismatch fail loudly rather than quietly.
Pinning the package manager has a related benefit. Different versions can resolve and write lockfiles differently, so a pinned tool reduces noisy lockfile diffs. For another approach to the same problem see Corepack and the packageManager field.
A setup that works
{
"devEngines": {
"runtime": { "name": "node", "version": ">=22", "onFail": "error" },
"packageManager": { "name": "npm", "onFail": "error" }
},
"engines": { "node": ">=22" }
}
Choose a version range that matches a supported release line. For which lines are supported right now, read which Node.js version to run in production.
Steps
- Decide the minimum supported Node.js version from the current support table.
- Add
devEngineswithonFailset toerrorfor runtime and package manager. - Keep
enginesin sync, since consumers of a library still read it. - Set
engine-strict=truein.npmrcif you also want installs of dependencies to fail on incompatibility, and expect some friction. - Make CI use the same Node version as the field, and fail if they differ.
- Review the range on a schedule, in line with the support dates.
Limits
These fields check the declared range. They do not prove the machine is patched, and they do not stop someone running an application with a different runtime in production. Enforce production versions in your image and deploy pipeline too. See also the engines field is advisory for the consumer side.
Safeguard flags end of life runtimes and vulnerable dependencies in your inventory. Try a free scan.
Quick check
What does devEngines do if onFail is missing?
It behaves as error, according to the npm docs.
Does engines block installs by default?
No. It only warns unless engine-strict is set.
Sources
Last reviewed: 19 October 2026.