sbe — Sandbox Exec
Run any command in a macOS sandbox with sensible defaults per language ecosystem. Defend your development machine against supply chain attacks at the OS kernel level.
sbe run -- npm install
sbe run -- cargo build
sbe run -- pip install -r requirements.txt
sbe run -- mix deps.get
sbe run -- ./gradlew build
Why
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 macOS sandbox-exec (Seatbelt/TrustedBSD) kernel-level sandbox. No code changes, no new package manager. Just prefix your command with sbe run --.
What It Blocks
| Attack Vector | How sbe Stops It |
|---|---|
Read ~/.ssh, ~/.aws, cloud creds |
SBPL denies file-read to sensitive paths |
Write to /Library/Caches, LaunchAgents |
SBPL denies file-write outside project + cache dirs |
| Network C2 on non-standard ports | Proxy allows only registry domains on 443; SBPL blocks all direct outbound |
osascript / AppleScript abuse |
SBPL denies process-exec for risky binaries |
curl/wget 2nd-stage download |
Proxy rejects non-allowlisted domains with 403 |
| Clipboard / screen exfiltration | SBPL denies pbcopy, pbpaste, screencapture |
Install
Or from the workspace root:
Requires macOS (uses sandbox-exec which is macOS-only).
Quick Start
# Auto-detects ecosystem from command name or project files
# Specify ecosystem explicitly
# See what SBPL profile would be generated (does not execute)
# Print resolved config + SBPL
# List all default profiles
# Disable network sandboxing for debugging
# Add a custom allowed domain
# Allow build-time downloads (enables curl/wget + adds domains to proxy)
# Allow an extra binary
# Stream sandbox violations in real-time
Supported Ecosystems
| Ecosystem | Auto-detected commands | Auto-detected files | Default allowed domains |
|---|---|---|---|
| Node.js | node, npm, npx, yarn, pnpm, bun |
package.json |
registry.npmjs.org, registry.yarnpkg.com, registry.npmmirror.com, github.com, codeload.github.com, objects.githubusercontent.com |
| Rust | cargo, rustc, rustup |
Cargo.toml |
crates.io, static.crates.io, index.crates.io, static.rust-lang.org, github.com, codeload.github.com, objects.githubusercontent.com |
| Python | python, python3, pip, pip3, uv, poetry, pdm, rye |
pyproject.toml, setup.py, requirements.txt, Pipfile |
pypi.org, files.pythonhosted.org, github.com, codeload.github.com, objects.githubusercontent.com |
| Elixir | mix, elixir, iex |
mix.exs |
hex.pm, repo.hex.pm, builds.hex.pm, cdn.hex.pm, github.com, codeload.github.com, objects.githubusercontent.com |
| Java | java, javac, mvn, mvnw, gradle, gradlew, sbt, scala, scalac, kotlinc |
pom.xml, build.gradle, build.gradle.kts, build.sbt |
repo1.maven.org, plugins.gradle.org, github.com, codeload.github.com, objects.githubusercontent.com |
Configuration
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
Config resolution order (last wins):
- Built-in ecosystem defaults
- Global config:
~/.config/sbe/config.yaml - Project config:
.sbe.yamlor.sbe.yml(walks up to git root) - CLI flags
Architecture
sbe CLI
|
+-------------+-------------+
| | |
Profile SBPL Gen Proxy Server
Resolver (sbe-core) (sbe-proxy)
(sbe-core) | |
| v |
+-----> sandbox-exec <------+
(macOS kernel)
|
+-----------+
| Your Cmd |----> HTTP_PROXY=127.0.0.1:PORT
| (npm, | |
| cargo,..)| All outbound forced through proxy
+-----------+
Inside sandbox (kernel-enforced): Outside sandbox:
- file-write denied except allowlist - sbe-proxy on localhost
- file-read denied for secrets - Domain allowlist filtering
- network only to localhost proxy - Audit log collection
- risky binaries blocked - SBPL tempfile management
Two-layer network defense:
- Kernel layer (SBPL): blocks all outbound except
localhost:PROXY_PORT+ DNS - Application layer (proxy): HTTP CONNECT proxy checks domain against allowlist before tunneling
This combination solves SBPL's limitation of IP-only filtering — CDN-backed registries use dynamic IPs, so domain-level filtering requires the proxy.
Crate Structure
sbe/
├── crates/
│ ├── core/ # sbe-core: profiles, config, SBPL generation, detection
│ │ └── src/
│ │ ├── profile/ # Per-ecosystem defaults (node, rust, python, elixir, java)
│ │ ├── config.rs # YAML config loading + merging
│ │ ├── sbpl.rs # SBPL profile generator
│ │ ├── detect.rs # Ecosystem auto-detection
│ │ └── error.rs
│ └── proxy/ # sbe-proxy: domain-filtering HTTP CONNECT proxy
│ └── src/
│ ├── server.rs # Async TCP listener + CONNECT handler
│ ├── allowlist.rs # Domain matching (exact + wildcard)
│ └── error.rs
├── apps/
│ └── cli/ # sbe binary
│ └── src/
│ ├── main.rs # Entry point + tracing setup
│ ├── cli.rs # clap argument definitions
│ ├── executor.rs # Sandbox lifecycle orchestration
│ └── audit.rs # sandboxd log streaming
└── specs/ # Design documents (PRD, design, impl plan)
CLI Reference
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)
-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 (SBPL port-443-only mode)
--audit Stream sandbox violations to stderr
--audit-log <PATH> Write violations to file
--dry-run Print SBPL to stdout, do not execute
-c, --config <PATH> Use specific config file
-v, --verbose Verbose output
sbe inspect [OPTIONS] -- <COMMAND>...
Print resolved config + generated SBPL without executing.
Accepts the same profile/override options as `run` (except --dry-run and --verbose).
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).
Development
# Build
# Run tests (requires cargo-nextest)
# Or with standard cargo
# Format + lint + test
# Install locally
Requirements
- macOS (uses
sandbox-exec) - Rust 2024 edition (stable)
cargo-nextestformake test(optional —cargo testworks too)
How It Works
- Detect ecosystem from command name (
npm-> Node) or project files (Cargo.toml-> Rust) - Load profile — built-in defaults merged with global/project
.sbe.yamland CLI flags - Start proxy — bind HTTP CONNECT proxy on
127.0.0.1:0, get ephemeral port - Generate SBPL — deny-by-default policy with explicit exceptions for the ecosystem
- Write tempfile — SBPL to
/tmp/sbe-XXXX.sbwith mode 0400 - Execute —
sandbox-exec -f /tmp/sbe-XXXX.sb <command>withHTTP_PROXYenv injected - Monitor — optionally stream sandboxd violations via
log stream - Cleanup — stop proxy, remove tempfile, propagate exit code
License
This project is distributed under the terms of MIT.
See LICENSE for details.
Copyright 2025 Tyr Chen