sbx

command module
v0.6.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 18, 2026 License: MIT Imports: 25 Imported by: 0

README

sbx

CI Go dependencies license

One Go binary. Every branch, task or agent gets its own Postgres, Redis or browser - asleep at 0 B of memory, awake on the first connection: 191 ms for redis, about a second for postgres, and that first connection is held rather than refused.

Recorded from a real run by scripts/demo.sh - the wake shown is whatever that machine did at that moment, not the benchmark median.

sbx serve --idle 5m &                          # once per machine, not per sandbox
sbx create my-branch --template postgres       # 492 ms once the image is local
eval "$(sbx env my-branch)"                    # it remembers what it was made from
psql -U app -d app                             # this wakes it

There is no sbx start and no sbx stop. Opening a socket is the only signal, so psql, a connection pool, Playwright and a test runner all wake it without knowing it exists.


What it's for

Five situations. They differ mostly in who types the commands - the tool is the same.

the problem what sbx does
Branches every branch shares one database, so a migration on one is a migration on all - and a stack per branch costs full memory for every branch you ever opened a sandbox per branch. Only what you are looking at is resident - the ones nobody has queried cost 0 B of memory, which is the measured figure in BENCHMARKS.md
Agents an agent needs somewhere to work that dies with the task, and its clients - psql, a pool, a test runner - cannot call an SDK to wake anything shell commands are the whole integration. --shell json for parsing, sbx add for a service the spec never declared
Fan-out the expensive part of a sandbox per task is not the container, it is getting the data in seed once, sbx snapshot, then sbx fork as many as you want. The migration runs once
CI jobs spend longer waiting for a stack than running tests sbx ready blocks until it is genuinely serving. On a persistent runner the next job reuses warm, migrated state
A small team you want per-branch environments without buying a platform the same binary on a box you already own, plus sbx url for a link that wakes on open

→ USE-CASES.md for the why of each, with the numbers.


Features

Wakes on any TCP connection no SDK, no client library, no wrapper. Anything with a socket
Sleeps to 0 B a stopped container with its volume intact - an idle sandbox costs no memory at all
Holds the first connection it waits rather than refusing - 5/5 measured, where a rival that refuses scores 0/5
One static binary zero non-stdlib dependencies, CI-gated. darwin · linux · freebsd on amd64 · arm64; Windows via WSL2 (sbx cannot dial a Windows named pipe)
One committed file sandbox.json describes what a branch needs. → SPEC.md
A box for your own commands mounts puts the repository in a service with your toolchain, so sbx exec -t my-branch dev sh is a shell in your code - it sleeps like everything else and exec wakes it - how
Templates built in --template postgres works with nothing on disk. Pinned by digest, dated
Snapshot & fork save every service's data, then make as many sandboxes from it as you want
Builds your image build: {context} instead of image:, cached by a hash of the context - not a clock
Ordering & secrets depends_on for creation order, ${VAR} so a committed spec names a secret without holding it
Limits cpu, memory, gpus per service - a laptop running twenty sandboxes needs a ceiling
Egress deny a bridge with no NAT: nothing routed leaves, and it is still reachable and wakeable
Isolation tiers --isolation gvisor|kata, refused with a reason where the runtime is absent
Two runtimes the same spec locally on docker or in a cluster on kubernetes - not the same capabilities either way, and sbx doctor tells you what this host can do
Housekeeping sbx gc reclaims what dead sandboxes left, listing by default and deleting only with --force
Deploy it anywhere sbx pack writes the image for a platform that gives one container and one HTTP port; sbx connect turns that back into ordinary local ports - several deployments merge into one local port map, so a sandbox spread over a platform still looks like one - walkthrough
Drive a deployed sandbox sbx ui --connect https://sbx.example.dev - the same dashboard, on a sandbox that is somewhere else. Several deployments merge into one screen; the keys act on the deployment (wake, sleep, limit, remove, logs), and f forwards a service's ports here so psql reaches it. All authorised by the connect token, which is why it is a credential worth guarding
A live dashboard sbx ui - every sandbox, awake or not, with cpu and memory per service against what it is allowed, and a trace of where each has been. v folds it to one line per sandbox: what it holds, how many are up, what the whole thing costs, and each service's share. a shows the machine instead - every container on it, ours or not - and the title carries both machines, since on macOS the VM's ceiling and the laptop's are different numbers. Wake, sleep, read logs, set a limit and remove from the keyboard
Reads as well as prints --json on list, doctor, history and env, so an agent driving this parses answers instead of column widths. → AGENTS.md
History and audit sbx history records what changed and every wake, with secrets redacted. It reads a file, so it works when docker does not
Observability structured logs on one stdout; console/ adds metrics and health - a separate module, so it has dependencies and the daemon still has none

