Skip to main content

Run commands in a sandbox with writes confined to selected paths

Project description

sbrun

sbrun launches commands in a sandbox that only allows writes beneath the current directory tree plus paths you explicitly opt into.

  • macOS: uses the Seatbelt sandbox via libsandbox
  • Linux: uses unprivileged user namespaces + mount namespaces (inspired by bubblewrap) by default; when the native sbrun binary is installed setuid root, it automatically switches to a privileged mount-namespace backend

The implementation is a single Rust crate:

  • the sbrun binary is the CLI
  • the same crate also exposes a Python sbrun.exec(...) API via PyO3
  • platform-specific sandboxing is selected at compile time

Install

Install the latest release:

curl -fsSL https://raw.githubusercontent.com/AnswerDotAI/sbrun/main/install.sh | bash

Build locally:

cargo build --release

Install the Python extension into an active virtualenv:

maturin develop --release

Use

Start an interactive login shell:

cd /path/to/project
sbrun

Run a command directly:

cd /path/to/project
sbrun python3 app.py

Run a shell snippet with your current $SHELL:

cd /path/to/project
sbrun -c 'touch ok.txt && echo hello'

Allow writes to an extra directory:

cd /path/to/project
sbrun --write /tmp -- python3 -c 'open("/tmp/sbrun-demo", "w").write("ok")'

Set environment variables to project-local directories:

cd /path/to/project
sbrun --env-dir IPYTHONDIR --env-dir MPLCONFIGDIR -- ipython

Remove selected variables from the child environment:

cd /path/to/project
sbrun --unset-env GITHUB_API_KEY --unset-env OPENAI_API_KEY -- python3 app.py

If the command itself starts with -, use -- to stop option parsing:

cd /path/to/project
sbrun -- -lc 'printf hello\n'

Help and version:

sbrun --help
sbrun --version

Install the persistent Linux sysctl fix and apply it:

sudo sbrun --kernel-install

CLI

  • -w, --write PATH: allow writes to a regular file or directory; repeatable
  • -d, --env-dir VAR: set VAR to .sbrun/VAR; repeatable
  • -u, --unset-env VAR: remove VAR from the child environment; repeatable
  • -c, --command STRING: run $SHELL -lc STRING
  • --kernel-install: install /etc/sysctl.d/90-sbrun.conf and run sysctl --system (Linux only; must be root, e.g. via sudo)
  • --config PATH: load that TOML file and ignore the standard config locations
  • --no-config: ignore config files entirely
  • --: stop parsing sbrun options

Behavior:

  • with no command, sbrun launches your $SHELL as an interactive login shell
  • with -c/--command, sbrun runs $SHELL -lc STRING
  • with --kernel-install, sbrun installs the persistent Linux sysctl config and runs sysctl --system
  • otherwise sbrun execs the given command directly
  • SBRUN_ACTIVE=1 is exported in the child environment
  • HOME stays your real home directory when one is available
  • TMPDIR is set to /tmp
  • the shell history file is writable by default
  • stdout/stderr redirected to regular files outside allowed writable paths are rejected unless SBRUN_ALLOW_STDIO_REDIRECTS=1

For bash prompt logic, you can use SBRUN_ACTIVE without replacing an existing PROMPT_COMMAND or PS1. Put this in ~/.bashrc:

sbrun_prompt_prefix() {
  [[ ${SBRUN_ACTIVE:-} == 1 ]] || return
  case $PS1 in
    '🔒 '*) ;;
    *) PS1="🔒 $PS1" ;;
  esac
}

case "$(declare -p PROMPT_COMMAND 2>/dev/null)" in
  "declare -a "*)
    case " ${PROMPT_COMMAND[*]} " in
      *" sbrun_prompt_prefix "*) ;;
      *) PROMPT_COMMAND+=(sbrun_prompt_prefix) ;;
    esac
    ;;
  *)
    case ";${PROMPT_COMMAND:-};" in
      *";sbrun_prompt_prefix;"*) ;;
      *) PROMPT_COMMAND="${PROMPT_COMMAND:+$PROMPT_COMMAND; }sbrun_prompt_prefix" ;;
    esac
    ;;
esac

If your login shell does not source ~/.bashrc, put the same snippet in ~/.bash_profile.

Config

sbrun reads TOML config from:

  • $XDG_CONFIG_DIRS/.../sbrun/config.toml
  • $XDG_CONFIG_HOME/sbrun/config.toml
  • ~/.config/sbrun/config.toml when XDG_CONFIG_HOME is unset

--config PATH replaces those defaults with one explicit file. --no-config skips config loading entirely.

Example:

version = 1

write = ["/tmp", "/Volumes/scratch"]
optional_write = [
  "~/.cache",
  "~/Library/Caches",
]

Rules:

  • version must be 1 when present
  • write entries are required and error if they do not resolve
  • optional_write entries are ignored when they do not resolve
  • config paths must be absolute or start with ~/
  • env_dir and unset_env are CLI-only

On first run, if no config file exists, sbrun auto-creates ~/.config/sbrun/config.toml with sensible platform defaults (writable /tmp, ~/.cache, ~/.config, etc). The defaults are also shipped in the repo as sbrun.default.macos.toml and sbrun.default.linux.toml.

Platform notes

macOS

The sandbox is applied via the Seatbelt profile language and libsandbox. All reads are allowed; writes are confined to the working directory and configured paths.

Linux

The sandbox uses unprivileged user namespaces (CLONE_NEWUSER) and mount namespaces (CLONE_NEWNS), the same approach used by bubblewrap. The root filesystem is bind-mounted read-only, then writable paths are bind-mounted back on top. Default installs require neither root nor setuid.

If the native sbrun binary is installed root-owned and setuid, sbrun automatically switches to a privileged Linux backend. In that mode it skips CLONE_NEWUSER, sets up the mount namespace as root, then drops back to the calling user before exec(). That avoids AppArmor's unprivileged user namespace restriction without changing kernel settings.

Example optional install:

sudo install -o root -g root -m 4755 ./target/release/sbrun /usr/local/bin/sbrun

The setuid mode only applies to the native binary, not a Python console-script wrapper.

Requires kernel.unprivileged_userns_clone=1 (the default on most distros).

On Ubuntu 24.04, the most common failure is AppArmor blocking unprivileged user namespaces. The usual symptom is that sbrun fails before starting your command with an error like:

  • write /proc/self/setgroups: Permission denied
  • write /proc/self/uid_map: Operation not permitted

You can confirm the host setup with:

unshare --user --map-root-user --mount sh -c 'id -u; mount | head -1'

If that fails, sbrun will fail too.

Two ways to make sbrun work on affected Ubuntu systems:

  1. keep the default unprivileged install and let sbrun install the persistent host setting:
sudo sbrun --kernel-install

That writes:

cat <<'EOF'
kernel.unprivileged_userns_clone=1
kernel.apparmor_restrict_unprivileged_userns=0
EOF

and then runs sysctl --system.

  1. install the native sbrun binary setuid root instead, which needs no kernel setting change:
sudo install -o root -g root -m 4755 sbrun /usr/local/bin/sbrun

GitHub-hosted Linux runners currently hit this restriction too, so this repo only runs full sandbox integration tests on macOS in GitHub Actions.

Python

The Python API is intentionally minimal and follows the same exec model as the CLI:

import sbrun

sbrun.exec(
    ["python3", "app.py"],
    write=["/tmp"],
    env_dir=["IPYTHONDIR"],
    unset_env=["GITHUB_API_KEY"],
)

On success, sbrun.exec(...) does not return because it replaces the current process image. On failure, it raises a Python exception.

Development

Build, test, and release notes live in DEV.md.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distributions

No source distribution files available for this release.See tutorial on generating distribution archives.

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

sbrun-0.0.9-cp39-abi3-manylinux_2_39_x86_64.whl (371.9 kB view details)

Uploaded CPython 3.9+manylinux: glibc 2.39+ x86-64

sbrun-0.0.9-cp39-abi3-macosx_11_0_arm64.whl (326.5 kB view details)

Uploaded CPython 3.9+macOS 11.0+ ARM64

File details

Details for the file sbrun-0.0.9-cp39-abi3-manylinux_2_39_x86_64.whl.

File metadata

File hashes

Hashes for sbrun-0.0.9-cp39-abi3-manylinux_2_39_x86_64.whl
Algorithm Hash digest
SHA256 2a22907dbb472bc445d0049cd89234cd689d2de13469259f74659b85c81680ce
MD5 df75468ff652f1fc8f932798e4da21ec
BLAKE2b-256 faba7dc6e99086d042cfb0aedabe340753b078ff8feb11c112eda622b5c92824

See more details on using hashes here.

Provenance

The following attestation bundles were made for sbrun-0.0.9-cp39-abi3-manylinux_2_39_x86_64.whl:

Publisher: release.yml on AnswerDotAI/sbrun

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file sbrun-0.0.9-cp39-abi3-macosx_11_0_arm64.whl.

File metadata

  • Download URL: sbrun-0.0.9-cp39-abi3-macosx_11_0_arm64.whl
  • Upload date:
  • Size: 326.5 kB
  • Tags: CPython 3.9+, macOS 11.0+ ARM64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for sbrun-0.0.9-cp39-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 33bf31ffad3d4a5aa47b773e21a9aea3f79d7c57c4a59a9b7e2cc14c58782895
MD5 a96d93ac4eff84032936ef47558e8ad7
BLAKE2b-256 1d448c63ef5a7c20c1d808eb87f39e1e1d959b52d77e5e9dc5d50e76977b6ecb

See more details on using hashes here.

Provenance

The following attestation bundles were made for sbrun-0.0.9-cp39-abi3-macosx_11_0_arm64.whl:

Publisher: release.yml on AnswerDotAI/sbrun

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page