Sandbox on macOS
For: cluster operators.
Seatbelt restricts file and network access, with different resource limits from Linux. Check the validation and isolation limits below before relying on a Mac for sandboxed work.
A macOS node uses Seatbelt (/usr/bin/sandbox-exec) for sandbox tier 1. When the node can start that profile, harness sessions and jobs started by outside events use it. An installed plugin's servers use it in the default --plugin-sandbox strict mode; auto leaves them outside a sandbox.
What a sandboxed process may do
oz0-node sandbox check says whether the Mac can run it: it runs /usr/bin/true under the same profile a session gets.
oz0-node sandbox check
# Sandbox: tier 1 (seatbelt)
The profile denies everything and then allows:
| Allowed | |
|---|---|
| Files it writes | its job's directories (the working directory, a harness's state), and /dev/null |
| Files it reads | Details |
| Network | the node's egress proxy, on a loopback port of its own, and the node's ports a session needs (the LLM proxy). Nothing else, so a process that ignores HTTPS_PROXY cannot connect. A profile can name loopback only as localhost, which Seatbelt matches at both 127.0.0.1 and ::1, so the node holds the proxy's port at both: no other program can listen there for the sandbox to reach |
| Unix sockets | none: not Docker's, not ssh-agent's, not the node's, not one in its own working directory |
| Hard links and clones | none: Seatbelt checks link and clonefile as operations of their own, which no rule for reading or writing covers, so the profile denies both by name; a process cannot give a file it may not read (the node's key, say) a second name or a copy in a folder it may. git, uv, pnpm and cargo copy where they cannot link or clone |
| macOS services | a short list by name (user lookups, logging, certificate checks, the system's configuration); never LaunchServices (open, which would start apps outside the sandbox) or the pasteboard |
| Terminals | Details |
| Descriptors | its standard streams only: a descriptor the node got without close-on-exec, from whatever started it, is marked close-on-exec before the command starts, since Seatbelt checks a file when it is opened and not when a descriptor someone else opened is read |
The egress proxy allows the hosts on the tenant's, the plugin's and the agent's lists, as on Linux.
How denials are reported
A refused operation fails with "Operation not permitted" and Seatbelt itself says nothing to the process. The node reads Seatbelt's denials back from the macOS unified log: each run's profile tags its denials with a random marker of its own, and the node follows the log (log stream) for that marker while the process runs. Then:
- A connection around the proxy (a program that ignores
HTTPS_PROXYand connects to an address itself) is reported as an egress denial of that host and port, with the reason "a direct connection, not through the egress proxy", in the job's live stream and on the job, like a host the proxy refused. Where macOS logs only the port of a refused connection (network-outbound remote:*:443), the denial names the host*: the log does not say which address it was. - Other denials (a write outside the job's directories, a read of a file it may not read, a Unix socket, a service): at the end of the command the node appends a short list to its error output, so that the person or agent reading it sees why it failed:
oz0 sandbox: Seatbelt denied 2 operation(s) of this command: file-write-create /Users/me/notes.txt (touch) network-outbound 93.184.216.34:443 (curl) (connections go through the egress proxy in HTTPS_PROXY, to the hosts on the egress allowlist)
Each operation is reported once per run, at most 100 of them; past that the list says the rest are not reported. Lookups of DNS through mDNSResponder are left out of the list: the egress proxy resolves names for the process.
So is what macOS's own programs try on every run and do without, which stays refused: a shell's write to the terminal (a sandboxed command's output goes where the node sends it), and CoreFoundation's reads of the account's text encoding (~/.CFUserTextEncoding) and global preferences (~/Library/Preferences/.GlobalPreferences*.plist), which are the account's files. The violations file (sandbox run --violations) keeps every denial, these too.
Reading the unified log needs an administrator: log stream refuses other users. A node that runs as a user who is not one still sandboxes, but without these reports, and the command's error output starts with a line that says so (oz0 sandbox: Seatbelt's denials are not reported for this run: ...). The install script's launch daemon runs as the user who ran sudo, who is one.
How it differs from Linux
- No network namespace. On Linux a sandbox has an empty network of its own; on macOS the profile decides each connection. The result is the same (only the proxy and the node's own ports), but it rests on the profile.
- The node's own ports at
::1. The node holds the egress proxy's port at both127.0.0.1and::1, but not the ports of its own services a session reaches (the LLM proxy), which listen at127.0.0.1only. The profile'slocalhostlets the sandbox connect to those ports at::1too, so another program on the Mac that listened at::1on such a port would be reachable from the sandbox while the session runs. - IPv4 inside IPv6 on loopback. Seatbelt matches
127.0.0.1and::1but not::ffff:127.0.0.1. A runtime that opens dual-stack sockets (Java by default) cannot reach the proxy until it uses IPv4: set-Djava.net.preferIPv4Stack=true. - Certificate checks go through
trustd. Programs that verify TLS with the system (Go programs, Swift, Security.framework) need it, so the profile allows it.trustdmay fetch a certificate's issuer and revocation URLs from outside the sandbox, which a hostile program could use to send a request to a host that is not on its list. Linux has no such service. This is a known gap.
The profile also refusescom.apple.TrustEvaluationAgent, which curl requests. Whether real-certificate validation needs that service remains unvalidated. Check certificate validation with the programs your jobs use before relying on HTTPS from a Mac sandbox. - No limits. Memory, process and CPU limits are Linux cgroups; a Mac's sandbox has none.
- No process namespace. A sandboxed process can signal and inspect only its own processes, but it sees that others exist.
- The unified log may be readable. The profile lets a process use the system's logging services, so on a node that runs as an administrator a process might read the Mac's log, other jobs' denials included. Direct access through those services remains unvalidated; do not assume all log reads are blocked. A sandboxed process cannot add denials to a job this way because only the kernel's denials are read.
Validation limits
Native validation is incomplete for the current node-data profile, installed-plugin servers and Intel Macs. File and service denials appear in the command's error output and its violations file; direct-network denials also appear on the job. Reporting through the unified log needs an administrator account.
Use oz0-node sandbox check on each Mac. Its successful probe shows that the profile can start a command; it does not establish every security property. The limits below apply even when the node reports tier 1.
Files the sandbox reads
The sandbox can read the system (/usr, /System, /etc, Homebrew in /opt/homebrew, Xcode, and of /Library only Apple, Developer/CommandLineTools, Frameworks, Java, Fonts, TeX, Preferences and Managed Preferences), its plugin, the node's managed Python and tools, and its own directories.
Nothing else of the node: its data directory (a system service's in /Library/Application Support/orchestrator-zero-node, or a user's) is denied after every allow but for the folders in it a process is given (the Python, the tools, its plugin, and its own directories where they lie there), which the denies leave out, and never identity, which holds the node's key; not other plugins or other jobs, and nothing in your home folder.
A sandbox run asked to read the data directory whole, or its identity, is refused.
Terminal restrictions
The sandbox can use only the standard streams it was given: no ioctl on /dev/tty or on a terminal device, and no opening either to read. A command run from a person's terminal cannot push keystrokes into it for that person's shell to run once the sandbox ends, or read what is typed there.
Processes after a session
macOS has no process namespace. sandbox run ends when its command ends. At session shutdown the node stops the original process group, including children that stayed in it.
A process that moves to another session or process group can survive. Seatbelt still restricts it, but it can write the job's working directory and connect to the loopback ports its profile allowed. It cannot use a later session's egress proxy, which requires that session's token.
This remains unresolved. Use Linux for work that requires all child processes to end with the sandbox. After an interrupted macOS session, check for remaining processes before reusing or removing its working directory.
Previous section links
Previous validation section: Read the validation limits.
Escape matrix checks: Read the validation limits and process cleanup limitation.