What this usually means
Deno enforces a sandbox that denies all I/O operations by default. Every file read, write, network access, subprocess spawn, or environment variable read requires explicit permission flags at startup or via runtime prompts. The error message always includes the exact resource that was denied (e.g., 'read access to "/home/user/data.txt"'). This is fundamentally different from Node.js where filesystem access is unrestricted. The root cause is almost never a filesystem permissions issue (file modes) but rather that the Deno runtime itself blocked the operation because the corresponding capability flag was not provided.
The first ten minutes — establish facts before touching code.
- 1Run the script with --allow-all to confirm the error is permission-related: deno run --allow-all script.ts
- 2Read the full error message — it always specifies the operation type and resource. Example: 'error: Uncaught PermissionDenied: Requires write access to "/var/data/output.json"'
- 3Check if your script uses dynamic permissions (e.g., Deno.permissions.request()) — these may be failing silently if the user denied the prompt.
- 4Review the command line you used to run the script. Common oversight: forgetting --allow-net when using fetch() or WebSocket.
- 5If running in a CI/CD pipeline, check that the Deno flags are passed correctly in the workflow YAML (e.g., deno run --allow-read --allow-env main.ts).
The specific files, logs, configs, and dashboards that usually own this bug.
- searchYour terminal output: the exact error line includes the resource path and operation type.
- searchScript entry point: the command or Dockerfile CMD that launches Deno.
- searchCI/CD pipeline definition (GitHub Actions, GitLab CI, etc.) for missing permission flags.
- searchAny .env or config files that might be read — check if --allow-env and --allow-read are both present.
- searchIf using Deno.serve() or fetch(), confirm --allow-net is present (and --allow-read for static files).
- searchFor subprocess calls (Deno.run or Deno.Command), ensure --allow-run is included.
- searchThe Deno.permissions.query() output in your code, if you use runtime permission checks.
Practical causes, not theory. These are the things you will actually find.
- warningMissing --allow-read flag when reading local files (e.g., reading a JSON config or database file).
- warningMissing --allow-net flag when making HTTP requests or WebSocket connections.
- warningMissing --allow-write flag when writing logs or output files.
- warningMissing --allow-env flag when accessing environment variables via Deno.env.get().
- warningUsing --allow-read but not --allow-write for a file that needs both read and write (e.g., updating a config file).
- warningRunning script with --no-prompt but without the required flags — Deno will not ask for permission, it will just fail.
- warningInconsistent flags across development and production environments (e.g., --allow-all in dev but restricted in prod).
Concrete fix directions. Pick the one that matches your root cause.
- buildAdd the specific permission flag: for reading a file, use --allow-read=/path/to/file or --allow-read for all files.
- buildUse granular paths: --allow-read=/home/user/data instead of blanket --allow-read to limit scope.
- buildEnable dynamic permissions with Deno.permissions.request() and handle the user's response (grant/deny).
- buildIf using Deno.serve() with TLS, --allow-net is needed even for localhost connections.
- buildCombine flags: deno run --allow-read --allow-write --allow-net script.ts
- buildFor environment variables, use --allow-env (or --allow-env=VAR1,VAR2 for specific vars).
- buildIn Docker, pass flags in the CMD: CMD ["deno", "run", "--allow-read", "--allow-net", "main.ts"]
A fix you cannot prove is a guess. Close the loop.
- verifiedRun the script again with the correct flags — it should no longer throw permission errors.
- verifiedCheck logs or output to confirm the file was read/written successfully (e.g., file exists with expected content).
- verifiedFor network: make a test fetch to the external resource and verify the response is received.
- verifiedUse Deno.permissions.query() in code to programmatically verify that required permissions are granted.
- verifiedAdd console.log() after permission-sensitive operations to confirm they completed without error.
- verifiedIf using dynamic permission prompts, simulate user granting and verify the operation proceeds.
Things that make this bug worse or harder to find.
- warningUsing --allow-all in production — it defeats Deno's security model and opens all resources.
- warningConfusing OS-level file permissions (chmod) with Deno's sandbox — Deno errors are not about file modes.
- warningForgetting to add --allow-write when writing to a file even if --allow-read is present (they are separate).
- warningAssuming that --allow-net also covers DNS resolution — it does, but only if you allow the DNS server (usually it works).
- warningIgnoring the '--no-prompt' flag in CI: without it, Deno will hang waiting for interactive permission prompts.
- warningUsing wildcard paths like --allow-read=/var/* when only a specific directory is needed — too permissive.
- warningNot handling Deno.errors.PermissionDenied in try/catch blocks when using dynamic permissions.
CI Build Fails with Deno Permission Denied for Environment Variable
Timeline
- 09:15Push commit that adds a new feature reading DATABASE_URL from environment.
- 09:17GitHub Actions workflow triggers: deno test --no-prompt --allow-read --allow-net
- 09:18Test fails with: error: Uncaught PermissionDenied: Requires env access to "DATABASE_URL"
- 09:20Developer checks CI logs and sees the missing --allow-env flag.
- 09:22Developer adds --allow-env to the deno test command in the workflow YAML.
- 09:23Re-runs workflow; still fails because the test also reads a config file (needs --allow-read).
- 09:25Developer adds --allow-read=./config.json to the command.
- 09:27Workflow passes; developer also updates the production start command in Dockerfile.
We had a Deno app running for months with CI tests passing fine. Then I added a feature that reads the DATABASE_URL environment variable to connect to PostgreSQL. The tests worked locally because I had --allow-env in my shell alias, but the CI pipeline only had --allow-read and --allow-net. The first CI run after the merge failed with the classic 'Requires env access' error. The error message pointed directly to the missing permission.
I quickly added --allow-env to the CI workflow YAML and re-ran. But then the test failed again — this time it was a different permission: reading a config file that was introduced in the same branch. The error said 'Requires read access to "./config.json"' — I had forgotten that --allow-read was already present but without a path, and the new code read a file that wasn't covered by the existing blanket --allow-read? Actually, I had mistakenly removed the --allow-read flag when adding --allow-env. So I added both: --allow-env and --allow-read=./config.json.
Eventually, the tests passed. But the real lesson was about consistency: we had different permission sets across local development, CI, and production. After that, we defined a single set of permissions in a config file and used Deno.permissions.request() for optional permissions. We also added a CI step that runs with --allow-all but checks that the needed permissions are documented. Now we avoid the 'works on my machine' trap with Deno's permission system.
Root cause
Missing --allow-env flag in CI pipeline when new code required access to DATABASE_URL environment variable.
The fix
Added --allow-env to the deno test command in GitHub Actions workflow YAML. Also ensured --allow-read covered the new config file path.
The lesson
Treat Deno permission flags as part of your infrastructure configuration. Keep them in sync across environments, and use granular paths to avoid over-permissioning. When adding new I/O or env access, always update the deployment scripts first.
Deno's security model is based on capabilities: every I/O operation is denied by default. The runtime checks a set of flags passed at startup or prompts the user interactively (unless --no-prompt is set). There are six main permission categories: read, write, net, env, run, and ffi. Each can be granted globally (e.g., --allow-read) or scoped to specific paths/domains (e.g., --allow-read=/tmp).
The error message format is consistent: 'Requires <operation> access to "<resource>"'. For example, 'Requires read access to "/etc/hosts"'. This tells you exactly which permission flag is missing. The resource string may be a file path, a URL host:port, or an environment variable name. If you see 'Requires env access to "HOME"', you need --allow-env=HOME or --allow-env.
Using --allow-read without a path grants read access to the entire filesystem. This is convenient but dangerous. Instead, Deno allows scoping: --allow-read=/home/user/project gives read access only to that directory tree. Similarly, --allow-net=api.example.com:443 restricts network access to that specific host and port. Scoped permissions are comma-separated and can be repeated.
One common pitfall: --allow-read=/data gives access to /data but not its subdirectories? Actually, it does give recursive access. But be careful with symbolic links: Deno resolves symlinks before checking permissions. So if /data is a symlink to /etc, you would grant read access to /etc. Always test with a minimal set of paths.
Deno also supports requesting permissions at runtime via Deno.permissions.request(). This is useful for CLI tools that need optional access. For example, you can request read access to a file only when the user chooses to import it. The API returns a PermissionStatus object with a 'granted' boolean. If the user denies the prompt (or if --no-prompt is set and the permission isn't already granted), the status will be 'denied'.
However, dynamic permissions have limitations: they cannot grant access to resources that require startup flags like --allow-run or --allow-ffi? Actually, they can request any permission except maybe --allow-run? Deno does support dynamic --allow-run permissions in recent versions. Check the docs for your version. Also, in non-interactive environments (like CI), dynamic requests will always be denied unless the permission is already granted via startup flags.
Use --log-level=debug to see which permissions are being checked and granted. This prints lines like 'perm: check read "/path" -> Denied' or 'perm: check read "/path" -> Granted'. This is invaluable for tracing the exact resource that triggered the error.
You can also use Deno.permissions.query() to programmatically check if a permission is granted. For example: const status = await Deno.permissions.query({name: 'read', path: '/etc/passwd'}); console.log(status.state); // 'granted' or 'denied'. This helps in writing defensive code that gracefully handles missing permissions.
Many developers assume that --allow-read also covers writing, but read and write are separate. Similarly, --allow-net does not imply --allow-env. Another misconception is that file paths are relative to the current working directory — they are, but Deno resolves them to absolute paths before checking permissions. So --allow-read=./data is equivalent to --allow-read=/absolute/path/to/data.
Some think that Deno's permission errors are the same as OS-level 'Permission denied' errors. They are not. OS errors come from the kernel (e.g., file not owned by user, wrong file mode). Deno errors come from the runtime's sandbox. The error message always says 'PermissionDenied' with a description that starts with 'Requires'. If you see a plain 'Permission denied' (no 'Requires'), it might actually be an OS error — check the file's permissions with ls -la.
Frequently asked questions
How do I give Deno permission to read a file but not write to it?
Use --allow-read=/path/to/file without --allow-write. Deno's read and write permissions are independent. If you need to both read and write the same file, you need both flags: --allow-read=/path/to/file --allow-write=/path/to/file.
Can I use environment variables to pass Deno permissions?
No. Deno permissions must be specified as command-line flags. There is no environment variable that can grant permissions. However, you can use a shell script or Makefile to define the flags and reuse them across environments.
Why does my fetch() fail with 'TypeError: fetch failed' even with --allow-net?
This error usually means the network request itself failed (e.g., DNS resolution, connection timeout, TLS error), not a permission issue. Check the network connectivity and the URL. If you see 'Requires net access' in the error, that's a permission issue. Otherwise, it's a network error — use try/catch and inspect the error details.
How do I run Deno with permissions only for specific environment variables?
Use --allow-env=VAR1,VAR2 where VAR1 and VAR2 are the variable names. For example, --allow-env=HOME,PATH grants access only to those two variables. Without the list, --allow-env grants access to all environment variables.
What is the difference between --no-prompt and --allow-all?
--no-prompt disables interactive permission prompts; if a required permission is missing, the script will fail with an error instead of asking. --allow-all grants all permissions (read, write, net, env, run, ffi). They are often used together in CI to avoid prompts, but --allow-all is too permissive for production. The recommended approach is to specify only the needed permissions and use --no-prompt to fail fast if something is missing.