sboxd 0.1.9

Policy-driven command runner for sandboxed dependency installation
Documentation
# Progressive Adoption

You do not need to sandbox everything on day one. sbox is designed to be added to an existing project incrementally — one command at a time — without breaking your current workflow.

---

## Why incremental matters

The biggest reason teams don't adopt sandboxing tools is friction. If adopting a tool means rewriting your Makefile, changing your CI pipeline, and debugging five broken environment variables before anything works, it won't happen — especially right after an incident when you're already under pressure.

sbox is built so you can start with zero risk and add containment gradually as you understand the behavior.

---

## Stage 0 — Look before you touch anything

Start with `sbox plan`. It shows exactly what a sandboxed command would look like without running it. No container starts. Nothing changes on disk.

```bash
cd myproject
sbox init --preset node      # generates sbox.yaml in seconds
sbox plan -- npm install     # shows the resolved policy
```

The output tells you: which container image, which directories are mounted and how, which environment variables are filtered, what network policy applies, and which profile was selected. You can review this like a diff before committing to anything.

This step alone has value. It makes your install policy **visible and reviewable**. You can add `sbox.yaml` to source control and treat it as a security policy document, even before you run a single sandboxed command.

---

## Stage 1 — Sandbox the install step

The highest-risk command is `npm install` (or `pip install`, `cargo build`, etc.) — that is where postinstall scripts run. Sandbox that one command. Leave everything else alone.

```bash
sbox run -- npm install    # sandboxed: postinstall scripts cannot reach host
npm run build              # still on host
node server.js             # still on host
```

This works because `node_modules/` is a bind mount. When the container writes packages to `/workspace/node_modules/`, those files land on your host filesystem at `./node_modules/`. When the container exits, the files stay. `npm run build` on the host finds them exactly where it expects.

If something breaks at this stage, `sbox plan -- npm install` shows you why. The most common issue is a file the install script tried to write that is now read-only — fix it with `writable_paths`:

```yaml
workspace:
  writable_paths:
    - node_modules
    - package-lock.json   # add this if npm needs to update it
```

**The residual risk at this stage:** whatever landed in `node_modules/` runs on your host with your full privileges when you call `npm run build`. The sandbox contained the postinstall scripts during install. It did not contain what was installed. A malicious package could plant code in `node_modules/.bin/` that runs during your next build step. Stage 3 closes this gap.

---

## Stage 2 — Make it transparent with shims

Once the sandboxed install works, stop typing `sbox run --` manually. Install shims:

```bash
sbox shim
export PATH="$HOME/.local/bin:$PATH"   # add to ~/.bashrc or ~/.zshrc
```

Now `npm install` in any project that has `sbox.yaml` automatically runs through sbox. In projects without `sbox.yaml`, npm runs exactly as before — the shim is a no-op.

Your Makefile, your CI scripts, your muscle memory — none of it needs to change. The sandbox activates by presence of `sbox.yaml`, not by changing how you invoke commands.

---

## Stage 3 — Extend to build and run commands

This is where you close the residual risk from Stage 1. Add profiles and dispatch rules for your build and run commands:

```yaml
profiles:
  install:
    mode: sandbox
    network: on
    network_allow:
      - "*.npmjs.org"
    writable: true
    role: install
    no_new_privileges: true

  default:
    mode: sandbox
    network: off
    writable: true
    no_new_privileges: true

dispatch:
  npm-install:
    match:
      - "npm install*"
      - "npm ci"
    profile: install
  everything-else:
    match:
      - "npm run*"
      - "npx *"
      - "node *"
    profile: default
```

Now `npm run build`, `npx eslint`, and `node server.js` also run in a sandbox. The code in `node_modules/` never executes directly on your host — it always runs inside a container with `network: off`.

With shims installed (Stage 2), you still just type `npm run build`. The sandbox is invisible.

---

## Stage 4 — Lock it down in CI

Once the sandbox is stable in local development, enforce stronger guarantees in CI where the risk is highest:

```yaml
runtime:
  strict_security: true
  require_pinned_image: true
```

**What strict mode adds:**
- Refuses if the image is not pinned to a digest — prevents silent image drift
- Refuses if a lockfile is missing for install commands — enforces reproducibility
- Refuses if sensitive env vars are passed through — no accidental `NPM_TOKEN` leakage

Pin the image digest:

```bash
podman pull node:22-bookworm-slim
podman inspect node:22-bookworm-slim --format '{{index .RepoDigests 0}}'
# docker.io/library/node@sha256:abc123...
```

```yaml
image:
  ref: node:22-bookworm-slim
  digest: sha256:abc123...
```

Now even if someone pushes a malicious update to `node:22-bookworm-slim`, your CI runs the pinned version you reviewed.

---

## Common friction and fixes at each stage

### Stage 1 friction

**`EROFS: read-only file system`** — npm tried to write to a path not in `writable_paths`. Add it:
```yaml
workspace:
  writable_paths:
    - node_modules
    - package-lock.json
```

**`ENOENT` for a package tarball** — you passed a host path to npm but the container sees it at a different path. The workspace is mounted at `/workspace`, not at its host path. Use the container path:
```bash
sbox run -- npm install /workspace/mypackage-1.0.0.tgz
```

**Environment variable missing** — only vars in `pass_through` reach the container. Add what you need:
```yaml
environment:
  pass_through:
    - TERM
    - CI
    - MY_NEEDED_VAR
```

### Stage 2 friction

**Shim runs real npm instead of sbox** — the real binary is earlier in PATH. Check:
```bash
which npm   # must show ~/.local/bin/npm, not /usr/bin/npm
```
Make sure `export PATH="$HOME/.local/bin:$PATH"` is in your shell profile and your terminal reloaded it.

### Stage 3 friction

**Container startup is slow for build commands** — each `sbox run` starts a fresh container (~0.5–1s). For build/run commands where you want faster iteration, enable reuse:
```yaml
profiles:
  default:
    reuse_container: true
```
The container stays alive between calls. Use `sbox clean` to reset it.

**Tool not found inside container** — your build script calls something that isn't in the image. Either add it to the image or switch to a fuller base image:
```yaml
image:
  ref: node:22-bookworm   # full Debian, more tools available
```