deny line and file it upstream instead of editing the
policy locally.
Prerequisites
- A working BoxLite install (Python
boxliteor Node@boxlite-ai/boxlite) and a machine with hardware virtualization — see Installation. - Command-line tools:
log(Unified Logging, ships with the system),sandbox-exec(at/usr/bin/sandbox-exec, ships with the system). - To modify and rebuild the policy: you need the BoxLite source tree and the
Rust toolchain (
cargo), plus themakeenvironment for the Python SDK.
Note: Seatbelt is a macOS-specific isolation mechanism. On Linux, BoxLite uses bwrap/landlock (src/boxlite/src/jailer/sandbox/bwrap.rs,landlock.rs), and the SBPL debugging flow on this page does not apply.
Quick Example (minimal flow)
The core of debugging is two steps: start monitoring first, then run the triggering operation. First terminal — real-time monitoring of boxlite-related Sandbox denials:deny(1) line appears, one of the box’s
operations was blocked by Seatbelt — see the “Debugging Workflow” below.
Debugging Workflow
Step 1: Start real-time log monitoring first
Start monitoring in a separate terminal (always start monitoring before running the test, otherwise you miss early denials):Step 2: Run the triggering operation
In another terminal, run the operation that fails (reuse the Quick Example above, or substitute your actual use-case script):Step 3: Interpret the denial
The format of a Sandbox denial in the log:Step 4: Update the policy
Based on the denial, add the corresponding allow rule to the SBPL policy:Step 5: Rebuild and retest
The.sbpl files are embedded at compile time via include_str!, so changes
take effect only after a rebuild:
Log command reference
Real-time streaming
Historical queries
Filtering tips
SBPL policy syntax
Basic structure
Common patterns
Validating syntax
BoxLite policy files
BoxLite’s Seatbelt policy is split across several files: the static fragments are insrc/boxlite/resources/seatbelt/, and the dynamic assembly is in
src/boxlite/src/jailer/sandbox/seatbelt.rs:
Viewing the full generated policy
The full runtime policy is assembled bybuild_sandbox_policy() in
src/boxlite/src/jailer/sandbox/seatbelt.rs and passed directly via
sandbox-exec -p. The absolute path of sandbox-exec is hardcoded to
/usr/bin/sandbox-exec (the constant SANDBOX_EXEC_PATH, to prevent PATH
injection).
When debugging, focus on:
- The static fragments:
src/boxlite/resources/seatbelt/*.sbpl - Dynamic path grants:
build_dynamic_read_paths()andbuild_dynamic_write_paths()(both inseatbelt.rs) - The denial records from
log show/log stream, used to locate the missing allow clause
Troubleshooting
Changes to .sbpl did not take effect
The .sbpl files are embedded into the binary at compile time via include_str!
(see the consts at the top of seatbelt.rs). After modifying them, you must
force a rebuild:
Path canonicalization (symlink problem)
macOS uses symlinks:/var -> /private/var, /tmp -> /private/tmp. The
policy must use the canonicalized real path:
Duplicate denials
The log may show “X duplicate reports for…”, meaning the same denial happened multiple times. Fix the root cause; you do not need to handle each one individually.Silent failure (no log, but a hang/crash)
Some denials are not written to the log immediately. If the process hangs or crashes without a denial record:- Check the crash reports:
ls ~/Library/Logs/DiagnosticReports/*shim* - Confirm the process actually started: check the host logs
- Temporarily run with sandbox isolation off to narrow it down (see the next item)
Distinguishing “permissions” from “sandbox”
Not all failures are caused by Sandbox. Also check:- File permissions (
ls -la) - Whether the directory exists
- Hypervisor.framework entitlements
Narrow the problem with a security preset (SDK layer)
To tell whether the problem is caused by the isolation layer, temporarily switch the security level in the SDK. Note: security options are passed viaadvanced=AdvancedBoxOptions(security=...); there is no top-level security=
keyword, and AdvancedBoxOptions lives only under the boxlite.boxlite
submodule (not exported at the top level):
If you want to manage the lifecycle manually with the nativeBoxlite+Boxhandles: the nativeBox.exec(...)returns anExecution(not anExecResult); you mustawait execution.wait()to get the exit code and read thestdout()/stderr()streams; remove withruntime.remove(box.id, force=True)(on the runtime, notbox.remove()). TheSimpleBoxform above already drains output automatically, which is more convenient for debugging.
If a box fails to start withSecurityOptions.maximum()but works withdevelopment(), the Seatbelt policy is almost certainly missing an allow rule — go back to Step 1 and capture thedenyline.
Start fails outright / no virtualization
macOS starts the microVM via Apple Hypervisor.framework (no/dev/kvm
required). If the machine has no hardware virtualization or the entitlement is
missing, the box fails to start and raises a standard RuntimeError (the
process stays alive and can be caught with try/except).
Image pull failure raises RuntimeError (not BoxliteError)
A missing command or an image pull failure raises a standard RuntimeError (Python) / bare Error (Node), not BoxliteError. See Error Handling.
Debugging checklist
- Start
log streambefore running the test - Filter logs by process name (
boxlite-shim) - Look for
deny(1)messages - Record the exact operation and target
- Add a minimal rule (prefer
literaloversubpath) - Document in a comment why the rule is needed
- Rebuild and retest
- Confirm no new denials appear

