Skip to content

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.

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.

macOSLinux
MechanismSeatbelt (sandbox-exec)Bubblewrap in an unprivileged user namespace
Needsnothing beyond the OSbwrap, iptables, unprivileged user namespaces
Privilegenonenone
Optionalnothing; an approved system extension adds transparent capture—

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.

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=fallback asked 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.

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.

This is the real difference. All three gate against the same rules.

GrantmacOS, capturemacOS, fallbackLinux
allow_http (HTTP/HTTPS)transparentHTTP_PROXY/HTTPS_PROXY — near-universal client supporttransparent
allow_tcptransparent, any clientALL_PROXY (SOCKS5) — many database clients do not speak SOCKStransparent, any client
allow_udpconnected sockets, QUIC included; see belownot available — UDP is deniedtransparent where the kernel provides xt_TPROXY
exposea bind permission, so * costs nothingsamea real relay per port, so ports must be named
IPv6transparent, gated per address like IPv4as IPv4 — the proxy variables carry eithertransparent, 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.

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:

macOSLinux
Mechanismscrapes Seatbelt denials from log streamseccomp-unotify filter in the sandboxed child
Seeswhat the sandbox deniedevery 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.