What each backend can do

The spec is the same file on both and the everyday commands are the same. The capabilities are not, and neither is the footing: locally a Docker-compatible runtime is a prerequisite - Docker Desktop, Colima, Rancher Desktop or rootless podman - and on macOS or Windows that runtime is a Linux VM, because a Linux container is a set of Linux kernel features. That is a property of containers rather than a gap here; on Linux there is no VM at all.

Where a capability is missing it is refused with a reason naming the backend rather than approximated, and sbx doctor says what this host can actually do. COMPARISON.md has the exhaustive table; the short version:

local · docker cluster · kubernetes
Wake on connect, sleep to 0 B, list · env · logs · exec · cp · rm · ready ✅ ✅ the everyday commands, identical on both
cpu / memory limits ✅ ⚠️ cpu and memory per service, and L in sbx ui. Docker adjusts the container in place; a cluster patches the Deployment, which rolls the pod — and needs credentials that may patch Deployments. The shipped activator Role deliberately cannot (deploy/activator.yaml grants deployments/scale only), so this works from your kubeconfig, not from inside the cluster
removing a limit once set ❌ ✅ docker's update API reads a zero as "leave unchanged", so a container keeps its ceiling until it is recreated
cpu / memory usage ✅ ❌ reading it from a cluster needs metrics-server, which is the operator's decision. Rows read n/a there rather than pretending a sample is coming
gpus: · snapshot · fork · gc · build: · prewarm · egress: "deny" · sbx url ✅ ❌ each refused in a cluster with a reason - the cluster answers are a device plugin, a CSI snapshot, a NetworkPolicy and an Ingress, none of which is the docker one in a hat
--isolation gvisor|kata · history · one committed sandbox.json ✅ ✅ a RuntimeClass in a cluster; refused wherever the runtime is absent

The dashboard

sbx ui - the fleet, what each service is using against what it is allowed, and where it has been. Recorded from a real run by scripts/ui-shot.sh rather than drawn, for the same reason as the demo above: a hand-drawn dashboard keeps a column that was renamed and a key that was rebound.

The bars and the traces are scaled to each service's own ceiling, so height means fullness, and every point is coloured by what was happening when it was drawn - a line can be red on the left and green on the right. Nothing has a ceiling until you give it one, so where there is none the trace is scaled and coloured against its own peak instead: the question it can still answer is "when was this service busy", and a service that never moves stays calm rather than being drawn against its own idle. L sets a limit on the selected service without leaving the dashboard.

Every address is a link. 127.0.0.1:20002 carries no scheme, so no terminal makes it clickable on its own; the dashboard marks them up, and iTerm2, WezTerm, Kitty, GNOME Terminal and Windows Terminal all open one on cmd- or ctrl-click. It assumes http://, which is the only scheme a browser can follow - a click on a postgres port opens a browser that cannot speak to it, which is the price of the ports where it can.


How to use

brew install aryanmehrotra/tap/sbx
# or
curl -fsSL https://raw.githubusercontent.com/aryanmehrotra/sbx/main/scripts/install.sh | sh
# or
go install github.com/aryanmehrotra/sbx@latest

Then run the daemon once - it owns the ports sbx env hands out, so nothing works without it. deploy/ has a launchd plist and a systemd unit, both running as you, not root. Check the machine first, and prove the whole cycle on it (~9 s once images are local):

sbx doctor       # what this host can and cannot do
sbx selftest     # create, sleep to zero, wake on a socket, data intact
A branch
git switch feature-x
sbx create feature-x                  # reads ./sandbox.json - `sbx init` writes one for you
eval "$(sbx env feature-x)"           # DATABASE_PORT=20002, REDIS_PORT=20003...
npm test                              # your tooling, unchanged

Switch away and it sleeps on its own. Switch back and the first query wakes it.

An agent
sbx create task-4711 --template postgres          # nothing on disk needed
sbx env task-4711 --shell json                    # {"DATABASE_HOST":"127.0.0.1", ...}
sbx add task-4711 cache --image redis:7-alpine --port 6379 --health 'redis-cli ping'

It connects with whatever tool it was going to use anyway, and the connection is the wake signal. If the agent runs code you did not write, add "egress": "deny" - and read the limits first.

One seeded database, many agents
sbx create main --template postgres
sbx exec main postgres psql -U app -d app -f /tmp/schema.sql   # seed once
sbx snapshot main golden
sbx fork golden agent-1                                        # as many as you want
sbx fork golden agent-2

A write in one is invisible to the others. Filesystem state only - processes start cold against warm data.

CI
sbx serve --idle 30m &
sbx create "$BRANCH" && sbx ready "$BRANCH"   # blocks until it is actually serving
eval "$(sbx env "$BRANCH")"
./run-tests.sh

