Fleet

Sandbox on macOS

Configure Seatbelt and check macOS isolation, resource and process-cleanup limits.

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.

The current Seatbelt profile and installed-plugin sandboxes have not completed native validation on macOS. Check your node before relying on it for sandboxed work.macOS sandboxes have no memory, process or CPU limits and no separate user per plugin. Intel Macs remain unvalidated. Processes that leave their original process group can survive a session.

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 writesits job's directories (the working directory, a harness's state), and /dev/null
Files it readsDetails
Networkthe 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 socketsnone: not Docker's, not ssh-agent's, not the node's, not one in its own working directory
Hard links and clonesnone: 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 servicesa 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
TerminalsDetails
Descriptorsits 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_PROXY and 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 both 127.0.0.1 and ::1, but not the ports of its own services a session reaches (the LLM proxy), which listen at 127.0.0.1 only. The profile's localhost lets the sandbox connect to those ports at ::1 too, so another program on the Mac that listened at ::1 on such a port would be reachable from the sandbox while the session runs.
  • IPv4 inside IPv6 on loopback. Seatbelt matches 127.0.0.1 and ::1 but 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. trustd may 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 refuses com.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.

Copyright © 2026