docs/research/macos-sandbox-exec.md
Bead: gt-6qt Date: 2026-03-08 Blocks: gt-2pb (Spike: macOS sandbox-exec polecat isolation)
YES — fully functional on macOS Sequoia 15.x despite deprecation.
/usr/share/sandbox/ and /System/Library/Sandbox/Profiles/ (100+ profiles)The deprecation is a "please don't use this" signal, not an imminent removal. The kernel-level sandboxing subsystem (Seatbelt/MACF) must remain for Apple's own use.
YES — core capability, works well.
Operations: file-read*, file-write*, file-read-data, file-read-metadata, file-write-create, etc.
Path filters:
(literal "/exact/path") — exact match(subpath "/dir") — dir and all descendants(regex "^/pattern") — POSIX regexExample (deny-default, allow specific):
(version 1)
(deny default)
(allow file-read* (subpath "/usr/lib"))
(allow file-read* (subpath "/System/Library"))
(allow file-read* file-write* (subpath (param "PROJECT_DIR")))
(allow file-write* (subpath "/private/tmp"))
Parameterized paths: sandbox-exec -D PROJECT_DIR=/path -f profile.sb command
Local test confirmed: Reading /etc/passwd denied with exit 134 when only /usr allowed.
YES — works reliably.
Network operations: network*, network-outbound, network-inbound, network-bind
Filters: (local ip "localhost:*"), (remote ip "localhost:*"), (remote unix-socket)
;; Loopback-only profile
(allow network* (local ip "localhost:*"))
(allow network* (remote ip "localhost:*"))
(allow network* (remote unix-socket))
Local test confirmed: curl to external host denied with exit 6 under (deny default).
OpenAI Codex found network enforcement "too effective" — their network_access=true config was silently ignored by seatbelt (GitHub issues #6807, #10390).
YES — via process-exec and process-fork operations.
(allow process-exec (literal "/usr/bin/python3"))
(deny process-exec (literal "/bin/ls"))
Key behaviors:
process-exec controls which binaries can be exec'dprocess-fork controls fork/vfork permission/bin/sh redirects to /bin/bash on macOS, so both must be allowedLocal test confirmed: /bin/ls denied (exit 126) while /bin/echo allowed in same shell session.
YES — confirmed working with Node.js v25.6.0 on this machine.
Required SBPL rules for Node.js:
| Rule | Why |
|---|---|
(allow file-ioctl) | Terminal raw mode / setRawMode |
(allow mach-host*) | os.cpus() / CPU detection |
(allow pseudo-tty) | PTY allocation |
(allow ipc-posix-sem) | Semaphores |
(allow iokit-open) | IOKit access |
Device paths: /dev/ptmx, /dev/ttys* | PTY devices |
/dev/random, /dev/urandom | Crypto/random |
/private/var/folders, /private/tmp | Temp directories |
Known issues WITHOUT these rules:
setRawMode fails with errno:1 (needs file-ioctl)os.cpus() returns empty array (needs mach-host*)ipc-posix-sem)Production users: Claude Code (Anthropic), Codex (OpenAI), ai-jail
Generally NO.
sandbox-exec itself is SIP-protected at /usr/bin/sandbox-exec(version 1)
(deny default) ;; whitelist mode (recommended for security)
(allow default) ;; blacklist mode
(debug deny) ;; log denied operations to system log
(debug all) ;; log all operations (verbose)
(allow|deny operation [filter...])
File: file*, file-read*, file-read-data, file-read-metadata, file-read-xattr, file-write*, file-write-data, file-write-create, file-write-flags, file-write-mode, file-write-mount, file-write-owner, file-write-setugid, file-write-times, file-write-unmount, file-write-xattr, file-ioctl, file-revoke, file-chroot
Network: network*, network-outbound, network-inbound, network-bind
Process: process*, process-exec, process-fork
IPC: ipc*, ipc-posix*, ipc-posix-sem, ipc-posix-shm, ipc-sysv*, ipc-sysv-msg, ipc-sysv-sem, ipc-sysv-shm
Mach: mach*, mach-bootstrap, mach-lookup, mach-priv*, mach-priv-host-port, mach-priv-task-port, mach-task-name, mach-per-user-lookup, mach-host*
System: sysctl*, sysctl-read, sysctl-write, system*, system-acct, system-audit, system-fsctl, system-lcid, system-mac-label, system-nfssvc, system-reboot, system-set-time, system-socket, system-swap, system-write-bootstrap
Other: pseudo-tty, iokit-open, job-creation, process-info*, signal, send-signal
Path filters:
(literal "/exact/path/to/file")
(subpath "/dir") ;; matches /dir and all descendants
(regex "^/pattern/.*\\.txt$")
Network filters:
(local ip "localhost:*")
(remote ip "localhost:80")
(remote unix-socket)
(local tcp "*:8080")
Mach service filters:
(global-name "com.apple.system.logger")
(local-name "com.example.service")
Logical combinators:
(require-all (subpath "/tmp") (require-not (vnode-type SYMLINK)))
(require-any (literal "/path/a") (literal "/path/b"))
Other filters:
(signing-identifier "com.example.app")
(target same-sandbox)
(sysctl-name "kern.hostname")
(deny (with no-report) file-write*) ;; suppress violation log
(deny (with send-signal SIGUSR1) network*)
(allow (with report) sysctl (sysctl-name "...")) ;; log even though allowed
;; CLI: sandbox-exec -D KEY=value -f profile.sb command
(allow file-read* file-write* (subpath (param "PROJECT_DIR")))
;; Conditional logic
(if (equal? (param "FEATURE") "YES")
(allow network-outbound))
(import "/System/Library/Sandbox/Profiles/bsd.sb")
(version 1)
(deny default)
(debug deny)
;; Process control
(allow process-exec)
(allow process-fork)
(allow signal (target same-sandbox))
(allow process-info* (target same-sandbox))
;; System info (Node.js needs these)
(allow sysctl-read)
(allow mach-host*)
(allow mach-lookup)
(allow iokit-open)
(allow ipc-posix-sem)
(allow ipc-posix-shm-read*)
;; Terminal support
(allow file-ioctl)
(allow pseudo-tty)
(allow file-read* file-write* (literal "/dev/ptmx"))
(allow file-read* file-write* (regex "^/dev/ttys[0-9]+"))
;; Standard devices
(allow file-write* (literal "/dev/null"))
(allow file-write* (literal "/dev/zero"))
(allow file-read* (literal "/dev/random"))
(allow file-read* (literal "/dev/urandom"))
;; System read access (read-only)
(allow file-read* (subpath "/usr/lib"))
(allow file-read* (subpath "/usr/bin"))
(allow file-read* (subpath "/usr/sbin"))
(allow file-read* (subpath "/System"))
(allow file-read* (subpath "/Library"))
(allow file-read* (subpath "/private/etc"))
(allow file-read-metadata)
;; Homebrew (if Node.js installed via Homebrew)
(allow file-read* (subpath "/opt/homebrew"))
;; Project directory (read + write)
(allow file-read* file-write* (subpath (param "PROJECT_DIR")))
;; Temp directories
(allow file-read* file-write* (subpath (param "TMPDIR")))
(allow file-read* file-write* (subpath "/private/var/folders"))
(allow file-read* file-write* (subpath "/private/tmp"))
;; Network: loopback only
(allow network* (local ip "localhost:*"))
(allow network* (remote ip "localhost:*"))
(allow network* (remote unix-socket))
Usage:
sandbox-exec -D PROJECT_DIR=/path/to/project -D TMPDIR=$TMPDIR -f profile.sb node app.js
.app bundle and code signing entitlementscom.apple.developer.endpoint-security.client)sandbox-exec is the only practical option for CLI tool sandboxing on macOS. No Apple-supported replacement exists for this use case. Both Anthropic (Claude Code) and OpenAI (Codex) use it in production. The deprecation is cosmetic — the kernel subsystem is permanent infrastructure.
Apple's Quinn "The Eskimo" from Developer Technical Support acknowledged this gap on Apple Developer Forums, noting that Endpoint Security is "a completely different mechanism" without providing a direct sandbox-exec replacement for CLI use cases.
| Test | Result |
|---|---|
| Basic sandbox-exec invocation | Works, no warnings |
| Filesystem deny (read /etc/passwd with only /usr allowed) | Denied, exit 134 |
| Network deny (curl external host) | Denied, exit 6 |
| Node.js v25.6.0 under sandbox | Works, CPUs detected (14), platform correct |
| Process-exec restriction (deny /bin/ls, allow /bin/echo) | ls exit 126, echo works |
| /bin/sh → /bin/bash redirect | Must allow both for shell scripts |