PolicyWitness is a witness harness for macOS sandbox policies: it applies an SBPL profile to a disposable process, records what the sandbox predicted for each probe and what happened when the probe was attempted, and prints the evidence as one JSON envelope.
Querying sandbox_check about a process and attempting an operation in that process under the same policy requires managing process lifecycles. sandbox_check answers for an existing PID, and sandbox application is one-way — a process gets exactly one sandbox. Evaluating a policy therefore means a fresh process per evaluation: compile and apply the policy to it once, aim both the query and the attempted operation at that PID while it lives, and carry the answer out through a channel the policy under test cannot sever.
The user guide has the specimen format, the output envelope and the operating guidance.
PolicyWitness operates on specimens: an SBPL policy plus a probe plan. The
controller hands each specimen to a fresh runner, an unsandboxed XPC host that
spawns a C worker that applies the policy to itself once. An optional file-extension
declaration first runs a separate issuer outside that policy; bounded token bytes
travel through a transient channel to the worker. Explicit lifecycle actions
consume the token and release its native handle in that same worker. A simple probe_plan
runs as one phase; an explicit phases sequence divides actions into ordered
intervals in that same worker. At each phase arrival, the host plans queries
afresh and, when eligible queries exist, starts a fresh validator against the
worker's PID. Collection closes before that phase's actions are released,
including on collection failure. The host joins the observations into one
reply and exits; the
controller wraps the reply in the envelope it prints.
Each ordinary step carries three records: the prediction, the attempt, and a comparison that relates the two. The comparison says what was observed and in what order; it never says whether prediction and enforcement agree, and no failure is attributed to the sandbox without evidence. Kernel denial records from the unified log are attached out of band, as candidates rather than verdicts, and cannot change the execution result. The guide's reading rules say how to use each record. Extension preparation, issuance and delivery have their own records; consume/release events carry native results and publication evidence without a comparison. Fresh reads retain descriptor identity and bounded bytes. None of these records declares effective permission or successful revocation.
How one run unfolds in time, the boundaries it crosses and the checks that pin each of them: the architecture tour.
macOS sandboxing isn't just SBPL: a process's effective sandbox is its SBPL profile applied on top of the entitlements its binary was codesigned with. The same SBPL can yield different kernel behavior depending on which entitlements are granted, so a specimen has to describe both halves to be a faithful witness.
By default the policy is applied to a worker holding no entitlements. To
observe another combination, install an external runner (BYOXPC, bring your
own XPC service): a copy of the bundled service that policy-witness runner install signs with your own entitlements plist. The policy is then applied to
a worker that holds those entitlements, the registry records what each binary
carries, and a specimen can require keys the worker must hold. The guide's
external runners section has
the install recipe and the signing constraints.
Download a notarized release from
GitHub Releases, or build
one as described under Build and bundle. Releases and builds target
macOS 26, the version this repository is tested on; Info.plist and
meson.build declare that minimum together. Run it from a normal
Terminal; a sandboxed automation harness can refuse the XPC lookup.
PW="/Applications/PolicyWitness.app/Contents/MacOS/policy-witness"
cat > /tmp/file_read_deny.json <<'JSON'
{
"schema_version": 4,
"specimen_id": "file_read_deny",
"policy": {
"format": "sbpl",
"sbpl_source": "(version 1) (allow default) (deny file-read-data)"
},
"probe_plan": [
{
"step_id": "fr1",
"sandbox_check": {
"operation": "file-read-data",
"filter": { "kind": "path", "value": "/etc/hosts" }
},
"attempt": { "kind": "file", "action": "open_read", "target": "/etc/hosts" }
}
]
}
JSON
$PW run /tmp/file_read_deny.json > /tmp/result.jsonThe envelope's one step records a deny prediction, an attempt that failed
with EPERM, and a comparison stating that the query preceded the attempt;
the rest of the envelope records who ran it, what was run and what the host
observed.
Short answers to common questions, including who needs this and what it can attribute, are in QUESTIONS.md.
Building PolicyWitness requires Rust with Cargo, Xcode Command Line Tools
(or Xcode), and Meson and Ninja on PATH (brew install meson ninja).
The Developer ID Application signing identity must already be in your
login keychain. From the repository root:
make build IDENTITY='Developer ID Application: YOUR NAME (TEAMID)'The build writes dist/PolicyWitness.app, a ZIP ready for notarization at
dist/PolicyWitness.zip, and the standalone user guide at
dist/PolicyWitness.md.
build.sh compiles the Rust components with Cargo and the Swift and C
components with Meson, assembles them into one app bundle, and signs it
with the supplied identity. The app carries a version derived from git
and generated evidence recording its binaries' hashes and entitlements.
The app contains:
dist/PolicyWitness.appContents/MacOS/policy-witness(Rust controller)Contents/MacOS/pw-runner-client(Swift NSXPCConnection wrapper)Contents/MacOS/sandbox-log-observer(Rust unified-log capture helper)Contents/MacOS/sbpl-check(SBPL compile-check helper)Contents/XPCServices/PWRunner.xpc(Swift XPC host; one host and worker per specimen, with a fresh validator per eligible phase)Contents/MacOS/pw-probe-runner(bundle-local C worker that applies the policy and runs probe attempts)Contents/MacOS/sb_api_validator(bundle-local validator launched per eligible phase for sandbox_check verdicts)Contents/MacOS/pw-extension-issuer(ordinary-authority file-extension issuer with a transient token channel)
Contents/Resources/Evidence/*(generated manifests: hashes/entitlements,symbols.json)
The build tour describes the toolchain, build settings and signing checks. Building produces a signed app; the release procedure covers notarization, acceptance testing and publication. The distribution directory describes the deliverables and preserved release archives.
- Using the app:
- User guide: docs/PolicyWitness.md
- Limits: docs/LIMITS.md
- FAQ: docs/QUESTIONS.md
- Signing/distribution: docs/SIGNING.md
- Consuming the output:
- Wire contracts (request, reply, envelope): docs/CONTRACT.md
- Accepted requests and refusal diagnostics: docs/REQUEST-GRAMMAR.md
- Implementation details:
- Build tour, native compilation and signing: docs/BUILD.md
- CLI contract and controller behavior: controller/README.md
- Runner service architecture: runner/README.md
- Architecture tour (one run in time, boundaries, the document graph): docs/ARCHITECTURE.md
- Contributing:
- Working guidance: AGENTS.md
- Contributing: CONTRIBUTING.md
- Working on the controller: controller/AGENTS.md
- Working on the runner and native helpers: runner/AGENTS.md
- Test harness and coverage: tests/README.md
- Working on tests: tests/AGENTS.md