Fix: Deno PermissionDenied — Missing --allow-read, --allow-net, and Other Flags
Part of: JavaScript & TypeScript Errors
Quick Answer
How to fix Deno PermissionDenied (NotCapable in Deno 2) errors — the right permission flags, path-scoped permissions, deno.json permission sets, and the Deno.permissions API.
The Error
You run a Deno script and it immediately crashes:
error: Uncaught PermissionDenied: Requires read access to "./config.json", run again with the --allow-read flag
at Object.readTextFileSync (ext:deno_fs/30_fs.js:732:9)
at file:///home/user/app/main.ts:3:1Or it fails on a network request:
error: Uncaught (in promise) PermissionDenied: Requires net access to "api.example.com:443", run again with the --allow-net flag
at mainFetch (ext:deno_fetch/26_fetch.js:195:9)
at file:///home/user/app/main.ts:7:1Or it crashes when reading an environment variable:
error: Uncaught PermissionDenied: Requires env access to "HOME", run again with the --allow-env flag
at Object.get (ext:runtime/30_os.js:85:16)
at file:///home/user/app/main.ts:5:1In Deno 2 (released October 2024), the same errors appear as NotCapable rather than PermissionDenied:
error: Uncaught (in promise) NotCapable: Requires write access to "./output/", run again with the --allow-write flagAll of these mean the same thing: your script tried to access a resource that Deno’s security sandbox blocked.
Why This Happens
Deno is secure by default. Unlike Node.js or Bun, a Deno script can’t read files, make network requests, or access environment variables unless you explicitly grant it permission to do so via command-line flags.
This is intentional. A script you download from the internet can’t quietly exfiltrate your SSH keys or phone home to an external server — unless you run it with --allow-read and --allow-net. That’s the design. Most modern runtimes assume the script you’re about to run is trusted; Deno assumes the opposite. That single inversion is what produces every PermissionDenied / NotCapable error you’ll ever see from Deno — your script (or one of its transitive dependencies) crossed a boundary that the sandbox hasn’t been opened on.
The reason this is enforced at the runtime layer, rather than at the OS or container layer, is supply-chain risk. A npm package with 50 million downloads can push a malicious version overnight; a Deno script importing the same package can’t escalate beyond the permissions you granted on the command line. The sandbox is not a sandbox in the OS sense (it doesn’t replace seccomp or AppArmor) — it’s a runtime capability check. Every native call into the standard library passes through a permission check before it hits the underlying syscall.
The permission system covers eight areas:
| Flag | What it controls |
|---|---|
--allow-read | File system reads |
--allow-write | File system writes |
--allow-net | Network access (TCP, fetch, WebSocket) |
--allow-env | Reading environment variables |
--allow-run | Spawning subprocesses |
--allow-sys | System info (hostname, memory, OS) |
--allow-ffi | Native library loading |
--allow-import | Remote HTTP/HTTPS module imports |
If your script triggers any of these without the corresponding flag, Deno throws PermissionDenied (or NotCapable in Deno 2) and exits.
Fix 1: Add the Correct Permission Flag
The simplest fix is to pass the missing flag when running your script.
Read access:
deno run --allow-read main.tsNetwork access:
deno run --allow-net main.tsEnvironment variables:
deno run --allow-env main.tsMultiple permissions at once:
deno run --allow-read --allow-net --allow-env main.tsGrant all permissions (for development or trusted scripts):
deno run --allow-all main.ts
# or the shorthand:
deno run -A main.tsThe error message always tells you which flag to add — read it carefully. For Requires net access to "api.example.com:443", add --allow-net. For Requires env access to "DATABASE_URL", add --allow-env.
Common Mistake: Using -A everywhere because it stops all permission errors feels like a shortcut, but it defeats the entire point of Deno’s security model. Any dependency in your project — including transitive ones you didn’t install directly — gets full access to your filesystem and network. Use -A during exploratory development, then tighten to specific flags before committing or deploying.
Fix 2: Scope Permissions to Specific Paths and Hosts
Using --allow-read with no argument grants read access to the entire filesystem. That works, but it’s broader than necessary. Every permission flag accepts an optional scope that limits what it applies to.
Restrict reads to specific directories:
# Only allow reading from ./config and ./data
deno run --allow-read=./config,./data main.tsRestrict network access to specific hosts:
# Only allow requests to these two hosts
deno run --allow-net=api.example.com,cdn.example.com main.ts
# With port
deno run --allow-net=127.0.0.1:8080 main.ts
# Wildcard subdomains
deno run --allow-net=*.example.com main.tsRestrict environment variable access:
# Only allow reading DATABASE_URL and NODE_ENV
deno run --allow-env=DATABASE_URL,NODE_ENV main.tsRestrict subprocess execution:
# Only allow running git and curl
deno run --allow-run=git,curl main.tsNote: --allow-run only controls what Deno permits at the sandbox level. The subprocess itself still needs OS-level execute permission. If you’re hitting bash: ./script.sh: Permission denied inside a subprocess, that’s a separate issue — see bash permission denied for OS-level fixes like chmod +x.
Pro Tip: Scoped permissions are production hygiene, not just security theater. If your script only needs to read ./config.json, grant --allow-read=./config.json. If a compromised dependency later tries to read ~/.ssh/id_rsa, Deno blocks it automatically.
Fix 3: Configure Permissions in deno.json Tasks
Running long permission flags manually every time gets tedious. Put them in deno.json under tasks:
{
"tasks": {
"dev": "deno run --allow-net --allow-read=./src --allow-env=DATABASE_URL,PORT src/main.ts",
"start": "deno run --allow-net --allow-read=./src --allow-env src/main.ts",
"test": "deno test --allow-read=./src,./tests --allow-env=TEST_DATABASE_URL"
}
}Then run:
deno task dev
deno task start
deno task testThis locks in your permission requirements alongside the command, so the whole team runs with the same flags. If you’re deploying Deno code to Cloudflare Workers via Wrangler, permissions work differently — the Workers runtime has its own access model. See Wrangler not working for Cloudflare-specific configuration issues.
Fix 4: Use Permission Sets in deno.json (Deno 2.5+)
Deno 2.5 added a permissions field to deno.json that lets you define named permission sets and reference them with -P:
{
"permissions": {
"default": {
"read": ["./src", "./deno.json"],
"net": ["api.example.com"],
"env": ["DATABASE_URL", "PORT"]
},
"dev": {
"read": true,
"write": true,
"net": true,
"env": true,
"run": ["deno", "git"]
},
"test": {
"read": ["./src", "./tests"],
"env": ["TEST_DATABASE_URL"]
}
}
}Use them at runtime:
# Uses the "default" permission set
deno run -P main.ts
# Uses the "dev" permission set
deno run -P=dev main.ts
# Uses the "test" permission set
deno test -P=testYou can also mix allow and deny within a set:
{
"permissions": {
"safe": {
"read": {
"allow": ["./src"],
"deny": ["./src/secrets"]
}
}
}
}This is the cleanest way to codify your security requirements in larger projects — the permission policy lives in the repo, not in tribal knowledge or shell scripts.
Fix 5: Check Permissions Programmatically
If your script needs to behave differently depending on what permissions it has, use the Deno.permissions API to query the current state before attempting an operation:
// Check before reading
const readStatus = await Deno.permissions.query({ name: "read", path: "./config.json" });
if (readStatus.state === "granted") {
const config = await Deno.readTextFile("./config.json");
// use config
} else {
// Fall back to defaults
console.warn("No read access — using default config");
}Request permission interactively (prompts the user in a terminal):
const status = await Deno.permissions.request({ name: "net", host: "api.example.com" });
if (status.state === "granted") {
const res = await fetch("https://api.example.com/data");
// ...
} else {
throw new Error("Network access denied — cannot fetch data");
}Revoke a permission once you no longer need it:
// Read the file, then immediately revoke read access
const data = await Deno.readTextFile("./credentials.json");
await Deno.permissions.revoke({ name: "read", path: "./credentials.json" });
// From this point forward, reading ./credentials.json will failThe query method is the most useful in practice — it lets you write defensive initialization code that degrades gracefully instead of crashing.
Permission descriptor shapes by type:
// Each permission type has a specific descriptor shape
await Deno.permissions.query({ name: "read", path: "./data" });
await Deno.permissions.query({ name: "write", path: "./output" });
await Deno.permissions.query({ name: "net", host: "example.com" });
await Deno.permissions.query({ name: "env", variable: "HOME" });
await Deno.permissions.query({ name: "run", command: "git" });
await Deno.permissions.query({ name: "ffi", path: "./native.so" });
await Deno.permissions.query({ name: "sys", kind: "hostname" });Fix 6: Block Specific Resources with —deny Flags
Deno 2 added --deny-* flags that take priority over any allow flag. This lets you grant broad access and carve out exceptions:
# Allow all env vars except credentials
deno run --allow-env --deny-env=AWS_SECRET_ACCESS_KEY,DATABASE_PASSWORD main.ts
# Allow all reads except the secrets directory
deno run --allow-read --deny-read=./secrets main.ts
# Allow network, but not to internal services
deno run --allow-net --deny-net=169.254.169.254 main.tsNote: Deny flags always win. If you pass both --allow-read=./secrets and --deny-read=./secrets/api-key.json, the file at ./secrets/api-key.json is denied regardless.
This pattern is useful when wrapping third-party scripts or running tools you don’t fully control, where you want to allow broad categories but block specific sensitive paths.
Deno’s Sandbox vs Node, Bun, WebAssembly, and Containers
Deno’s permission model is the most visible thing that separates it from other JavaScript runtimes. It’s worth comparing how each runtime handles trust.
Deno. Capability-based, deny-by-default, opt in via flags. The only runtime in this list that refuses to read a file by default. Permissions are per-process; flags can be scoped to specific paths, hosts, env variables, or commands. Deny lists override allow lists.
Node.js. No sandbox at all by default. A script can read any file the OS user can read, open any socket, exec any subprocess, and load native addons. Node 20 introduced experimental --permission and --allow-fs-read / --allow-fs-write / --allow-child-process flags that mirror Deno’s model conceptually, but coverage is incomplete (no network granularity, no env granularity in stable releases) and adoption inside the ecosystem is minimal. In practice, Node still relies entirely on OS-level controls.
Bun. Like Node, no runtime sandbox. Bun is explicitly designed for performance and Node.js compatibility, not for sandboxed execution. If you need a Bun-equivalent sandbox you have to fall back to containers or VMs. Bun-specific errors are covered in Fix: Bun Not Working, but none of them involve permission flags.
WebAssembly (standalone, via wasmtime/wasmer). The strictest of all. A raw WASM module has zero capabilities — no syscalls, no clock, no filesystem. Capabilities arrive through the WASI host interface. Wasmtime’s --allow-precompiled, --dir, and --env flags grant capabilities at module load time. The model is similar to Deno’s: deny by default, opt in by flag. Unlike Deno, the granularity is finer (per-file-descriptor) but the ergonomics are much worse — you can’t fetch from inside WASM without a host-provided shim.
Containers (Docker, Podman). OS-level isolation, not language-level. A containerized Node script can do anything inside the container — and the container can do anything its cap_add and bind mounts allow. This works at a different layer than Deno: Deno restricts what your code calls; the container restricts what the process can touch on the host. They compose well. Putting a Deno script in a minimal scratch container with --cap-drop ALL gives you defense in depth that neither layer provides alone.
Quick decision table:
| Runtime | Default | Granularity | Where the policy lives |
|---|---|---|---|
| Deno | Deny everything | Path, host, env var, command | CLI flags or deno.json |
| Node.js | Allow everything | Path-only (--permission, experimental) | CLI flags |
| Bun | Allow everything | None (no sandbox) | n/a |
| WASI (wasmtime) | Deny everything | Per-fd, per-dir, per-env | CLI flags at module load |
| Docker | OS-level | Capabilities, namespaces, syscalls | Dockerfile, docker run flags |
The practical takeaway: if you’re picking a runtime for untrusted user code, Deno or wasmtime are the only realistic options without dragging in containers. If you’ve already chosen Node or Bun and need isolation, containerize (and accept the heavier ops cost). If you’re hitting PermissionDenied and considering “just use Node” as a workaround, you’re trading away the only thing in JavaScript that gives you a defensible answer to “what does this dependency have access to?”
One area where this matters in production: serverless runtimes. Cloudflare Workers, Vercel Edge, Deno Deploy, AWS Lambda each have their own slightly different sandbox semantics. Workers uses isolates with no filesystem at all. Lambda runs full Linux but with IAM-scoped network access. Deno Deploy enforces the same --allow-* model as the Deno CLI. When permissions errors appear after deploy but not locally, the deploy target’s sandbox is usually stricter than your dev sandbox. See Fix: Wrangler Not Working for the Cloudflare Workers slice of this problem.
Still Not Working?
Deno 2: NotCapable replaces PermissionDenied
In Deno 2, permission flag violations now raise Deno.errors.NotCapable instead of Deno.errors.PermissionDenied. The error message wording stays the same (“run again with the —allow-read flag”), but if you’re catching errors by type, update your catch blocks:
// Old code (Deno 1):
try {
await Deno.readTextFile("./data.json");
} catch (e) {
if (e instanceof Deno.errors.PermissionDenied) {
console.error("No read permission");
}
}
// Updated for Deno 2:
try {
await Deno.readTextFile("./data.json");
} catch (e) {
if (e instanceof Deno.errors.NotCapable) {
console.error("No read permission");
}
}Deno.errors.PermissionDenied still exists in Deno 2 but is reserved for actual OS-level permission errors (the file exists but the OS user doesn’t have access) — separate from Deno’s security model violations.
--allow-run and Subprocesses with LD_PRELOAD or DYLD_*
Deno 2 added a restriction: if you use a scoped --allow-run=git and the subprocess is launched with LD_PRELOAD or DYLD_* environment variables set, Deno blocks it even if git is in the allow list. You’ll see:
error: Uncaught NotCapable: Requires --allow-all permissions to spawn subprocess with LD_PRELOAD environment variableThe fix is to use unscoped --allow-run (grants subprocess access to any program) or --allow-all. There’s no way to pass a specific LD_PRELOAD subprocess through a scoped --allow-run in Deno 2.
npm Packages That Need Additional Permissions
Most npm packages imported via npm: work without extra permissions. But packages that use native addons (.node files compiled from C++) require --allow-ffi:
deno run --allow-ffi npm:sharp main.tsnpm packages with preinstall/postinstall scripts don’t run by default in Deno. To enable them:
deno run --allow-scripts npm:my-package main.tsWarning: --allow-scripts lets npm lifecycle scripts run arbitrary shell commands. Only use it with packages you trust.
The Prompt Appeared Once but Not Now
By default, Deno prompts you interactively when a permission is needed and stdin is a TTY. If the prompt appeared and you allowed it, that grant is only for the current process — it doesn’t persist between runs.
If you’re running in a CI/CD pipeline and the prompt never appears, it’s because Deno detects a non-TTY environment and treats it as a denial. Add the required flags explicitly to your CI command:
# In CI — no prompt, just fail fast if a flag is missing
deno run --no-prompt --allow-net --allow-env main.ts--allow-hrtime Is Gone in Deno 2
If you’re upgrading from Deno 1, --allow-hrtime no longer exists. Remove it from your commands — it was deprecated in Deno 1.x and removed in Deno 2. High-resolution timing via performance.now() works without it.
deno compile Embeds Permissions at Compile Time
When you compile a Deno script with deno compile, the permission flags you pass become part of the binary. The compiled executable will only ever have those permissions — users can’t add more at runtime.
# Compile with network and env access
deno compile --allow-net=api.example.com --allow-env=API_KEY --output myapp main.ts
# Running the binary — no flags needed, no more can be added
./myappIf you compile without a needed permission and the binary crashes with PermissionDenied, you must recompile with the correct flag. There’s no way to grant additional permissions to an already-compiled Deno binary.
Checking Which Permissions Your Script Actually Needs
If you’re not sure what permissions a script requires, run it with -A first and watch the error messages to discover the minimum set. Once you know what it accesses, replace -A with the specific flags.
Alternatively, use deno info to inspect your script’s module graph and external dependencies before running it:
deno info src/main.tsThis shows all imported modules (including npm packages) so you can reason about what external access they might require before executing anything.
If you’re migrating an existing Node.js project to Deno, the node cannot find module article covers compatibility shims and module resolution differences you’ll likely hit alongside the permission errors.
Permission Errors That Don’t Mention a Flag
Occasionally you’ll see a NotCapable error whose message doesn’t tell you which flag to add. The two common cases are dynamic imports of remote modules (need --allow-import) and Deno.openKv() (needs filesystem access where the KV file is stored, even though it’s “just a database call”). Run the script with OLLAMA_DEBUG-style verbosity by setting DENO_LOG=debug and re-running — the debug log enumerates every capability check Deno performs and which one failed.
Tests Pass Locally but CI Fails With Permission Errors
deno test runs without permissions by default. If a test reads a fixture file, it crashes with NotCapable. Add the flags to your deno.json tasks.test definition rather than passing them ad hoc — that way local and CI share the same allow list. Common failure: a test imports a module that calls Deno.env.get("CI") to detect the CI environment, which itself requires --allow-env=CI.
Granting Permissions to npm Packages Specifically
Inside the npm compatibility layer, packages run with whatever permissions you granted to the top-level script. You can’t grant --allow-net only to one npm package — Deno doesn’t isolate per-import. If a single dependency needs broader access than the rest of your app, the only mitigation is to extract that dependency into a separate Deno subprocess and grant it only the flags it needs. This is the same shape of problem you’d hit with Fix: Bash Permission Denied when subprocess chains drop privileges — but at the runtime layer instead of the OS layer.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: jose JWT Not Working — Token Verification Failing, Invalid Signature, or Key Import Errors
How to fix jose JWT issues — signing and verifying tokens with HS256 and RS256, JWK and JWKS key handling, token expiration, claims validation, and edge runtime compatibility.
Fix: Fastify Not Working — 404, Plugin Encapsulation, and Schema Validation Errors
How to fix Fastify issues — route 404 from plugin encapsulation, reply already sent, FST_ERR_VALIDATION, request.body undefined, @fastify/cors, hooks not running, and TypeScript type inference.
Fix: Vinxi Not Working — Dev Server Not Starting, Routes Not Matching, or Build Failing
How to fix Vinxi server framework issues — app configuration, routers, server functions, middleware, static assets, and deployment to different platforms.
Fix: Better Auth Not Working — Login Failing, Session Null, or OAuth Callback Error
How to fix Better Auth issues — server and client setup, email/password and OAuth providers, session management, middleware protection, database adapters, and plugin configuration.