sbexec 0.1.0

Run commands in a macOS sandbox with supply chain attack protection
sbexec-0.1.0 is not a library.

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

cargo install --path apps/cli

Or from the workspace root:

make install

Requires macOS (uses sandbox-exec which is macOS-only).

Quick Start

# 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

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):

  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

# 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. Executesandbox-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 for details.

Copyright 2025 Tyr Chen