Run any command in a kernel-enforced sandbox with sensible defaults per language ecosystem. Defend your development machine and CI runners against supply chain attacks. Supports macOS (Seatbelt / SBPL) and Linux (Landlock LSM + seccomp-bpf).
sbe run -- npm install
sbe run -- cargo build
sbe run -- pip install -r requirements.txt
sbe run -- mix deps.get
sbe run -- ./gradlew build
Package managers execute arbitrary code during install and build: npm
postinstall scripts, Rust build.rs, Python setup.py, Elixir mix compile
hooks, Gradle plugins. A single compromised dependency can read your SSH keys,
exfiltrate cloud credentials, install persistent malware, or establish C2
channels β all silently, in the background.
sbe wraps your existing tools in a self-applied kernel sandbox: macOS
sandbox-exec or Linux Landlock + seccomp. No code changes, no new package
manager. Just prefix your command with sbe run --.
| Attack Vector | macOS (Seatbelt / SBPL) | Linux (Landlock + seccomp) |
|---|---|---|
Read ~/.ssh, ~/.aws, cloud creds |
SBPL file-read* denylist |
denyRead forbidden-list (see Caveats) |
Write to /Library/Caches, LaunchAgents |
SBPL file-write* allowlist |
Landlock write allowlist |
| Network C2 on non-standard ports | SBPL pins egress to proxy / :443 |
Landlock NET_CONNECT_TCP (β₯6.7) or :443 |
Second-stage download via curl/wget |
Proxy 403s non-allowlisted domains | Same proxy, identical behaviour |
osascript / AppleScript abuse |
SBPL process-exec denylist |
n/a (Linux) |
sudo, pkexec, privilege escalation |
n/a (macOS) | Lint refuses allowExec subpaths covering sudo/pkexec/etc. |
| Clipboard / screen exfiltration | SBPL denies pbcopy/screencapture |
Allowlist omits them |
| Module load / kernel attack surface | n/a | seccomp blocks bpf, init_module, kexec_*, ptrace, β¦ |
cargo install --path apps/cliOr:
make installSupported targets:
- macOS β any release with
/usr/bin/sandbox-exec(all SIP-compliant builds). - Linux β kernel β₯5.13 for basic enforcement, β₯6.7 for full
per-port TCP filtering (Landlock ABI v4). On 5.13β6.6 a
--allow-degradedfallback is available; without that flag, sbe refuses to start rather than silently downgrading.
The bundled composite action installs a prebuilt sbe binary for the runner's
OS and architecture, then adds it to PATH. It works on both Linux and macOS
GitHub-hosted runners:
jobs:
build-linux:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: tyrchen/sbe@sbexec-v0.3.0 # or @master with `version: latest`
with:
version: latest
- run: sbe --version
- run: sbe run -- cargo build
build-macos:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: tyrchen/sbe@sbexec-v0.3.0
with:
version: latest
- run: sbe run -- cargo buildInputs:
| Input | Default | Description |
|---|---|---|
version |
latest |
Release to install. Accepts latest, a semver (0.3.0), or a full tag (sbexec-v0.3.0). |
github-token |
${{ github.token }} |
Token used for releases API + asset download. |
Outputs: version (resolved tag) and bin-path (absolute path to the
installed binary).
Supported runner / architecture matrix (auto-detected via $RUNNER_OS and
$RUNNER_ARCH):
| Runner | Arch | Release artifact |
|---|---|---|
ubuntu-* |
x86_64 | x86_64-unknown-linux-musl |
ubuntu-* |
arm64 | aarch64-unknown-linux-musl |
macos-* |
arm64 | aarch64-apple-darwin |
Linux runners on ubuntu-latest / ubuntu-24.04 (kernel 6.x) get full
enforcement via Landlock ABI v4 + seccomp-bpf. macOS runners use
sandbox-exec / SBPL.
# Auto-detects ecosystem from command name or project files
sbe run -- npm install
sbe run -- cargo build
# Specify ecosystem explicitly
sbe run -p python -- pip install flask
# See what policy would be installed (does not execute)
sbe run --dry-run -- npm install
# Print resolved config + generated policy
sbe inspect -- cargo build
# List all default profiles
sbe profiles
# Disable network sandboxing for debugging
sbe run --allow-all-network -- npm install
# Add a custom allowed domain
sbe run -n "api.mycompany.com" -- npm install
# Allow build-time downloads (enables curl/wget + adds domains to proxy)
sbe run -f "download.example.com" -- cargo build
# Allow an extra binary
sbe run -e /usr/bin/curl -- npm install
# Stream sandbox violations in real-time
sbe run --audit -- npm install
# Linux: proceed under a kernel without ABI v4 net filter (best-effort)
sbe run --allow-degraded -- cargo build sbe CLI
β
βββββββββββββββ΄ββββββββββββββ
β β
Profile/Config SandboxBackend (cfg-selected at
Resolver trait compile time)
(sbe-core) β
βββββββββββββ΄ββββββββββββ
β β
βββββββββββΌβββββββββ ββββββββββββΌβββββββββ
β MacosSandbox β β LinuxSandbox β
β (sandbox-exec) β β (Landlock + β
β β β seccomp + β
β - SBPL gen β β pre_exec) β
β - tempfile β β - Ruleset build β
β - spawn -f β β - BPF compile β
βββββββββββ¬βββββββββ ββββββββββββ¬βββββββββ
β β
βββββββββββββ¬ββββββββββββ
β
βΌ
ββββββββββββ
β user β HTTP_PROXY β sbe-proxy
β command β βββββββββββββββββββββββββββΊ
ββββββββββββ (same path both platforms)
Two-layer network defense:
- Kernel layer: SBPL or Landlock pins outbound traffic to
localhost:PROXY_PORT(or:443). - Application layer (proxy): an HTTP CONNECT proxy checks the requested domain against the per-ecosystem allowlist before tunneling.
This combination defeats CDN-backed registries: SBPL and Landlock can't filter by hostname, but the proxy does β and the kernel forces every TCP egress through it.
The README's "What It Blocks" table summarises the wins. These are the known gaps β places where the marketing implies coverage that the implementation can't actually deliver. Don't trust sbe to be the last line of defense against any of these.
/proccross-process snooping (Linux). The baseline read anchors include/proc/, so a sandboxed build script can list every process the invoking user owns and read its/proc/<pid>/environ,cmdline,cwd,fd/*. If your shell exportedAWS_SECRET_ACCESS_KEY, an attacker-controllednpm installsees it. Mitigation: setkernel.yama.ptrace_scope=2and avoid putting secrets in env vars of unrelated processes; macOS isn't affected.- DNS-over-UDP is unfiltered (Linux). Landlock has no UDP filter at
any ABI. With
/etc/resolv.confreadable, an attacker can encode exfil data in DNS subdomains and the kernel resolver will deliver them. The HTTP CONNECT proxy filters HTTP/HTTPS by hostname but never sees DNS itself. - TLS to port 443 on any host when proxy is disabled. JVM tools
(Maven, sbt's coursier, Gradle's resolver) don't honor
HTTP_PROXYenv, so the java profile ships withenableProxy: falseand Landlock allows TCP egress on port 443 to anywhere. A compromised Maven plugin can establish a TLS C2 channel to any host. Kernel still blocks every non-443 outbound; filesystem and exec restrictions still apply. - Gradle on Linux disables the kernel net filter entirely. Gradle's
CLI β daemon IPC uses a random localhost TCP port that Landlock v4
can't express. Users opting into Gradle via
allowAllNetwork: truelose all kernel TCP filtering for that profile. /dev/tcp,/dev/udp(bash built-ins).bashis in the defaultallowExec, and bash's< /dev/tcp/host/portopens a socket in-process. Same kernel syscalls (socket+connect); the proxy doesn't see this traffic. On port-443, fully unfiltered (see above).- Audit logging is best-effort on Linux. The auditor reads
/dev/kmsg, which requiresCAP_SYSLOGon most hardened hosts (kernel.dmesg_restrict=1is the Ubuntu default). When it can't read kmsg, sbe falls back to "violations surface asEACCESexit codes" β silent for any attack that doesn't trip a kernel deny event (DNS exfil, /proc snooping, TLS:443 C2).
These differences from the macOS path are surface-level β the same sbe run
UX still works. Documented here so you know what you're getting:
denyReadis allowlist-omission, not subtractive deny. Landlock has no way to subtract from a granted subtree. sbe ships a curated read-allowlist on Linux (/etc,/lib,/usr,/proc,/sys,/tmp,$HOMEXDG dirs) that intentionally excludes~/.ssh,~/.aws, etc. Anything you list indenyReadbecomes a sealed forbidden-list: future config changes that try to grant read on a forbidden path are rejected at backend-time.denyExecis a no-op. Landlock is allowlist-only;denyExecentries in a Linux profile emit a warning and are otherwise ignored. The defaults ship a per-binaryallowExecenumeration that omitssudo,su,pkexec,doas,chsh,chfn,newgrp,sg,passwd,gpasswd,mount,umount. A.sbe.yamlthat grantsallowExec: ["/usr/bin/"]is rejected at startup (use--allow-degradedto override after considering the threat model).PR_SET_NO_NEW_PRIVSis mandatory. Linux requires it for unprivileged seccomp; sbe sets it before applying any filter. The flag persists acrossexecveand disablessetuidbits across the descendant tree. Consequence:sudo/su/pkexeccannot escalate β this is desired. The handful of tools that depend on setuid binaries (e.g., legacyping) will fail; the vast majority of build scripts are unaffected.- UDP is unfiltered. Landlock filters only TCP. DNS over UDP, NTP, QUIC
egress are not subject to per-port enforcement. The seccomp baseline blocks
AF_PACKETraw sockets but notSOCK_DGRAMonAF_INET. The HTTP CONNECT proxy is TCP-only by design. - JVM tools don't get domain filtering. The java profile ships with
enableProxy: falseon both macOS and Linux, because JVM HTTP clients (Maven, Gradle's resolver, sbt's coursier) do not honour the standardHTTP_PROXY/HTTPS_PROXYenv vars β they require-Dhttps.proxyHostsystem properties which sbe cannot inject dynamically (the proxy port is allocated at runtime). The kernel filter still pins TCP egress to port 443 (no random ports, no port-80 sneaking), but per-domain filtering is delegated to whatever network controls you run outside sbe. - Gradle on Linux requires opt-in. Gradle's CLI talks to a separate
daemon over TCP on a kernel-chosen random localhost port. Landlock
ABI v4 filters TCP by port only β there's no way to express "any port
on 127.0.0.1" the way macOS SBPL can. Gradle does not accept a fixed
daemon port via any CLI flag. To run Gradle under sbe on Linux, set
allowAllNetwork: truefor thejavaprofile in your.sbe.yamlβ this disables kernel TCP filtering entirely for that profile. sbt (default UDS IPC) and Maven (single-JVM) work without this opt-in. macOS users are unaffected (SBPL has(remote ip "localhost:*")). - DBus-resolved DNS may fail. Tools that resolve via systemd-resolved's
DBus path (some Python/Node DNS libraries through
nss-systemd) will hitEACCESon/run/dbus/system_bus_socket. The fallback through glibc'sgetaddrinfoover UDP (/etc/resolv.conf) works. Workaround for affected tools: use--allow-fetchor add the DBus socket to a custom profile. - Kernel <6.7 needs
--allow-degraded. Without Landlock ABI v4, sbe cannot pin TCP egress to a specific port. With the flag, a best-effort seccompconnect()arg filter is used and a warning is printed. We refuse to silently downgrade.
| Ecosystem | Auto-detected commands | Auto-detected files |
|---|---|---|
| Node.js | node, npm, npx, yarn, pnpm, bun |
package.json |
| Rust | cargo, rustc, rustup |
Cargo.toml |
| Python | python, python3, pip, pip3, uv, poetry, pdm, rye |
pyproject.toml, setup.py, requirements.txt, Pipfile |
| Elixir | mix, elixir, iex |
mix.exs |
| Java | java, javac, mvn, mvnw, gradle, gradlew, sbt, scala, scalac, kotlinc |
pom.xml, build.gradle, build.gradle.kts, build.sbt |
sbe profiles prints the full per-OS defaults.
Create a .sbe.yaml (or .sbe.yml) in your project root, or
~/.config/sbe/config.yaml for global defaults:
profiles:
node:
allowWrite:
- "./dist"
allowDomains:
- "api.mycompany.com"
allowFetch:
- "download.example.com" # enables curl/wget + adds to proxy allowlist
env:
NODE_ENV: production
# Custom profile extending an existing one
my-app:
extends: node
allowDomains:
- "internal-registry.mycompany.com"
enableProxy: true
allowAllNetwork: false
allowDegraded: false # Linux only; default falseConfig resolution order (last wins):
- Built-in ecosystem defaults (per-OS YAML embedded at compile time)
- Global config:
~/.config/sbe/config.yaml - Project config:
.sbe.yamlor.sbe.yml(walks up to git root) - CLI flags
sbe/
βββ crates/
β βββ core/ # sbe-core: profile + backends
β β βββ src/
β β βββ profile/ # Per-ecosystem defaults (per-OS YAML)
β β β βββ defaults-macos.yaml
β β β βββ defaults-linux.yaml
β β βββ sandbox/ # SandboxBackend trait + impls
β β β βββ mod.rs # Trait + cfg-selected Sandbox re-export
β β β βββ macos/ # sandbox-exec backend
β β β β βββ mod.rs
β β β β βββ sbpl.rs
β β β β βββ exec.rs
β β β βββ linux/ # Landlock + seccomp backend
β β β βββ mod.rs
β β β βββ probe.rs # Kernel/ABI probe
β β β βββ policy.rs # YAML render for --dry-run
β β β βββ landlock.rs # Ruleset builder
β β β βββ seccomp.rs # BpfProgram builder
β β β βββ exec.rs # pre_exec wiring
β β βββ config.rs
β β βββ detect.rs
β β βββ error.rs
β βββ proxy/ # sbe-proxy: domain-filtering CONNECT proxy
βββ apps/
β βββ cli/ # sbe binary
βββ specs/ # Design documents
sbe run [OPTIONS] -- <COMMAND>...
Options:
-p, --profile <NAME> Use a specific profile (overrides auto-detect)
-n, --allow-domain <DOMAIN> Add domain to network allowlist (repeatable)
-N, --deny-domain <DOMAIN> Remove domain from allowlist (repeatable)
-w, --allow-write <PATH> Add writable path (repeatable)
-r, --deny-read <PATH> Add read-denied path (repeatable)
-e, --allow-exec <PATH> Allow execution of binary (repeatable)
-E, --deny-exec <PATH> Deny execution of binary (repeatable; macOS only)
-f, --allow-fetch <DOMAIN> Allow build-time downloads (enables curl/wget + adds to proxy)
--allow-all-network Disable network sandboxing entirely
--no-proxy Disable proxy (kernel port-443 mode)
--allow-degraded Proceed under a degraded kernel (Linux <ABI v4)
--audit Stream sandbox violations to stderr
--audit-log <PATH> Write violations to file
--dry-run Print policy to stdout, do not execute
-c, --config <PATH> Use specific config file
-v, --verbose Verbose output
sbe inspect [OPTIONS] -- <COMMAND>...
Print resolved config + generated policy without executing.
macOS: SBPL Scheme document.
Linux: YAML policy showing Landlock ruleset + seccomp action table.
sbe profiles
List all built-in ecosystem profiles and their defaults.
Exit codes: sbe passes through the child process exit code. sbe's own errors use 125 (internal error) and 126 (sandbox setup failed).
- Detect ecosystem from command name (
npmβ Node) or project files (Cargo.tomlβ Rust). - Load profile β built-in per-OS defaults merged with global/project
.sbe.yamland CLI flags. - Probe backend β
sandbox-execon macOS, Landlock ABI level on Linux. Refuse on missing capability unless--allow-degraded. - Start proxy β bind HTTP CONNECT proxy on
127.0.0.1:0, get ephemeral port. - Compile policy β SBPL string + tempfile on macOS; Landlock
Ruleset+BpfProgramin-memory on Linux. - Execute β
- macOS:
sandbox-exec -f /tmp/sbe-XXXX.sb <command>withHTTP_PROXYenv injected. - Linux:
Command::pre_execissuesprctl(PR_SET_NO_NEW_PRIVS) β landlock_restrict_self β seccomp(TSYNC), thenexecve. No tempfile on disk.
- macOS:
- Monitor β optionally stream violations (macOS
sandboxd; Linux/dev/kmsgaudit). - Cleanup β stop proxy, propagate exit code.
make build
make test # uses cargo-nextest under sbe
make fmt
make lint
make check # fmt + lint + test
make install- Rust 2024 edition (stable)
- macOS, or Linux β₯5.13 (β₯6.7 for full network parity)
cargo-nextestformake test(optional)
This project is distributed under the terms of MIT.
See LICENSE for details.
Copyright 2025-2026 Tyr Chen