When Node.js 24 shipped in May 2025, one small rename buried in the changelog mattered more to me than V8 13.6 or npm 11: --experimental-permission became --permission. That flag has been promoted to Long Term Support since October 28, 2025, which means the runtime finally has a built in way to say "this process may read these files and nothing else."
If you have ever installed a build tool at 2am and wondered what it could reach on your disk, this is the feature you have been waiting for. Here is the field guide I keep open when I lock down a service.
The one flag that turns everything off
The model is deny by default. The moment you pass --permission, Node blocks filesystem, child process, worker thread, and native addon access until you explicitly grant it back.
# Nothing gets through. This will throw ERR_ACCESS_DENIED
# the second it tries to read a file.
node --permission app.js
Think of --permission as the master switch and every --allow-* flag as a hole you punch in the wall on purpose.
Filesystem: read and write are separate
Reads and writes have their own flags, and both accept * for everything or a comma separated list of paths.
# Read the app dir, write only to a logs folder.
node --permission \
--allow-fs-read=./src \
--allow-fs-read=./package.json \
--allow-fs-write=./logs \
server.js
A detail that bites people: paths are resolved, and a trailing wildcard like --allow-fs-read=/home/nick/* grants a directory tree, while a bare path grants exactly that entry. Be specific. The whole point is to stop npm run build from quietly reading your .env or ~/.ssh.
Network, child processes, and workers
These are the escape hatches an attacker actually wants, so they each need their own grant.
| Flag | What it opens | When I use it |
|---|---|---|
--allow-net | Outbound and inbound network | Almost always for a web service |
--allow-child-process | spawn, exec, fork | Rarely, and it makes me nervous |
--allow-worker | worker_threads | CPU bound jobs |
--allow-addons | Native .node addons | Only for packages that need them |
--allow-child-process is the big one. Grant it and you have basically handed back the keys, because a child process runs without the parent's restrictions. If a dependency needs it, that is worth a second look.
Put it in a config file, not a 200 character command
Nobody wants to maintain a shell line with eight flags. Node reads a config file, so the policy lives in version control next to the code.
{
"permission": {
"allow-fs-read": ["./src", "./package.json"],
"allow-fs-write": ["./logs"],
"allow-net": true,
"allow-child-process": false,
"allow-worker": true
}
}
node --experimental-config-file=node.config.json app.js
Now a code review can catch the moment someone flips allow-child-process to true, which is exactly the kind of change that should require a conversation.
Check permissions at runtime before you fail
You do not have to let an operation blow up to learn it was blocked. process.permission.has() tells you ahead of time, which is handy for graceful degradation.
// Skip the disk cache instead of crashing when writes are denied.
function writeCache(path, data) {
if (!process.permission.has('fs.write', path)) {
console.warn(`No write access to ${path}, skipping cache`);
return false;
}
fs.writeFileSync(path, data);
return true;
}
The scopes map to the flags: fs.read, fs.write, net, child, worker, addon. Pass a second argument for path scoped checks on the filesystem ones.
Roll it out with audit mode first
Turning on deny by default in an app you did not write can be a bad afternoon. Node 25.8.0 added --permission-audit, which runs every check but logs violations through the diagnostics channel instead of throwing.
# Discover what your app actually touches, break nothing.
node --permission --permission-audit app.js 2> permissions.log
Run your test suite or click through the app, collect the warnings, and turn each one into a precise --allow-* grant. Then drop audit mode and let denials become real errors.
What it is not
Two honest caveats. First, this is not a sandbox. It is a guardrail against accidental or supply chain access, not a defense against a determined attacker who already has code execution and time. Second, child_process remains the soft spot, because anything you spawn escapes the model unless you re-apply flags to the child.
Rafael Gonzaga, who chairs the Node.js Security Working Group, walked through the history and design tradeoffs of this feature in a talk that is still the clearest explanation I have found:
The takeaway
For years the answer to "what can this npm package read on my machine" was "everything the process can." Node 24 makes the honest answer "whatever you granted, and I can list it." I have started adding a node.config.json to new services on day one, the same way I reach for a .gitignore. The next thing I want is this on by default in the tooling I run locally, so that a random postinstall script has to ask before it goes looking around my home directory.