sbx prewarm moves the image pull into a step your runner can cache.

On Kubernetes

The same spec, the same commands, --provider kubernetes:

sbx create my-branch --provider kubernetes --namespace sbx
sbx env    my-branch --provider kubernetes

Services become Deployments and Services in that namespace, and deploy/activator.yaml is the in-cluster component that plays the daemon's part. build: and egress: "deny" are refused there rather than approximated - USE-CASES.md explains why.

Every command
sbx create / rm make one from sandbox.json or --template, destroy it with its data
sbx env exports for your tooling - posix, fish, powershell, cmd, json
sbx ready wake it and block until it's serving - the CI one-liner
sbx exec [-t] run anything inside it; -t attaches a terminal for a shell or psql
sbx logs [-f] every service on one stdout, structured - the one command that does not wake anything
sbx cp files in and out (: marks the inside path)
sbx add drop in a service nobody declared - the agent affordance
sbx url a public link that wakes it when opened
sbx snapshot / fork save every service's data, then make as many sandboxes from it as you want
sbx init / validate ask what this branch needs and write the spec · check one without creating anything
sbx prewarm pull the images now, so the first create isn't a download
sbx gc reclaim volumes whose sandbox is gone; --snapshots includes saved states, --force actually deletes
sbx doctor what this machine can and cannot do, before you rely on it
sbx list · sbx ui what exists and what's awake · the same, live, with cpu and memory
sbx history · sbx templates what happened and who did it · the built-in specs
sbx serve the daemon - owns the ports, does all waking and sleeping; one per machine
sbx selftest the whole cycle, on your machine

Every command that touches a sandbox takes --provider docker|kubernetes, --namespace, --isolation container|gvisor|kata and --socket - serve takes all but --isolation, plus --idle, --refresh and --ready. SBX_PROVIDER_KIND, SBX_NAMESPACE and SBX_ISOLATION set the defaults, and DOCKER_HOST is honoured.


How it compares

Nothing has to call an SDK to wake a sandbox. A connection pool can't call sandbox.connect(), and neither can pg_dump, a migration tool or a test runner somebody else wrote. Fly's proxy and Knative's activator manage that too - Fly's is in Fly's cloud, Knative's is HTTP only. Any protocol, on your own machine, with no account is the corner nobody else is in.

And waking is not the hard part; holding the connection while it wakes is. A client that gets refused and does not retry is a client that fails, and most don't retry:

wakes on so the client can be first attempt served
sbx any TCP connection anything with a socket 5/5 measured, nginx and postgres
E2B · Daytona · Modal · Vercel · Cloudflare an SDK call only your own code your code retries
Fly Machines a request through Fly Proxy anything, incl. TCP held by the proxy
Knative an HTTP request HTTP/gRPC/WS only held by the activator
Lazytainer packets crossing a threshold anything that retries 0/5 measured - refused, then served 5 s later

That last row is the one measurement in this project where a live rival was run side by side and lost outright.

Yes, if your branches share one database; if an agent needs somewhere to work that dies with the task; if CI spends longer waiting for a stack than running tests.

No, if you need to run code you did not write - a container shares your kernel, and E2B, Vercel Sandbox or Modal give you a real one. Or if you want somebody else to operate it: that's Neon, and always will be.

→ COMPARISON.md - the full field, every claim sourced or measured, including where we lose and a table of the figures this project got wrong and corrected.

Honest limits

The wake is paid on connect, not on the query. A client that gives up connecting in two seconds will give up on a cold postgres. Raise its connect timeout - PGCONNECT_TIMEOUT for libpq - and expect a pooled client to see a server-initiated close when a sandbox sleeps.

A container shares the host kernel unless you run --isolation gvisor, which CI proves end to end. Egress can be denied but not filtered by domain. Memory is not restored, so a wake starts processes cold. This has not yet been run in production anywhere outside its own test suite - what is exercised on every push is below.


Benchmarks

Every number below was measured by a script in this repo, on the machine named beside it. → BENCHMARKS.md for conditions, distributions and how to re-run each.

Level 1 - the wake. What a caller waits for when it connects to a sleeping sandbox. The workload dominates, not sbx: a browser is slow because Chrome is slow to start.

xychart-beta
    title "Wake latency by workload - median ms, lower is better"
    x-axis ["redis", "chrome (warm)", "postgres", "kubernetes", "chrome (cold)"]
    y-axis "milliseconds" 0 --> 4600
    bar [191, 766, 931, 1534, 3744]

redis 191 · chrome warm 766 · postgres 931 · kubernetes 1534 · chrome cold 3744

