The Policy System

Every .melker file declares what it needs. The engine enforces it. This page explains how permissions work, how they're sandboxed, and how you stay in control.

1. What is a policy?

A JSON block that says exactly what the app can do.

Every .melker file contains a <policy> block at the top. It declares the app's name, description, and the permissions it requires. The engine reads this block before any code runs.

rss-reader.melker
<policy>
{
  "name": "RSS Reader",
  "description": "Read news from multiple feeds",
  "permissions": {
    "net": ["news.ycombinator.com", "feeds.bbci.co.uk"],
    "browser": true
  }
}
</policy>

This RSS reader can fetch from two specific hosts and open links in your browser. It cannot read files, run commands, or contact any other server. If it tries, the request fails immediately.

The policy is a contract between the app developer and you. You can read it before running the app, just like you'd inspect a web page's permissions. The key difference from traditional terminal programs: everything not declared is denied.

No policy? Local files without a <policy> tag get a minimal auto-policy: read access to the current directory and clipboard support. No network, no writes, no subprocesses. Remote files (URLs) must always include a policy.

Reference: .melker file format

2. How permissions are enforced

Runtime-level sandboxing, not application-level checks.

When you run melker app.melker, three things happen in sequence:

Three-process pipeline
1. CLI          parses the command, loads the file
2. Launcher     reads the policy, shows the approval prompt
3. Runner       executes the app in a sandboxed subprocess

The launcher converts the policy into Deno's --allow-* permission flags and spawns the runner as a restricted subprocess. This is not a JavaScript-level sandbox that can be bypassed with clever code. Deno enforces permissions at the runtime level. If the app tries something it wasn't granted, the call throws a PermissionDenied error.

What the launcher actually spawns
deno run \
  --allow-read=/tmp/...,/home/user/project \
  --allow-net=earthquake.usgs.gov \
  --no-prompt \
  melker-runner.ts app.melker

The --no-prompt flag is critical. It means the sandboxed app cannot interactively ask for new permissions. Violations fail immediately instead of presenting a "do you want to allow?" dialog.

Auditable launcher: The launcher process loads ~18 source files. No framework code, no rendering engine, no components. If you want to verify what happens before your app runs, the surface area is small enough to read.

Node.js limitations

The experimental melker-node binary uses Node's --permission model, which is less granular than Deno's. The same policy syntax works, but enforcement differs in several ways:

