macOS confinement via sandbox-exec and an inline SBPL profile.
confine/2 wraps the caller's argv as
/usr/bin/sandbox-exec -p <profile> -- <argv...>The executable is the absolute /usr/bin/sandbox-exec, never a bare name, so
a poisoned PATH cannot substitute a different binary.
The profile fences writes, not reads
The profile opens with (allow default). That is deliberate and it is the
honest description of what this provider buys you: it denies every
file-write* performed by the confined process, then re-allows the
console /dev nodes and — under :workspace_write — every directory in
Nous.Sandbox.writable_roots/1. Reads, network, and process spawning are
not restricted. This is a write fence against a prompt-injected
rm -rf, not a confidentiality boundary.
Nous.Sandbox.writable_roots/1 is the single owner of the writable set; this
module never recomputes it, so the profile cannot drift from the rest of the
system's idea of what is writable.
Writes delegated to a running daemon are out of scope
"Denied" above is exactly as narrow as it reads: the confined process may
not perform the write. It does not mean no byte can land outside the writable
set. (allow default) permits mach-lookup, so the confined process can ask
an already-running, unconfined system daemon to write on its behalf, and
seatbelt checks the daemon's credentials, not ours. Executed, under the real
:workspace_write profile:
sandbox-exec -p <profile> -- sh -c 'defaults write com.nous.esc pwned yes'exits 0, prints no denial signature, and leaves a 56-byte plist in $HOME —
outside every entry of writable_roots/1 — because cfprefsd performed the
write. Anything reachable over XPC/launchd that writes on a client's behalf
is the same class; where the Docker CLI is present, docker run -v /:/host
is that class with a much larger blast radius (reasoned, not executed).
Closing this would take a deny mach-lookup (global-name ...) list that
grows with every macOS release, so the boundary is named rather than patched.
Two things that look like this and are not: osascript -e 'do shell script'
(the process it spawns inherits our sandbox) and a nested sandbox-exec
(it cannot widen the profile — sandbox_apply fails with EPERM).
The /dev write nodes are not a hole
base_profile/0 re-allows /dev/null, /dev/stdout, /dev/stderr,
/dev/tty and (subpath "/dev/fd") in every mode, :read_only included.
Those are the caller's own already-open descriptors: the confined process can
write through an fd its unconfined parent opened, and it still cannot
open() a new path outside the writable set (verified under the real
:workspace_write profile — exec 9> "$HOME/x" is EPERM, and
/dev/fd/<non-numeric> is ENOENT, so nothing can be created there). Without
them cmd > /dev/stdout, tee /dev/stderr, tee /dev/tty and > /dev/fd/1
all failed with "Operation not permitted" — ordinary idioms in model-authored
shell — while all four succeed under Nous.Sandbox.Bwrap, whose --dev /dev
tmpfs makes those nodes writable. The two providers now tell the caller the
same story.
On sandbox-exec being deprecated
Apple has marked sandbox-exec deprecated and has shipped no replacement for
sandboxing an arbitrary CLI process. It nevertheless still works on current
macOS and is what OpenAI's Codex CLI, Anthropic's Claude Code, and Chrome all
use for exactly this job. Depending on it is an accepted risk: if Apple
removes it, Nous.Sandbox.chain/0 gains a replacement provider and nothing
above the seam changes. That is what the seam is for.
Summary
Functions
Wrap argv in sandbox-exec under an SBPL profile derived from policy.
Check that sandbox-exec exists and that the kernel actually enforces
base_profile/0.
Functions
@spec confine([String.t()], Nous.Sandbox.Policy.t()) :: {:ok, Nous.Sandbox.Confined.t()}
Wrap argv in sandbox-exec under an SBPL profile derived from policy.
Pure — it builds strings and returns data. The profile denies all writes
except the /dev console nodes and, under :workspace_write, the subpaths
reported by Nous.Sandbox.writable_roots/1.
@spec probe(pos_integer()) :: {:ok, :full} | {:error, :unusable}
Check that sandbox-exec exists and that the kernel actually enforces
base_profile/0.
The probe runs the real base_profile/0 — the same string :read_only gets,
which grants no writable root at all — and asserts both directions in one
spawn:
/bin/sh -c 'echo x > /dev/null && ! : > "$0"' <path under System.tmp_dir!/0>Exit 0 therefore means "a permitted write succeeded and a write outside
every writable root was actually refused". The previous probe ran
/usr/bin/true under (version 1) (allow default), a profile with no deny
clause: it proved the binary exists and the parser was happy, never that one
write is stopped.
Exit 0 is {:ok, :full}; a missing binary, a nonzero exit, a timeout, or any
other runner error is {:error, :unusable}. Seatbelt has no partial grade —
either the kernel applies the profile or it does not — and anything ambiguous
fails closed.