Level 2 - against the field. Same harness, same machine, same rules: a sample counts only on a correct protocol reply, and the target must be verifiably asleep at t0.

xychart-beta
    title "sbx vs live rivals - wake ms (bars) · first attempt served in the label"
    x-axis ["sbx nginx 5/5", "zeropod 4/4", "sbx postgres 5/5", "Lazytainer 0/5"]
    y-axis "milliseconds" 0 --> 3400
    bar [174, 272, 931, 3286]

sbx nginx 174 (5/5) · zeropod 272 (4/4) · sbx postgres 931 (5/5) · Lazytainer 3286 (0/5)

zeropod is faster than it looks and restores RAM, which sbx does not - that row is the one we lose, and it is measured rather than conceded. Lazytainer's 3286 ms is mostly refusals: it served the first attempt zero times out of five.

Level 3 - the steady state. The wake is a one-off; these are what every operation pays for the life of the sandbox. Note the units - the scale spans five orders of magnitude:

level direct through sbx cost
a request on an open connection 14.5 µs 30.1 µs +15 µs
opening a new connection 0.69 ms 0.79 ms +0.1 ms, inside the noise
bulk transfer 12136 MB/s 6870 MB/s −43%

The middle row was 68 ms until this project measured it: the daemon re-ran the health check through docker exec on every accepted connection, and no benchmark here could see it because they all measured six bytes on one connection.

xychart-beta
    title "Bulk transfer - MB/s, higher is better"
    x-axis ["direct to docker", "through sbx"]
    y-axis "MB/s" 0 --> 13000
    bar [12136, 6870]

direct 12136 MB/s · through sbx 6870 MB/s

Level 4 - memory at rest. The number the whole design exists for.

xychart-beta
    title "Resident memory - MB, lower is better"
    x-axis ["sleeping sandbox", "sbx daemon", "mysql tuned", "clickhouse", "mysql stock"]
    y-axis "megabytes" 0 --> 420
    bar [0, 9.1, 110, 199, 411]

sleeping sandbox 0 · sbx daemon 9.1 · mysql tuned 110 · clickhouse 199 · mysql stock 411

A sleeping sandbox is 0 B of memory and the volume it already had. The daemon fronting every sandbox on the machine is 9.1 MB - corrected from a published 4.5 MB, which was wrong.


Docs

AGENTS.md pointing Claude, Codex or any agent at sbx - a block to paste, and the four things that are not obvious
SPEC.md every field of sandbox.json, and a docker-compose mapping
USE-CASES.md seven shapes this fits, and the ones it doesn't
ARCHITECTURE.md the pieces, both data paths, addressing
COMPARISON.md against E2B, Daytona, Modal, Fly, Neon, Knative, zeropod
BENCHMARKS.md every number, and the script that produced it
TROUBLESHOOTING.md what to do about what you're seeing
DECISIONS.md why it's shaped this way - mostly things that broke
console/ metrics, health and a read-only API for a running daemon
CONTRIBUTING.md how the test tiers are arranged, and what a change should come with
SECURITY.md the threat model, what counts as a vulnerability, and where to report one

MIT. Issues and patches welcome.

Documentation

Overview

sbx gives every branch, task or agent its own copy of a project's backing services, and charges nothing for the ones nobody is using.

A repo declares its services once, in sandbox.json. Anyone - a person, an agent, CI - creates a sandbox from that spec and gets their own containers on their own ports, with their own data. Those containers are stopped by default. `sbx serve` owns their public ports: a connection wakes the container behind it and is spliced through, and a few idle minutes stops it again. The caller sees a slow first query, never a failure.

sbx create feature-x          # realise the spec, once
sbx env feature-x             # exports your existing tooling already reads
sbx ready feature-x           # wake it and block until it is serving
sbx add feature-x pg --image postgres:16 --port 5432
sbx list
sbx rm feature-x
sbx serve --idle 5m           # the daemon; run one per machine

There is deliberately no start and no stop. Whatever can start a sandbox eventually becomes the thing that left one running, and then two components believe they own the lifecycle. Only the daemon does, because only the daemon can see demand.

Directories

Path Synopsis
internal
cli
history
Package history is the answer to "what happened to this sandbox, and who did it".
Package history is the answer to "what happened to this sandbox, and who did it".
hostinfo
Package hostinfo reports what the machine a person is sitting at has.
Package hostinfo reports what the machine a person is sitting at has.
selfstat
Package selfstat reads the resource usage of the container this process is in, from its own cgroup - no container runtime and no socket required.
Package selfstat reads the resource usage of the container this process is in, from its own cgroup - no container runtime and no socket required.
tui
ui
update
Package update answers "is there a newer sbx" without ever making anybody wait for it.
Package update answers "is there a newer sbx" without ever making anybody wait for it.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL