sbrun

Launches commands under in a macOS or Linux sandbox and only allows writes beneath the directory where it was started

View the Project on GitHub AnswerDotAI/sbrun

Development

Developer workflow and release notes for sbrun.

Layout

Local Build

Build a debug binary and install the sbrun CLI into the active venv:

./tools/local-build.sh

Or manually:

python -m pip install -e '.[dev]'   # dev deps (pytest, maturin)
cargo build                          # produces target/debug/sbrun (used by the tests)
maturin develop                      # installs the sbrun binary onto the venv PATH

Testing

Run the full local verification suite with:

tools/test.sh

That runs:

Tests run on both macOS and Linux. GitHub Actions runs the full suite on macOS from .github/workflows/test.yml on pushes to main. On GitHub-hosted Linux it only runs cargo test and cargo build, because the hosted environment blocks the user-namespace setup needed for the Linux sandbox integration tests. Use a self-hosted Linux runner, or any Linux environment whose policy allows unprivileged user and mount namespaces, for full Linux integration coverage.

Versioning

The canonical version lives in Cargo.toml.

Bump the patch version with:

ship-rs-bump

Release

Push a tag like v0.0.3 to trigger the GitHub Actions release workflow in .github/workflows/release.yml.

The workflow builds on both macOS and Linux in parallel:

For the local release flow:

  1. run ship-rs-bump
  2. review and commit the version change
  3. run ship-rs-release

PyPI

Publish the Python package with:

maturin publish --release --strip

The CI workflow publishes both macOS and Linux wheels to PyPI automatically.

Platform notes

macOS: sandbox is applied via Seatbelt (libsandbox). Requires macOS.

Linux: default sandbox uses unprivileged user namespaces + mount namespaces (inspired by bubblewrap). When the native sbrun binary is installed setuid root, the same binary automatically switches to a privileged mount-namespace backend instead and drops back to the caller before exec(). Default unprivileged mode still requires kernel.unprivileged_userns_clone=1 (default on most distros). The CLI also supports sudo sbrun --kernel-install, which writes /etc/sysctl.d/90-sbrun.conf and runs sysctl --system on Linux.