The Permission Model

Nothing in the audit workflow stops an installed package from reading .env at runtime. The permission model does: start with --permission and every filesystem, child-process, worker, network and addon operation is denied unless granted on the command line. It is not experimental — the documentation marks it Stability: 2 - Stable, a status it reached in Node.js v23.5.0 2,131 and v22.13.0.

report.js — what the process is actually allowed to doJavaScript
import { readFileSync, writeFileSync } from 'node:fs';
import { execSync } from 'node:child_process';
const dir = 'C:\\tmp\\perm-demo';
console.log('read :', process.permission.has('fs.read', `${dir}\\report.js`));
console.log('net  :', process.permission.has('net'));
try {
  writeFileSync(`${dir}\\stolen.txt`, 'exfiltrated');
} catch (err) {
  console.log(err.code, '|', err.permission, '|', err.resource);
}
console.log('bytes:', readFileSync(`${dir}\\report.js`).length);
execSync('whoami');
Output
> node --permission --allow-fs-read="C:\tmp\perm-demo\*" report.js
read : true
net  : false
ERR_ACCESS_DENIED | FileSystemWrite | \\?\C:\tmp\perm-demo\stolen.txt
bytes: 498
Error: Access to this API has been restricted. Use --allow-child-process to manage permissions.
    at file:///C:/tmp/perm-demo/report.js:13:1 {
  code: 'ERR_ACCESS_DENIED',
  permission: 'ChildProcess',
  resource: 'C:\\WINDOWS\\system32\\cmd.exe'
}

A denial is an ordinary throw carrying code: 'ERR_ACCESS_DENIED' plus permission and resource, and process.permission.has() lets you assert your sandbox at startup rather than discover a missing grant in production. The trailing \* matters: without it the grant covers the directory entry, not the files inside.

--allow-fs-read=<path> and --allow-fs-write=<path> take a path, * for a subtree, and are repeatable; --allow-child-process, --allow-worker, --allow-net, --allow-addons and --allow-wasi are all-or-nothing. NODE_OPTIONS will not carry --permission, so it belongs in start: node --permission --allow-fs-read=./dist/* server.js.

--permission-audit reports violations without denying them, which is how you discover the grant list for an existing service. Be precise about versions: on Node.js 25.8.0 audit mode still enforces filesystem denials, and the Node.js 26 changelog records the fix — audit mode no longer enforces fs and addon permissions — so use Node.js 26 for a real dry run. Node.js 26 also adds --allow-ffi and --allow-openssl-store, absent on 25.8.0; with FFI enabled by default in 26.9.0, --allow-ffi keeps a dependency out of arbitrary native libraries.