Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save briangershon/ff47675042032c63585fbc23eb860880 to your computer and use it in GitHub Desktop.

Select an option

Save briangershon/ff47675042032c63585fbc23eb860880 to your computer and use it in GitHub Desktop.
Node.js: target Active LTS + harden npm installs with .npmrc

Node.js: Target Active LTS + Harden npm Installs with .npmrc

Notes from setting up a fresh Node/TypeScript project in mid-2026 — grounded in the actual package.json/.npmrc of a real repo, not generic advice.

Which Node version to target

As of mid-2026:

Line Status Notes
Node 24 Active LTS Promoted Oct 2025, supported until ~Oct 2026 (Maintenance begins), EOL ~Apr 2028
Node 22 Maintenance LTS Security fixes only
Node 26 Current Released May 2026, becomes the next Active LTS in Oct 2026

Target Active LTS, not Current. Current releases haven't gone through the stabilization period LTS promotion implies, and tooling/ecosystem support lags on brand-new majors. Right now that means Node 24.

One thing changing soon: starting with Node 27, Node moves to a single release per year (April), with every release becoming LTS — Node 26 is the last release under the old Current/LTS split. Worth knowing so "target the LTS line" doesn't silently stop meaning what you think it means once that transition lands.

Pin it, don't just document it. Two things, together:

// package.json
{
  "engines": {
    "node": ">=24"
  }
}
# .npmrc
engine-strict=true

Without engine-strict, the engines field is advisory — npm just prints a warning. With it, npm install hard-fails (EBADENGINE) on a mismatched Node version instead of letting someone install and hit subtly wrong behavior later. Verified: the same install that succeeds cleanly on Node 24.18.0 fails immediately under Node 20.18.2 with engine Not compatible with your version of node/npm.

.npmrc: five settings, five specific risks

Each of these was added deliberately, not copy-pasted as a bundle — here's what each one actually buys.

engine-strict=true

Covered above — turns engines into an enforced install-time gate instead of a comment.

ignore-scripts=true

Blocks preinstall/install/postinstall lifecycle scripts from running for any dependency, direct or transitive. This is the single biggest lever against a common supply-chain attack shape: a compromised package's postinstall script exfiltrating env vars, SSH keys, or CI secrets at install time, before any application code even runs.

Caveat: some legitimate packages need a postinstall step (native module builds, platform binary selection via a download script rather than optionalDependencies). Before enabling this repo-wide, audit the actual dependency tree:

for d in node_modules/*/package.json node_modules/@*/*/package.json; do
  grep -l '"postinstall"\|"install"\|"preinstall"' "$d" 2>/dev/null
done

If something breaks, don't blanket-disable this — investigate whether that specific package's script is actually needed (most modern packages now ship prebuilt binaries via optionalDependencies instead, which install fine with scripts off).

save-exact=true

npm install <pkg> writes an exact version ("4.1.10") instead of a range ("^4.1.10") into package.json. This doesn't change what npm install (no args) resolves — that's package-lock.json's job — but it stops a future npm install <new-pkg> or manual edit from reintroducing a wide range that could silently drift to a compromised patch/minor release.

min-release-age=3

Requires npm ≥ 11.10.0. Refuses to resolve any dependency version published less than 3 days ago. This directly targets the "malicious version published, widely installed within hours, pulled a day later" attack pattern that's hit the npm ecosystem repeatedly — by the time a version clears 3 days, it's had time to be caught.

Caveat: npm's current implementation has no per-package exception list (pnpm's equivalent does). If you urgently need a same-day security patch for one package, you'll need to temporarily comment this out or override on the command line rather than exclude just that package.

audit-level=high

Sets the severity threshold npm audit treats as a failure. Doesn't run automatically on npm install — wire it into CI (npm audit --audit-level=high) if you want it enforced rather than just available on demand.

fund=false

Not security — just quiets the funding-message noise on every install. Included for a clean CI log, nothing more.

How this was verified

  • Clean rm -rf node_modules package-lock.json && npm install on Node 24.18.0 / npm 11.16.0: 0 vulnerabilities, all dependency scripts confirmed unnecessary before turning ignore-scripts on.
  • Same install attempted under Node 20.18.2 (nvm exec 20.18.2 npm install): failed immediately with EBADENGINEengine-strict doing its job.
  • Injected a fake secret-shaped value into a test fixture to confirm unrelated test tooling actually catches it — not .npmrc-related, but same "verify the guard actually guards" discipline applied throughout.

Sources

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment