Skip to main content
Finding which rule fired needs nothing but an installed SDK. Changing the policy needs a source checkout and a rebuild — on a published SDK, capture the deny line and file it upstream instead of editing the policy locally.

Prerequisites

  • A working BoxLite install (Python boxlite or 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 the make environment 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:
Second terminal — run a minimal operation that exercises the isolation layer (copy and run directly):
Back in the first terminal: if a 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:
Common denial types:

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 in src/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 by build_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() and build_dynamic_write_paths() (both in seatbelt.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:
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:
  1. Check the crash reports: ls ~/Library/Logs/DiagnosticReports/*shim*
  2. Confirm the process actually started: check the host logs
  3. 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 via advanced=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 native Boxlite + Box handles: the native Box.exec(...) returns an Execution (not an ExecResult); you must await execution.wait() to get the exit code and read the stdout()/stderr() streams; remove with runtime.remove(box.id, force=True) (on the runtime, not box.remove()). The SimpleBox form above already drains output automatically, which is more convenient for debugging.
If a box fails to start with SecurityOptions.maximum() but works with development(), the Seatbelt policy is almost certainly missing an allow rule — go back to Step 1 and capture the deny line.

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 stream before 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 literal over subpath)
  • Document in a comment why the rule is needed
  • Rebuild and retest
  • Confirm no new denials appear

Further reading