The Node.js permission model is a runtime restriction you switch on with --permission. With the flag set, the process cannot touch the file system through node:fs, spawn child processes, start worker threads, load native addons or use WASI unless you grant each capability explicitly. It has been stable since v23.5.0 and v22.13.0.
Everything else about it is more complicated than that sentence suggests.
What does —permission actually block?
Start a process with the flag and nothing else, and most side effects fail immediately:
node --permission server.js
Reading a config file throws, and the error tells you which scope and which resource:
Error: Access to this API has been restricted
at node:internal/main/run_main_module:23:47 {
code: 'ERR_ACCESS_DENIED',
permission: 'FileSystemRead',
resource: '/home/user/index.js'
}
Two reads are granted for you. The entry point is implicitly readable, and so is any module preloaded with -r, behaviour that landed in v24.2.0 and v22.17.0. Everything else you list yourself:
node --permission \
--allow-fs-read=/srv/app \
--allow-fs-read=/etc/ssl/certs \
--allow-fs-write=/var/log/app \
--allow-child-process \
server.js
--allow-fs-read and --allow-fs-write each take a single path or *. Comma-delimited lists stopped working in v20.7.0, so repeat the flag once per path. Since v24.4.0 and v22.18.0 the flags are passed down to child processes through NODE_OPTIONS, so spawning node no longer hands the child a clean slate.
Native addons sit behind their own gate, --allow-addons, and fail differently: you get ERR_DLOPEN_DISABLED at require time, not ERR_ACCESS_DENIED. Anything with a compiled binding trips it, which is one more point in favour of the built-in driver in our comparison of node:sqlite and better-sqlite3. The full flag list is in the Node.js permissions documentation.
Does it stop outbound network calls?
On Node 24, the current LTS line, no. --allow-net arrived in v25.0.0 and carries stability 1.1, active development. It is also a single boolean covering all sockets, with no host filtering of the kind Deno offers with --allow-net=example.com.
That gap matters more than the ones it fills. The scenario people enable a sandbox for, a compromised transitive dependency quietly posting environment variables to someone else’s server, is not covered on the release most production services are running. Egress filtering still belongs in your network layer.
How do I find out which permissions my app needs?
Reading a stack trace, adding one flag, restarting, and repeating is a bad afternoon. Node 24.20.0, released on 26 August 2026, and Node 25.8.0 both added --permission-audit for this. Checks run, nothing is denied, and every violation is published on a diagnostics channel.
Write a small preload that subscribes to all eight channels. -r preloads CommonJS, so this file uses require:
// audit.js
const diagnostics_channel = require('node:diagnostics_channel');
const scopes = ['fs', 'net', 'child', 'worker', 'inspector', 'wasi', 'addon', 'ffi'];
const seen = new Set();
for (const scope of scopes) {
diagnostics_channel.channel(`node:permission-model:${scope}`).subscribe((msg) => {
const line = `${msg.permission}\t${msg.resource ?? ''}`;
if (!seen.has(line)) {
seen.add(line);
console.error(`[permission] ${line}`);
}
});
}
process.on('exit', () => {
console.error(`[permission] ${seen.size} distinct denials`);
});
Then run the app, or better, run the whole test suite against it:
node --permission-audit -r ./audit.js server.js
Each message is an object with permission, the scope name, and resource, the file path or host. Turn the distinct resources into flags and you have your allow-list without a single restart loop. If you pass --permission and --permission-audit together, --permission wins and you are back in enforce mode, so drop the audit flag when you promote.
Audit mode composes with the allow flags too. Grant the permissions you are already confident about, audit the rest, and tighten one scope at a time. Piping those denials into your existing telemetry works like any other diagnostics channel subscriber, which sits comfortably alongside an OpenTelemetry setup for Node.
Can I drop permissions after startup?
Since Node 24.20.0, yes. process.permission.drop() revokes a grant for the rest of the process lifetime, which matches how servers actually behave: read secrets and certificates during boot, then never touch them again.
import fs from 'node:fs';
// started with --permission --allow-fs-read=/etc/myapp
const config = JSON.parse(fs.readFileSync('/etc/myapp/config.json', 'utf8'));
const key = fs.readFileSync('/etc/myapp/tls.key');
process.permission.drop('fs.read', '/etc/myapp');
console.log(process.permission.has('fs.read', '/etc/myapp/config.json')); // false
Two rules constrain it. The reference has to match the original grant, so if you allowed /etc/myapp you drop /etc/myapp, not a file inside it, and if you allowed * you can only drop the whole scope by calling drop('fs.read') with no reference. And the drop applies to future checks only. Open file descriptors, sockets, child processes and worker threads carry on working, so close them yourself before assuming they are gone.
Where does the Node.js permission model leak?
The documented limitation is symbolic links. They are followed even when the target sits outside a granted path, so a relative symlink inside /srv/app pointing at /etc quietly extends the grant. Audit the trees you allow, particularly anything the application can write to itself.
The undocumented gaps are worth knowing about too. The March 2026 security releases fixed CVE-2026-21716, where FileHandle.chmod() and FileHandle.chown() on the promises API skipped the write check their callback equivalents already had, letting restricted code change permissions and ownership on an open descriptor. It was an incomplete fix for CVE-2024-36137, filed two years earlier. Two more permission fixes shipped in the same batch, patched in 20.20.2, 22.22.2, 24.14.1 and 25.8.2.
That pattern is the honest picture. Enforcement is being retrofitted API by API as holes turn up, and the rate at which they turn up suggests more are waiting. Treat the flag as defence in depth. Code you actively distrust still needs a process boundary or a container.
Should you turn it on?
Turn it on for narrow leaf processes: a build step, a scheduled job, a queue consumer, an image resizer. Their allow-lists are short, they change rarely, and if a dependency update starts reading ~/.aws/credentials you find out in CI rather than in an incident review. Grant the exact directories, leave --allow-child-process off, and let the build fail when something new appears.
For a general HTTP service on Node 24, run it in audit mode instead. You get a full inventory of what the app touches with no risk of a denied call taking down a request path, and you are ready to enforce the day you move to a line where --allow-net exists.
Skip it where a container already gives you a read-only root file system and a locked egress policy. Two overlapping controls that both need maintaining is worse than one that actually gets maintained.