Skip to content

Fix: Deno PermissionDenied — Missing --allow-read, --allow-net, and Other Flags

FixDevs ·

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:1

Or 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:1

Or 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:1

In 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 flag

All 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.

The permission system covers eight areas:

FlagWhat it controls
--allow-readFile system reads
--allow-writeFile system writes
--allow-netNetwork access (TCP, fetch, WebSocket)
--allow-envReading environment variables
--allow-runSpawning subprocesses
--allow-sysSystem info (hostname, memory, OS)
--allow-ffiNative library loading
--allow-importRemote 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.ts

Network access:

deno run --allow-net main.ts

Environment variables:

deno run --allow-env main.ts

Multiple permissions at once:

deno run --allow-read --allow-net --allow-env main.ts

Grant all permissions (for development or trusted scripts):

deno run --allow-all main.ts
# or the shorthand:
deno run -A main.ts

The 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.ts

Restrict 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.ts

Restrict environment variable access:

# Only allow reading DATABASE_URL and NODE_ENV
deno run --allow-env=DATABASE_URL,NODE_ENV main.ts

Restrict subprocess execution:

# Only allow running git and curl
deno run --allow-run=git,curl main.ts

Note: --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 test

This 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=test

You 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 fail

The 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.ts

Note: 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.

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 variable

The 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.ts

npm packages with preinstall/postinstall scripts don’t run by default in Deno. To enable them:

deno run --allow-scripts npm:my-package main.ts

Warning: --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
./myapp

If 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.ts

This 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.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles