Tracing permissions
esrun grants nothing by default, so a deployment has to name what it may reach. The hard part was never the flags — it is knowing which ones you need, and esdev grants everything, so the dev loop never tells you. Run the program once under a trace:
esdev --trace-permissions app.mjs
esdev: the permissions this run used read fs_read imports import net fetch env process_env esrun --allow-read --allow-imports --allow-net --allow-env app.mjs
That last line is the one to deploy with.
It reports what was used, not what was granted
The trace watches the capability check itself — the only place that knows what a program reached for, as opposed to what it was handed. So a run under esdev's full grant still tells you the minimum esrun needs.
Refusals are reported too, and left out of the line:
esdev --trace-permissions --deny-all app.mjs
imports import (denied — the program asked and was refused) esrun app.mjs
The program asked and did not get it; whether that was the right answer is your call, not a trace's. The report is printed however the run ended — including a process.exit() and a ^C drain, which for a server is every run.
Workers are in the same report
A worker runs on its own thread in its own isolate, and its grants are set at the spawn — the place they are hardest to get right. Anything it reaches for appears in the report alongside the main agent's.
The line grants each capability unnarrowed. Narrowing it — --allow-read=./data, --allow-net=api.example.com — is still yours, and a scope is enforced provider-side rather than at the capability bit. See Securing the runtime.
Then shorten it further
Bundling removes a capability outright: an unbundled program needs --allow-imports so the loader can walk node_modules, and a bundle has no imports left to resolve.
esdev build app.mjs esrun --allow-read --allow-net --allow-env dist/app.js