# 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
| 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
```bash
cargo install --path apps/cli
```
Or from the workspace root:
```bash
make install
```
Requires macOS (uses `sandbox-exec` which is macOS-only).
## Quick Start
```bash
# 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 SBPL profile would be generated (does not execute)
sbe run --dry-run -- npm install
# Print resolved config + SBPL
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
```
## Supported Ecosystems
| **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:
```yaml
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):
1. Built-in ecosystem defaults
2. Global config: `~/.config/sbe/config.yaml`
3. Project config: `.sbe.yaml` or `.sbe.yml` (walks up to git root)
4. 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:**
1. **Kernel layer (SBPL):** blocks all outbound except `localhost:PROXY_PORT` + DNS
2. **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
```bash
# Build
make build
# Run tests (requires cargo-nextest)
make test
# Or with standard cargo
cargo test
# Format + lint + test
make check
# Install locally
make install
```
### Requirements
- macOS (uses `sandbox-exec`)
- Rust 2024 edition (stable)
- `cargo-nextest` for `make test` (optional — `cargo test` works too)
## How It Works
1. **Detect ecosystem** from command name (`npm` -> Node) or project files (`Cargo.toml` -> Rust)
2. **Load profile** — built-in defaults merged with global/project `.sbe.yaml` and CLI flags
3. **Start proxy** — bind HTTP CONNECT proxy on `127.0.0.1:0`, get ephemeral port
4. **Generate SBPL** — deny-by-default policy with explicit exceptions for the ecosystem
5. **Write tempfile** — SBPL to `/tmp/sbe-XXXX.sb` with mode 0400
6. **Execute** — `sandbox-exec -f /tmp/sbe-XXXX.sb <command>` with `HTTP_PROXY` env injected
7. **Monitor** — optionally stream sandboxd violations via `log stream`
8. **Cleanup** — stop proxy, remove tempfile, propagate exit code
## License
This project is distributed under the terms of MIT.
See [LICENSE](LICENSE.md) for details.
Copyright 2025 Tyr Chen