How Deno's Permission System Works: CLI Flags, Runtime Checks, and the JavaScript API
Deno isolates script execution by requiring explicit permission grants for privileged actions, enforcing security through a three-layer architecture that spans command-line parsing, a centralized Rust permission store, and a JavaScript query API.
The denoland/deno repository implements a secure-by-default runtime where file system access, network requests, and environment variable reads are denied unless explicitly granted. Unlike Node.js, which provides unrestricted access to system resources, Deno's permission system ensures that every side-effecting capability is gate-kept by a central mechanism. This article examines the source code implementation, from CLI argument parsing in cli/args/flags.rs to the runtime permission checks in runtime/permissions.rs.
The Three-Layer Permission Architecture
Deno's permission model operates through distinct layers that translate user intent into enforced security boundaries.
CLI Flag Parsing and Initial State
When a Deno process starts, the command-line parser in cli/args/flags.rs processes all --allow-* flags (such as --allow-read, --allow-net, and --allow-env). These flags define the initial Permissions state seeded to the runtime. If a flag is omitted, the corresponding permission is denied by default, establishing the secure-by-default posture before any JavaScript code executes.
The Runtime Permission Store
At the core of the system lies the Permissions struct defined in runtime/permissions.rs. This struct maintains the state of each permission as granted, denied, or prompt, and implements the public API exposed to JavaScript. The permission store handles complex logic including:
- Tracking granular permissions (specific paths for
--allow-read, host patterns for--allow-net) - Managing interactive prompts when the
--promptflag is enabled - Enforcing permission revocation and subsequent re-granting
JavaScript Permission API and Ops Bridge
The Deno.permissions namespace provides the JavaScript façade for the permission system, implemented through ops in runtime/ops/permissions.rs. This layer exposes three primary methods:
query– Check the current state of a permission without consuming user interactionrequest– Attempt to grant a permission, triggering an interactive prompt if necessaryrevoke– Explicitly remove a previously granted permission
These methods bridge JavaScript calls to the underlying Rust Permissions struct, ensuring consistent state across the runtime boundary.
How Privileged Operations Enforce Permissions
When JavaScript code invokes a privileged operation like Deno.readFile or Deno.connect, the corresponding Rust op checks the permission store before executing the system call. For example, file system operations verify permissions through methods such as state.read.allow within the op implementation. If the permission is not granted, the op returns a permission-denied error immediately, preventing unauthorized access.
This enforcement mechanism applies uniformly across all capabilities:
- File system access checks
--allow-readand--allow-writepermissions - Network operations validate
--allow-netgrants - Environment variable access requires
--allow-env - System information queries check
--allow-sys
Working with Deno Permissions in Practice
Granting Permissions via CLI
Pass --allow-* flags when starting the Deno process to pre-grant capabilities:
# Allow read access to specific directories only
deno run --allow-read=/tmp,/home/user/data script.ts
# Allow network connections to specific hosts
deno run --allow-net=example.com,api.example.com server.ts
# Allow all environment variable access
deno run --allow-env script.ts
Querying Permission State at Runtime
Scripts can inspect current permissions without triggering prompts using Deno.permissions.query():
// script.ts
const status = await Deno.permissions.query({ name: "net" });
console.log(status.state); // "granted", "denied", or "prompt"
if (status.state === "granted") {
const response = await fetch("https://api.example.com/data");
}
Requesting Permissions Dynamically
Use Deno.permissions.request() to prompt for elevated privileges at runtime (requires --prompt flag):
// script.ts
const result = await Deno.permissions.request({ name: "env" });
if (result.state === "granted") {
console.log("HOME directory:", Deno.env.get("HOME"));
} else {
console.error("Cannot access environment variables");
}
Revoking Previously Granted Permissions
Explicitly remove permissions to reduce the attack surface during sensitive operations:
// script.ts
// Revoke read access to a specific file
await Deno.permissions.revoke({ name: "read", path: "./secret.txt" });
// Revoke all network permissions
await Deno.permissions.revoke({ name: "net" });
Using Interactive Prompts
Enable interactive permission requests by starting Deno with the --prompt flag:
deno run --prompt script.ts
When the script calls Deno.permissions.request(), the runtime displays an interactive terminal prompt asking the user to allow or deny the specific capability.
Key Source Files in the Deno Repository
The permission system spans several critical files in the denoland/deno codebase:
runtime/permissions.rs– Defines thePermissionsstruct, permission-state handling, and the public API used by the JavaScript layer.runtime/ops/permissions.rs– Implements the ops that exposequery,request, andrevoketo JavaScript and validates permission state for other system ops.cli/args/flags.rs– Parses command-line flags (--allow-*,--prompt) and initializes the permission configuration passed to the runtime.runtime/worker.rs– Instantiates a worker with its ownPermissionsinstance, linking the CLI-derived configuration to the runtime environment.
These files collectively implement Deno's fine-grained, explicit permission model, ensuring scripts run with the least privilege required while giving developers full programmatic control over capability grants.
Summary
- Deno's permission system requires explicit opt-in for all privileged operations through CLI flags like
--allow-readand--allow-net. - The
Permissionsstruct inruntime/permissions.rsmaintains the central state of granted, denied, and promptable capabilities. - The JavaScript
Deno.permissionsAPI providesquery(),request(), andrevoke()methods for runtime permission introspection and management. - All privileged Rust ops check the permission store (e.g., via
state.read.allow) before executing system calls, returning permission-denied errors for unauthorized access. - The
--promptflag enables interactive permission requests, allowing scripts to request elevated privileges dynamically during execution.
Frequently Asked Questions
What happens if I run a Deno script without any --allow flags?
If you execute a Deno script without granting permissions via CLI flags, the runtime operates in a restricted sandbox mode. All file system, network, and environment access is denied by default, and any attempt to use these capabilities throws a permission-denied error. You must either pre-grant permissions with --allow-* flags or use the --prompt flag to enable interactive permission requests at runtime.
Can Deno permissions be restricted to specific paths or hosts?
Yes, Deno supports fine-grained permission scoping. You can specify allowed paths when using --allow-read or --allow-write (e.g., --allow-read=/tmp,/home/user/data), and restrict network access to specific hosts with --allow-net=example.com or --allow-net=192.168.1.0/24. These constraints are enforced by the Permissions struct in runtime/permissions.rs.
How do I temporarily revoke a permission after granting it?
Use Deno.permissions.revoke() to remove previously granted permissions programmatically. For example, await Deno.permissions.revoke({ name: "read", path: "./secret.txt" }) removes read access to that specific file. After revocation, subsequent attempts to access the resource will fail until the permission is re-granted either via CLI flags or a new request() call with user approval.
What is the difference between Deno.permissions.query() and Deno.permissions.request()?
Deno.permissions.query() inspects the current permission state without user interaction, returning immediately with "granted", "denied", or "prompt". In contrast, Deno.permissions.request() attempts to acquire the permission and may trigger an interactive terminal prompt (if using --prompt) or return the current state if already determined. Use query() for checks, request() for acquiring new permissions.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →