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
sbrunbinary is installed setuid root, it automatically switches to a privileged mount-namespace backend
The implementation is a single Rust crate:
- the
sbrunbinary 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: setVARto.sbrun/VAR; repeatable-u, --unset-env VAR: removeVARfrom the child environment; repeatable-c, --command STRING: run$SHELL -lc STRING--kernel-install: install/etc/sysctl.d/90-sbrun.confand runsysctl --system(Linux only; must be root, e.g. viasudo)--config PATH: load that TOML file and ignore the standard config locations--no-config: ignore config files entirely--: stop parsingsbrunoptions
Behavior:
- with no command,
sbrunlaunches your$SHELLas an interactive login shell - with
-c/--command,sbrunruns$SHELL -lc STRING - with
--kernel-install,sbruninstalls the persistent Linux sysctl config and runssysctl --system - otherwise
sbrunexecs the given command directly SBRUN_ACTIVE=1is exported in the child environmentHOMEstays your real home directory when one is availableTMPDIRis 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.tomlwhenXDG_CONFIG_HOMEis 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:
versionmust be1when presentwriteentries are required and error if they do not resolveoptional_writeentries are ignored when they do not resolve- config paths must be absolute or start with
~/ env_dirandunset_envare 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 deniedwrite /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:
- keep the default unprivileged install and let
sbruninstall 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.
- install the native
sbrunbinary 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
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file sbrun-0.0.9-cp39-abi3-manylinux_2_39_x86_64.whl.
File metadata
- Download URL: sbrun-0.0.9-cp39-abi3-manylinux_2_39_x86_64.whl
- Upload date:
- Size: 371.9 kB
- Tags: CPython 3.9+, manylinux: glibc 2.39+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2a22907dbb472bc445d0049cd89234cd689d2de13469259f74659b85c81680ce
|
|
| MD5 |
df75468ff652f1fc8f932798e4da21ec
|
|
| BLAKE2b-256 |
faba7dc6e99086d042cfb0aedabe340753b078ff8feb11c112eda622b5c92824
|
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
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
sbrun-0.0.9-cp39-abi3-manylinux_2_39_x86_64.whl -
Subject digest:
2a22907dbb472bc445d0049cd89234cd689d2de13469259f74659b85c81680ce - Sigstore transparency entry: 1222428283
- Sigstore integration time:
-
Permalink:
AnswerDotAI/sbrun@8570418ab8984706e9e60a351169ea29ff8c5191 -
Branch / Tag:
refs/tags/v0.0.9 - Owner: https://github.com/AnswerDotAI
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@8570418ab8984706e9e60a351169ea29ff8c5191 -
Trigger Event:
push
-
Statement type:
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
33bf31ffad3d4a5aa47b773e21a9aea3f79d7c57c4a59a9b7e2cc14c58782895
|
|
| MD5 |
a96d93ac4eff84032936ef47558e8ad7
|
|
| BLAKE2b-256 |
1d448c63ef5a7c20c1d808eb87f39e1e1d959b52d77e5e9dc5d50e76977b6ecb
|
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
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
sbrun-0.0.9-cp39-abi3-macosx_11_0_arm64.whl -
Subject digest:
33bf31ffad3d4a5aa47b773e21a9aea3f79d7c57c4a59a9b7e2cc14c58782895 - Sigstore transparency entry: 1222428230
- Sigstore integration time:
-
Permalink:
AnswerDotAI/sbrun@8570418ab8984706e9e60a351169ea29ff8c5191 -
Branch / Tag:
refs/tags/v0.0.9 - Owner: https://github.com/AnswerDotAI
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@8570418ab8984706e9e60a351169ea29ff8c5191 -
Trigger Event:
push
-
Statement type: