Deno Permissions Model Explained
Deno permissions explained: allow and deny flags, scoping, deno.json permission sets, Permissions API, and security footguns.
Deno runs your code in a sandbox that grants nothing up front: file system, network, environment variables, subprocesses, system information, and native libraries (FFI) are all closed until you switch each one on with an --allow-* flag, and nearly every flag takes an argument that narrows the grant to named paths, hosts, or variables.
If you are arriving from Node, your first Deno script will almost certainly stop dead on a permission error, and the fix is rarely the blanket -A that muscle memory reaches for. Working out which flag to add, and how tightly to scope it, is most of the learning curve.
This is the inverse of the historical Node.js default, and it’s the single most important thing to understand before running any Deno script. This article breaks down what’s blocked by default, every --allow-* and --deny-* flag with scoping syntax, the Deno 2.x additions (deny precedence, --allow-sys, --allow-import, wildcard env), permission sets in deno.json, the runtime Permissions API, and the two security footguns the docs are quietest about.
Key Takeaways
- By default, Deno code cannot read or write files, open network connections, read environment variables, spawn subprocesses, access system information, or load native libraries. You opt in per capability with an
--allow-*flag. - Every
--allow-*flag has a--deny-*counterpart, and deny always wins:--allow-read=. --deny-read=./secretsgrants the project directory but keeps./secretsunreadable. - In Deno 2 a denied capability raises
Deno.errors.NotCapable(renamed from the oldPermissionDenied), separating Deno’s permission refusals from ordinary OS errors. - Since Deno 2.5 you can define named permission sets in
deno.jsonand apply them with-P=name(or adefaultset with a bare-P), keeping least-privilege flags in version control. - Nothing in the initial static import graph is checked against the permission system before it loads, and
--allow-runruns subprocesses outside the sandbox. Those are the two ways untrusted code escapes.
Why is Deno secure by default?
Nothing runs with ambient privileges: disk, network, environment, and subprocess spawning all stay shut until you open them. That design decision came directly from Ryan Dahl, Node’s original creator, who built Deno to reverse Node’s “full access to everything” default. In Deno, dependencies get no ambient authority of their own; in Node, a package inherits whatever system I/O the surrounding process can reach, and that gap is the sharpest difference between the two runtimes.
Node has since added its own permission model. It shipped experimentally in Node 20 behind --experimental-permission, was marked stable in v23.5.0, and Node 24 retired the experimental spelling in favour of plain --permission. Deno’s model is still deeper: it’s the default rather than an opt-in flag, and it covers more capability classes with finer scoping.
Discover how at OpenReplay.com.
What are Deno’s —allow-* permission flags?
Each capability maps to one flag, and most flags accept an allow-list argument. A bare flag grants everything in that class; an argument narrows it. A bare --allow-net grants access to every host on every port, while --allow-net=api.example.com:443 restricts the program to exactly one host and port.
| Flag | Guards | Scoped example | Deny counterpart |
|---|---|---|---|
--allow-read | File-system reads | --allow-read=./data,config.ini | --deny-read |
--allow-write | File-system writes | --allow-write=./tmp | --deny-write |
--allow-net | Network access | --allow-net=api.example.com:443 | --deny-net |
--allow-env | Environment variables | --allow-env=PORT,HOST | --deny-env |
--allow-run | Subprocesses | --allow-run=git,deno | --deny-run |
--allow-sys | System info APIs | --allow-sys=hostname | --deny-sys |
--allow-ffi | Native libraries | --allow-ffi=./lib.so | --deny-ffi |
--allow-import | Remote HTTPS imports | --allow-import=jsr.io | --deny-import |
Note there is no --allow-hrtime. That flag was removed in Deno 2.0, and high-resolution timing APIs like performance.now() are always available now.
When a script needs a permission you didn’t grant, Deno pauses and prompts interactively:
┏ ⚠️ Deno requests net access to "deno.com:443".
┠─ Requested by `fetch()` API.
┗ Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all net permissions) >
Answer y to grant once, n to deny (which raises Deno.errors.NotCapable), or A to allow the whole class. In CI, pass the flags up front so nothing blocks on a prompt.
How the flag set grew: deny precedence, --allow-sys, --allow-import, wildcard env
Deny flags landed in Deno 1.36 (August 2023), and every --allow-* flag has carried a --deny-* counterpart since. Wherever the two overlap it is the denial that applies, which lets you grant broadly and carve out exceptions:
deno run --allow-read=. --deny-read=./secrets app.ts
--allow-sys, which dates back to Deno 1.26 (October 2022), gates system-info APIs such as Deno.hostname() and Deno.systemMemoryInfo(). The one genuinely new capability class in Deno 2.0 was --allow-import, which governs which HTTPS hosts your code may pull modules from at runtime; plain HTTP is never permitted, static imports are filtered against the list automatically, and naming your own hosts replaces Deno’s built-in set rather than adding to it. Use --deny-import to block specific hosts outright.
Environment access gained suffix wildcards in Deno 2.1. Instead of listing every variable, scope by prefix:
deno run --allow-env="AWS_*" main.ts
Declaring permissions in deno.json
Since Deno 2.5 you can define named permission sets in deno.json and apply them with -P=name (or --permission-set=name), keeping least-privilege flags in version control instead of retyping them on every run. The object keys are the flag names (read, write, net, env, sys, run, ffi, import), as documented in the deno.json reference:
{
"permissions": {
"default": {
"read": ["./deno.json"],
"env": true,
"run": { "allow": ["git"] }
},
"process-data": {
"read": ["./data"],
"write": ["./data"]
}
},
"tasks": {
"dev": "deno run -P main.ts"
}
}
Run deno run -P=process-data main.ts for the named set, or deno run -P main.ts for the default set. Deno 2.5 also added the DENO_AUDIT_PERMISSIONS env var: point it at a file path and Deno appends a JSONL entry for every permission the program touches, whether that access was granted or refused. It is a quick way to find out what a script genuinely needs.
The runtime Permissions API
Query permissions in code before a restricted operation to fail gracefully instead of crashing on a NotCapable error. Deno.permissions exposes query, request, and revoke, each taking a descriptor like { name: "net", host: "example.com" }:
const desc = { name: "net", host: "example.com" } as const;
let status = await Deno.permissions.query(desc); // "prompt" | "granted" | "denied"
if (status.state === "prompt") {
status = await Deno.permissions.request(desc); // triggers the y/n/A prompt
}
if (status.state === "granted") {
await fetch("https://example.com");
}
await Deno.permissions.revoke(desc); // drop it again
query reports the current state without prompting, request prompts the user if the state is still prompt, and revoke gives a capability back. This lets long-running programs check before touching a resource and take a different path when access isn’t available.
The two footguns: imports and --allow-run
Two behaviors let untrusted code sidestep the sandbox, and both are worth internalizing. First, everything Deno can resolve statically from your entry point (local files, npm and JSR packages, and remote URLs written out as string literals) is fetched before the permission system gets a say, so a dependency can read its own source and reach the network before your first --allow-* flag ever applies. That free pass covers loading and nothing else: the moment the code executes, every operation is checked again. --allow-import scopes which remote hosts can be imported, but it does not make imports themselves require a runtime grant, so audit third-party code before you pull it in.
Second, --allow-run is the sharpest footgun: whatever you spawn becomes a process in its own right, carrying the privileges the operating system grants it rather than the narrow set you handed to Deno. That means --allow-run=deno lets a sandboxed script relaunch Deno with --allow-all and escape entirely. It also only restricts which executable runs, not its arguments: --allow-run=cat lets code read any file via cat. Scope it to specific trusted binaries like --allow-run=git, and note that --allow-ffi carries the same class of risk, since native libraries execute as machine code outside the JavaScript-layer checks.
The practical stance: grant the narrowest allow-list that works, add --deny-* over sensitive paths, and treat --allow-run and --allow-ffi as trust boundaries, not conveniences. Start from zero permissions, run the script, and add back exactly what the prompts (or the DENO_AUDIT_PERMISSIONS log) tell you it needs.
FAQs
What is the difference between a Deno permission prompt saying no and Deno.errors.NotCapable?
They are the same outcome from different entry points. When you answer 'n' to an interactive prompt, or run without the required flag, the denied operation raises Deno.errors.NotCapable in Deno 2 (renamed from the old PermissionDenied). The rename lets you distinguish Deno's own permission refusals from ordinary operating-system errors like a missing file, since both previously surfaced as similar-looking failures.
Does --allow-net=example.com also allow HTTPS on port 443?
Yes. When you specify a host without a port, such as --allow-net=example.com, Deno permits connections to that host on any port, including 443. To restrict to a single port you must write it explicitly as --allow-net=example.com:443, which then blocks all other ports on that host. A bare --allow-net with no argument grants every host on every port.
Can I combine --allow-read with --deny-read on overlapping paths?
Yes, and deny always wins. Running --allow-read=. --deny-read=./secrets grants read access to the entire project directory except ./secrets, which stays unreadable. Deny flags take precedence over allow flags in every capability class, so this pattern lets you grant broadly and carve out sensitive paths rather than enumerating every allowed file individually.
Do I need --allow-import to use npm or JSR packages?
No, not for statically imported packages. Anything Deno can resolve from your entry point without executing code, npm and JSR packages included, is fetched before the permission system is consulted. --allow-import decides which HTTPS hosts remote imports may come from, and plain HTTP is never an option. A specifier computed at runtime is different: a dynamic remote URL needs --allow-import, and a dynamic local path needs --allow-read.
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.
Star on GitHub12k