Platform Support
One policy, two backends today. They answer the same allow/deny questions; they differ only in how much traffic each can route into the gate without the client cooperating.
Requirements
Section titled “Requirements”Neither backend needs root, a daemon, or a kernel module. macOS captures
transparently once Wardcell.app is installed, and falls back to a
proxy-variable mode on a Mac where it is not.
| macOS | Linux | |
|---|---|---|
| Mechanism | Seatbelt (sandbox-exec) | Bubblewrap in an unprivileged user namespace |
| Needs | nothing beyond the OS | bwrap, iptables, unprivileged user namespaces |
| Privilege | none | none |
| Optional | nothing; an approved system extension adds transparent capture | — |
How each confines
Section titled “How each confines”macOS — by denial. The rendered profile is (deny default) plus the
grants your policy makes, and exactly one network rule: outbound to the
proxy’s address. Everything else is refused by the kernel.
Linux — by topology. The sandbox runs in a network namespace with no route to anything. A dummy interface exists only so the kernel has somewhere to send packets, which is what makes the redirect fire. The single channel out is a bind-mounted Unix socket served by wardcell on the host. If that socket is gone, there is no network at all.
Two modes on macOS
Section titled “Two modes on macOS”Transparent capture is the default. Install Wardcell.app and approve its
system extension once, and every run is captured with nothing set: the
extension hands a sandboxed process’s flows to wardcell directly, so a client
that ignores HTTP_PROXY is intercepted anyway.
The fallback reaches the proxy through the standard environment variables instead, and needs nothing installed. A run takes it in exactly two cases:
- nothing answered the extension’s control endpoint, which settles that none is installed; or
WARDCELL_CAPTURE=fallbackasked for it.
An extension that is installed and is not admitting runs refuses the run instead. Dropping silently to a weaker floor because the stronger one is broken is how a fault gets absorbed for hours — the run believes it is captured and nothing says otherwise — so wardcell will not do it.
Both are real sandboxes with the same profile and the same policy. What differs is how much traffic reaches the gate without the client cooperating.
What capture does not stop
Section titled “What capture does not stop”A captured sandbox’s DNS lookups are relayed to the machine’s resolver unexamined. macOS resolves every process’s names through one shared daemon, so refusing a query on the sandbox’s behalf poisons a cache the rest of the Mac reads — measured at 45–80 seconds of machine-wide unreachability for that name. Relaying is the lesser harm, and it leaves a real gap: a process that encodes data into the names it looks up has a low-bandwidth channel out, whatever the policy says. Nothing else about the policy is weakened — a connection to the resolved address still has to be allowed — and closing the gap is planned.
Network coverage
Section titled “Network coverage”This is the real difference. All three gate against the same rules.
| Grant | macOS, capture | macOS, fallback | Linux |
|---|---|---|---|
allow_http (HTTP/HTTPS) | transparent | HTTP_PROXY/HTTPS_PROXY — near-universal client support | transparent |
allow_tcp | transparent, any client | ALL_PROXY (SOCKS5) — many database clients do not speak SOCKS | transparent, any client |
allow_udp | connected sockets, QUIC included; see below | not available — UDP is denied | transparent where the kernel provides xt_TPROXY |
expose | a bind permission, so * costs nothing | same | a real relay per port, so ports must be named |
| IPv6 | transparent, gated per address like IPv4 | as IPv4 — the proxy variables carry either | transparent, where the kernel can carry it; refused immediately and reported where it cannot |
Both macOS failures are safe. A client that ignores the proxy variables loses egress; a flow wardcell cannot name is matched on its address, and an address no rule mentions is denied. Neither escapes.
How a flow gets its name — and when it doesn’t
Section titled “How a flow gets its name — and when it doesn’t”A policy says allow_http "api.anthropic.com"; a program connects to an
address. Something has to join the two, and the platforms do it differently.
Linux answers the sandbox’s DNS itself. Each run has its own resolver, so
an allowed name gets an address minted for that name alone, and a later
connection to it means exactly one host. Nothing is guessed. Both A and
AAAA are answered, from one allocation per name, so a client that prefers
IPv6 reaches the same host under the same rule as one that prefers IPv4.
macOS reads the name the OS attached to the flow. Every process on a Mac resolves through one shared daemon, so wardcell cannot answer a sandbox’s lookups without answering them for the whole machine. Instead it uses the name macOS records when it resolved the address — which a program cannot fake, and which is present even when the answer came from a cache. A name that matches a rule is then re-resolved by wardcell before anything is dialled, so the address the sandbox was handed is never trusted.
The one case this does not cover, and it is worth knowing about:
Lookups are handled differently on the two platforms, and it is worth knowing which you are on.
Linux gates them. A query for a name your policy does not allow is refused, so data cannot be smuggled out inside subdomain labels.
macOS capture relays them. Every lookup reaches your machine’s resolver, and it is the connection that gets refused. Resolving a name reaches nothing by itself, and refusing lookups there is expensive: the whole machine shares one resolver, so a refusal wardcell invents makes that name unresolvable for every other process for about a minute.
Filesystem coverage
Section titled “Filesystem coverage”Equivalent on both, by opposite means: macOS denies by rule with the real
filesystem intact, Linux mounts only what is granted. Either way an ungranted
path cannot be read, and an ungranted write is refused rather than quietly
accepted — on Linux the space between grants is mounted read-only, so the write
fails with Read-only file system.
policy draft’s learn mode works on both too, by different means — and the
Linux one sees more:
| macOS | Linux | |
|---|---|---|
| Mechanism | scrapes Seatbelt denials from log stream | seccomp-unotify filter in the sandboxed child |
| Sees | what the sandbox denied | every attempt: opening, executing, connecting, creating, removing, renaming, changing a file in place, and stat-only lookups |
The Linux tracer observes syscalls as they are made, so it reports what a program reached for whether or not policy would have refused it. The full watched list is in the Linux reference.
One difference is worth planning around: on macOS, draft one run at a time. The denial log is the machine’s, not the run’s, so a draft has to work out which lines are its own — and a process the kernel has already killed cannot be asked. Two drafts at once can each pick up the other’s grants. Linux watches its own sandbox directly and has no such limit. Details.