Skip to Content
Examples10 · Permissions

10 · Permissions

One script tries five capabilities: reading a file, writing a file, reading environment variables, spawning a child process, and making an outbound HTTP request. It reports which ones got through. Run it bare first, then add grants.

Source: examples/10-permissions

Run with nothing granted

cd examples/10-permissions 3va run demo.js
=== 3va deny-by-default permission demo === [DENIED] read file -> EACCES [DENIED] write file -> EACCES [ok] env vars visible: (none) [DENIED] child process -> Process spawn denied. Run with --allow-child-process … [DENIED] network fetch -> no --allow-net grant [!] 4 permissions were denied during this run: read ./secret.txt grant with: --allow-read=./secret.txt write ./output.txt grant with: --allow-write=./output.txt spawn a child process grant with: --allow-child-process connect to localhost grant with: --allow-net=localhost

At the end, 3va lists every refusal with the exact flag that would allow it. The environment check isn’t in the list because the demo enumerates process.env (Object.keys), which just returns {}. Reading a variable by name (process.env.HOME) is reported.

Run with everything granted

3va run demo.js \ --allow-read=./secret.txt \ --allow-write=./output.txt \ --allow-net=localhost \ --allow-env \ --allow-child-process
[ok] read file: The demo read me — grant --allow-read=./secret.txt to see this. [ok] wrote file: ./output.txt [ok] env vars visible: SHELL, SESSION_MANAGER, … [ok] child process -> I am a child process … [3va-tcp] FAILED localhost:9999 => Connection refused (os error 111) [ok] network fetch (permission granted — ECONNREFUSED, no server on :9999)

The network check succeeds even though the connection fails. Nothing listens on port 9999, but the request was allowed out.

How each denial surfaces

This is the most useful thing the example teaches. A denial doesn’t look the same for every API:

CapabilityFlagWhen denied
File read--allow-read=<path>fs throws, err.code === "EACCES"
File write--allow-write=<path>fs throws, err.code === "EACCES"
Environment--allow-env or --allow-env=A,BNo error: process.env just looks empty (or only shows granted names)
Child process--allow-child-processexecSync throws Process spawn denied. Run with --allow-child-process
Network--allow-net=<host>async error event whose message mentions allow-net

--allow-env=HOME narrows it down: the script then sees exactly HOME and nothing else.

Things to keep in mind

  • An empty environment isn’t an error. Reads by name show up in the denial summary, but code like process.env.DATABASE_URL ?? "sqlite://local.db" silently takes the fallback when the grant is missing. If a variable is required, check it and fail loudly.
  • Denied network calls don’t throw. They fail asynchronously. Always attach .on("error") (or catch on fetch), or the failure is easy to miss.
  • --allow-child-process gives up much of the sandbox. The child is a normal OS process: echo, cat ~/.ssh/id_rsa, curl all run with your user’s rights, outside 3va’s checks. Grant it only to scripts you trust.
  • --allow-env without a list exposes every variable, including tokens in your shell. Prefer --allow-env=NAME1,NAME2.
  • The [3va-tcp] FAILED … line comes from the runtime, not from the script. It’s a diagnostic printed on a failed connection.
  • The file has a #!/usr/bin/env 3va shebang, so after chmod +x demo.js you can run ./demo.js directly (3va ./script.js is treated as 3va run). Run like that, it gets no permissions at all, because a shebang line can’t carry --allow-* flags portably. Use 3va run demo.js --allow-… when the script needs grants.
  • To go from denied to minimal grants without trial and error, use 3va permissions learn demo.js --write, which records them in package.json. Full reference: Permissions.