OS Sandbox

Kernel-enforced containment for agent shell commands — Seatbelt on macOS, bwrap on Linux

The OS sandbox puts a real /bin/sh -c inside a kernel-enforced filesystem jail — sandbox-exec (Seatbelt) on macOS, bwrap (bubblewrap) on Linux — and refuses to run unless it has measured that the jail actually blocks writes. Under PRAISON_SHELL=sandboxed this applies to every execute_command, not only commands with shell operators.

Diagram

OS Sandbox

blocked out-of-jail write

write escaped

🖥️ Shell command

🔍 Enforcement
proven?

🛡️ Run inside jail

🚫 Refuse — never degrade

Quick Start

  1. Run the coding agent sandboxed

    PRAISON_SHELL=sandboxed drives the OS sandbox — a real shell inside Seatbelt or bwrap.

    bash
    PRAISON_SHELL=sandboxed praisonai code
    text
    >>> run: echo written > r2.txt
    exit=0 success=True sandbox=seatbelt; r2.txt contents 'written'
  2. Watch an escape get denied by the kernel

    A write outside the workspace hits a real OS denial, not a soft warning.

    text
    >>> run: echo pwned > /tmp/PWNED.txt
    /bin/sh: /tmp/PWNED.txt: Operation not permitted
    PWNED exists: False

Two Backends

Each platform uses its native primitive; everywhere else, no wrapper is built.

PlatformBackendHow it confines
macOSsandbox-exec (Seatbelt)Generated profile denies file-write* outside the writable set, and denies reads and writes under the masked secret dirs
Linuxbwrap (bubblewrap)Read-only root, workspace bind-mounted writable, --tmpfs over the masked secret dirs, --unshare-pid, network unshared
Othernonebuild_wrapper() returns None — containment unavailable

Workspace and cwd

The writable set is PRAISONAI_WORKSPACE (or the process cwd) plus $TMPDIR. The tool’s cwd= argument must be inside that set, or the command is refused — the model cannot promote a path to writable by passing it as cwd. Network is denied by default.


Environment and masked paths

Env scrub. Variables whose names match KEY|TOKEN|SECRET|PASSW|CREDENTIAL (case-insensitive) are not passed into the jail. Entries passed explicitly through execute_command(env=...) are the allow-list and still go through.

Env applied inside the jail. Explicit env= entries are set by /usr/bin/env k=v … inside the jail, never on the bwrap / sandbox-exec launcher — otherwise LD_PRELOAD and friends would load code before containment starts.

Masked dirs. ~/.ssh, ~/.aws, ~/.gnupg, ~/.config/gcloud and ~/.azure are hidden (--tmpfs on bwrap, deny file-read file-write on Seatbelt). The rest of the filesystem stays readable because toolchains need it.

Own PID namespace (Linux). --unshare-pid makes the fresh /proc show only the jail, so /proc/<host pid>/environ cannot reveal the scrubbed env.


Refuse, Never Degrade

A sandbox that is assumed but not enforcing is worse than none — callers relax on a belief that isn’t true.

PRAISON_SHELL=sandboxed refuses to run when the OS primitive is missing or not enforcing, rather than silently falling back to an uncontained shell. To accept an uncontained shell deliberately, use PRAISON_SHELL=unsafe.


Probe Before Claiming

probe_enforcement() measures the jail instead of trusting it.

Diagram
Kernelprobe_enforcement()execute_commandModelKernelprobe_enforcement()execute_commandModelecho written > r2.txtis the jail enforcing?run child, write in-jail canary + out-of-jail escapecanary written, escape blocked(True, ‘seatbelt’, ‘blocked out-of-jail write’)approve? (critical risk)yes/bin/sh -c inside jail (own PID namespace on Linux)exit=0, contained

The probe runs a child under the wrapper that writes an in-jail canary and attempts a write outside the writable set:

  • If the escape write succeeds, the backend is reported unavailable.
  • The canary guards against a false positive — if the wrapper couldn’t start the child at all, the missing escape file proves nothing, so the canary must exist for the result to count.

Results are (enforcing: bool, backend: str, detail: str) and cached per process. Flipping to a permissive Seatbelt profile flips the probe from (True, 'seatbelt', 'blocked …') to (False, 'seatbelt', 'did not block a write outside the writable set').


Not SandboxConfig.native()


Best Practices

Trust the refusal

If sandboxed mode refuses because containment can’t be proven, install the primitive (bwrap on Linux) rather than switching to unsafe.

Scope the writable set

Set PRAISONAI_WORKSPACE to the project root; only it and the temp dir are writable. Keep other paths out so an escape attempt is a real out-of-jail write the kernel can deny.

Leave network off unless needed

network=False unshares the network on Linux and denies it in the Seatbelt profile. Enable it only when a build step genuinely needs it.

Pair with approval

The sandbox runs behind the critical-risk execute_command approval — containment and human sign-off reinforce each other.


Related