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=localhostAt 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:
| Capability | Flag | When 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,B | No error: process.env just looks empty (or only shows granted names) |
| Child process | --allow-child-process | execSync 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")(orcatchonfetch), or the failure is easy to miss. --allow-child-processgives up much of the sandbox. The child is a normal OS process:echo,cat ~/.ssh/id_rsa,curlall run with your user’s rights, outside 3va’s checks. Grant it only to scripts you trust.--allow-envwithout 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 3vashebang, so afterchmod +x demo.jsyou can run./demo.jsdirectly (3va ./script.jsis treated as3va run). Run like that, it gets no permissions at all, because a shebang line can’t carry--allow-*flags portably. Use3va 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 inpackage.json. Full reference: Permissions.