Inbound Ports
By default nothing outside can reach a process inside. expose opens a
host-loopback port and relays it in.
net { expose ["3000", "127.0.0.1:8080"]}Both forms mean the same thing; the address half is accepted only for
explicitness and must be loopback (127.0.0.1 or [::1]). 0.0.0.0 is an
error — expose never opens a port to your network.
Named ports vs *
Section titled “Named ports vs *”expose ["*"]means different things on each platform, deliberately:
| macOS | a bind permission. “Any port” costs nothing, so * grants everything. |
| Linux | the sandbox has its own loopback, so each port needs a real relay. There is no sane reading of “listen on all 65535”, so * enumerates nothing. |
So * grants everything macOS can grant and nothing Linux has to enumerate.
A port you need reachable on Linux must be named. policy explain reports
the difference.
Example: a dev server
Section titled “Example: a dev server”fragment "dev-server" { selects_on = { commands = ["npm", "vite"] }
net { expose ["5173"] # named, so it works on both platforms }}wardcell run -- npm run dev# then open http://localhost:5173 in your browserNot this: reaching a loopback service
Section titled “Not this: reaching a loopback service”expose is only the inbound direction — something outside reaching a listener
inside the sandbox. Two related needs are documented elsewhere:
- A sandbox process calling another process in the same sandbox over
localhostneeds nothing for TCP — it works on any port, noexposeand no grant. UDP on loopback is still gated, so a UDP helper needs anallow_udp ["127.0.0.1:PORT"]grant. See macOS and Linux. - A sandbox calling a service on your machine’s loopback is an outbound
grant. The portable way is the reserved
host.internal—allow_tcp ["host.internal:5432"]— which reaches the host on both platforms. A literal127.0.0.1grant also works on macOS, but on Linux names only the sandbox’s own loopback. See the platform pages for the distinction.