Blog / 01

Node.js 24.20.0 LTS Adds Package Maps and Audit Mode

WebRestart

On August 26, 2026 the Node.js project shipped v24.20.0 'Krypton' (LTS) alongside 26.8.0 and a same-day 26.8.1 on the Current line. Everything new in 24.20.0 is SEMVER-MINOR, so upgrading inside the 24.x line is a rebuild, not a migration. The two additions worth reading about are experimental package maps — a declarative replacement for node_modules resolution — and --permission-audit, which runs the Permission Model in warning-only mode so you can find out what your app actually touches before you start denying things.

Neither is a security patch. If you are still on 24.18.x or earlier, note that the July 29 CVE fixes and the 24.19.0 feature drop are both behind you in the same jump.

Package maps

--experimental-package-map=<path> (#62239) points Node at a JSON file that declares, explicitly, which packages exist, where they live on disk, and which of them each package is allowed to import. Stability is 1 — Experimental, and it is new in 24.20.0.

The file format is small. Each entry under packages takes a required url (a file: URL, absolute or relative) and an optional dependencies object mapping bare specifiers to package keys:

{
  "packages": {
    "app": {
      "url": "./packages/app",
      "dependencies": {
        "@myorg/utils": "utils",
        "@myorg/ui-lib": "ui-lib"
      }
    },
    "utils": {
      "url": "./packages/utils"
    }
  }
}

Resolution then works by lookup rather than by walking the filesystem. Node works out which package the importing file belongs to, checks the specifier against that package's declared dependencies, and forwards the resolved location to the normal algorithm. A specifier that is not declared throws MODULE_NOT_FOUND — even if the package is sitting right there in a hoisted node_modules. A file outside any mapped package throws ERR_PACKAGE_MAP_EXTERNAL_FILE.

That single behaviour is the point. Phantom dependencies — the module you never declared but import anyway because your package manager hoisted it — stop being a latent bug that surfaces on someone else's machine and become a hard error at resolution time. Monorepos with conflicting peer versions get a way to say what they mean. And because resolution is a map lookup instead of repeated directory stats, startup does less I/O.

The compatibility story is the pragmatic bit: package maps sit alongside a normal node_modules install. A package manager can emit both, tools that understand the map get strict enforcement, and tools that do not fall back to hoisting. You can try this on one service without committing the whole estate.

Permission Model: audit mode and drop()

The Permission Model has been the right idea with a bad adoption curve — you flip it on, something you did not know about hits the filesystem, and you turn it off again. --permission-audit (#61869) fixes that ordering problem. It runs the checks without enforcing them: instead of raising ERR_ACCESS_DENIED, each permission decision is published to a per-scope diagnostics channel (node:permission-model:fs and friends) and the operation proceeds.

So the workflow is now: run your test suite or a canary under audit, subscribe with diagnostics_channel, collect what the app really reads, writes and executes, and write your --allow-* flags from evidence rather than from guesswork.

permission.drop() (#62672) comes at least-privilege from the other end. It lets a process shed permissions at runtime — read your config and your certificates during boot, drop filesystem read, keep serving. One caveat from the implementation discussion: dropping a permission does not close handles you already hold. Sockets and file descriptors opened before the drop stay open, and releasing them is your code's job.

Smaller, but useful

  • using scopes for AsyncLocalStorage (#61674) — a new withScope() method, so using scope = storage.withScope(data) does what storage.run(data, fn) did, without the closure. If you carry a request context through a NestJS or Fastify request path, this removes a layer of callback nesting per hop.
  • node:stream/iter (#62066) — a new module, documented as the Iterable Streams API.
  • WebAssembly JSPI (#59941) — JavaScript Promise Integration is now enabled, letting synchronous WASM code suspend on a JS promise.
  • Test runnercontext.log() with a matching test:log event (#64389) and entryFile in TestStream events (#64309), both backported from the 26.x line we wrote about last month.
  • Root certificates updated to NSS 3.125.

On Current, 26.8.0 landed the same day with new crypto cipher modes and zlib additions; 26.8.1 followed immediately as an out-of-band fix for node --version wrongly reporting an alpha designation. Neither is a security release.

What to do

Take 24.20.0 on the next routine rebuild — nothing here is urgent and nothing here is breaking.

node --version
docker run --rm node:24-alpine node --version

Then pick one service and spend an afternoon on audit mode. It is the cheapest way to find out whether the Permission Model is realistic for your workload, and the output is a concrete list of flags rather than an opinion. Package maps are worth a spike if you run a monorepo and have been bitten by hoisting, but keep them experimental for now.

Full detail is in the 24.20.0 release notes and the package maps documentation.


Not sure which Node version is running where, or whether your services would survive the Permission Model? Talk to us — runtime audits and upgrade paths, without the three-month project.