CapabilityDenoNode.js
Network filteringPer-host (--allow-net=host)Binary on/off (--allow-net)
Subprocess filteringPer-command (--allow-run=cmd)Binary on/off (--allow-child-process)
Environment variablesPer-variable (--allow-env=KEY)Not restricted
System infoPer-interface (--allow-sys=hostname)Not restricted
Deny overrides--deny-* flagsNot available
FilesystemExact pathsDirectory globs (path/*)
What this means in practice: If a Node.js app declares "net": ["api.example.com"], the sandbox allows all network access, not just that host. Similarly, "run": ["ffmpeg"] allows any subprocess. The policy still documents intent, but Node cannot enforce it at the same granularity. Deno remains the recommended runtime for stronger sandboxing.

Reference: Policy architecture

3. Permission types

Array permissions for granular control, boolean shortcuts for common patterns.

Array permissions

These accept a list of allowed values. Each maps directly to a Deno --allow-* flag.

PermissionControlsExample
netNetwork access by hostname["api.example.com"]
readFilesystem read paths[".", "/data"]
writeFilesystem write paths["./output"]
runSubprocess commands["ffmpeg", "git"]
envEnvironment variables["API_KEY"]
ffiNative library paths["/lib/native.so"]
sysSystem info interfaces["hostname", "uid"]

Use ["*"] as a wildcard to allow all values for a given permission.

Boolean shortcuts

Common permission patterns have shortcuts that expand into the right combination of net and run permissions. The expansion is smart: it checks which commands actually exist on your system.

ShortcutWhat it grants
mapNetwork access to OpenStreetMap, CartoDB, and other tile servers
shaderPer-pixel shader callbacks on canvas elements (runtime-only, no Deno flag)
clipboardrun access to clipboard commands (xclip, pbcopy, etc.)
browserrun access to open / xdg-open for opening URLs
airun + net for AI assistant tools (audio, API access)
keyringrun access to OS keychain commands
allEverything (equivalent to --allow-all)

Special values

ValueWhereMeaning
"*"Any arrayWildcard: allow all
"cwd"read, writeExpands to the current working directory at runtime
"samesite"netExpands to the host the app was loaded from (remote apps)
A policy using multiple permission types
{
  "permissions": {
    "net": ["api.example.com", "samesite"],
    "read": ["cwd"],
    "write": ["./output"],
    "run": ["ffmpeg"],
    "env": ["API_KEY"],
    "clipboard": true,
    "browser": true
  }
}

4. What you get for free

Some permissions are granted automatically so apps don't need to declare boilerplate.

Every app gets a baseline set of permissions regardless of its policy. These cover the minimum needed for the engine itself to work.

Implicit read access

The app can always read its own file, the system temp directory (for bundled code), the Deno module cache, and the Melker state directory. Local apps also get read access to the current working directory.

Implicit write access

The system temp directory (for bundler output), the Melker state directory (for persisting app state across runs), and the log directory.

Implicit environment variables

Terminal detection variables (TERM, COLORTERM, KITTY_WINDOW_ID, etc.), path variables (HOME, PATH, TMPDIR), and all MELKER_* and XDG_* variables.

Read vs. write: The current working directory is implicit for read but not for write. If your app needs to write files, you must declare "write": ["cwd"] explicitly. This prevents apps from silently modifying your files.

Reference: Environment permission analysis

5. The approval prompt

You approve an app once. Re-approval is only needed when permissions change.

The first time you run a .melker file, Melker shows a prompt listing the requested permissions. You can approve or decline. Approvals are cached so you won't be asked again.

When does re-approval happen?

Local files: Approval is based on a hash of the policy JSON. You can edit the app's code freely without triggering a new prompt. Only changing the <policy> block requires re-approval.

Remote files (URLs): Approval is based on a hash of the entire file content plus the policy. Any change to a remote app triggers re-approval. This prevents a remote server from silently changing behavior.

Managing approvals

CLI commands
# Show the policy without running the app
melker --show-policy app.melker

# Revoke approval for a specific app
melker --revoke-approval app.melker

# Clear all cached approvals
melker --clear-approvals

# Skip the prompt (CI, scripts, trusted sources)
melker --trust app.melker
What --trust means: It skips the approval prompt but still runs the app with its declared policy permissions. If the app has a <policy> tag, those permissions are applied normally. If there is no declared policy, the default auto-policy is used (read access to the current directory and clipboard).

Reference: CLI reference

6. CLI overrides

Grant or revoke permissions at the command line without editing the app.

You can modify an app's effective permissions when you run it, without touching the .melker file.

Adding permissions
# Give an offline app network access for testing
melker --allow-net=api.example.com app.melker

# Enable the AI assistant
melker --allow-ai app.melker

# Enable shaders on an app that forgot to declare it
melker --allow-shader app.melker
Removing permissions
# Run an app but block clipboard access
melker --deny-clipboard app.melker

# Block a specific host while keeping the rest
melker --deny-net=tracking.example.com app.melker

The precedence is: deny > allow > policy. A --deny flag always wins, even if the policy or a --allow flag grants the same permission.

7. Real-world examples

From zero permissions to full system access, and everything in between.

Mandelbrot explorer - no permissions needed
{
  "name": "Mandelbrot",
  "permissions": {}
}

Pure computation. No network, no files, no subprocesses. The app runs entirely within the sandbox with only implicit permissions.

Earthquake dashboard - network + map tiles
{
  "name": "earthquake-dashboard",
  "permissions": {
    "net": ["earthquake.usgs.gov", "raw.githubusercontent.com"],
    "map": true
  }
}

Fetches earthquake data from USGS and tectonic plate boundaries from GitHub. map: true adds the tile server hosts needed for the interactive map.

Color selector - AI + clipboard
{
  "name": "Color Selector",
  "permissions": {
    "ai": ["*"],
    "clipboard": true
  }
}

Uses the AI assistant to suggest color palettes and copies selected colors to the clipboard. ai expands into the specific run and net permissions needed for AI tools.

htop clone - full access with a comment explaining why
{
  "name": "htop",
  "permissions": {
    "all": true
  },
  "comment": "Linux reads /proc, macOS uses sysctl/ps. Needs full access."
}

A system monitor needs to read /proc, run system commands, and access environment variables. The comment field appears in the approval prompt so the user understands why full access is requested.

See more examples: How Melker Works