Sandboxed Docker dev environments. Pick a flavor (npm, bun, rust, java,
claude, ...), sbx init a project, then sbx shell (or sbx run) to drop
into a container with your repo bind-mounted at the same path it lives at
on the host. Single Rust binary; dynamic shell completions via
clap_complete.
Modern dev environments are full of code you didn't write and don't audit: transitive npm/pip/cargo dependencies, postinstall scripts, AI coding agents running shell commands, language servers, build plugins. Any one of them runs with your full user privileges by default, meaning access to your SSH keys, browser cookies, cloud credentials, shell history, and every other project on disk.
That's the blast radius supply-chain worms keep exploiting. The Shai-Hulud npm worm (Sept 2025) propagated through a single compromised package and exfiltrated GitHub tokens, npm tokens, and cloud credentials from anyone who installed it, turning developer machines into the spreading mechanism. It wasn't the first and won't be the last: similar credential-stealing payloads keep shipping through compromised packages and prompt-injected agents.
sbx shrinks that blast radius. Each project runs in its own container
with only the files it needs: the repo, declared mounts, scoped caches.
No host home directory, no SSH agent unless you opt in, no docker socket
unless you opt in, no host network unless you opt in. If something inside
the sandbox tries to read ~/.aws/credentials or ~/.ssh/id_ed25519,
there's nothing there to read. Outbound network access can be gated
through an allow-listed host-proxy, public exposure goes through a
separately-managed Cloudflare tunnel, and TLS termination happens in a
sidecar so per-project secrets stay per-project.
It's not a security boundary as strong as a VM, but it's a meaningful
default-deny for the day-to-day "I just ran npm install" risks.
./install.sh # one-time, places `sbx` on $PATH
sbx build base # build the base image
sbx build npm # …and one flavor
cd ~/code/some-project
sbx init npm # tag the project; writes .sbx/config.toml
sbx config port add 3000 # publish 3000 to the host
sbx run # spin up & run config.toml's `start` (or shell in)That's the 90% case. Everything else (HTTPS, VPN, public URLs, multi-service sidecars, claude profiles…) is covered below.
./install.shThen for completions, add to your shell rc:
# Bash
source <(COMPLETE=bash sbx)
# Zsh
source <(COMPLETE=zsh sbx)
# Fish
COMPLETE=fish sbx | sourcesbx completions <shell> also prints a static completion script if you'd
rather check one in.
Flavors live under ~/.config/sbx/flavors/<flavor>/Dockerfile. The repo
ships a working set in examples/config/flavors/,
base, npm, bun, rust, java, and claude. Copy the ones you
want into ~/.config/sbx/flavors/, then sbx build base and
sbx build <flavor> to bring them up.
sbx init [-p] <flavor> Mark cwd as <flavor> and build the image
-p stores the marker in $SBX_PRIVATE_DIR instead of ./.sbx
sbx Print top-level help
sbx shell [cmd...] Enter the project's container (or run `cmd` in it)
sbx shell -f <flavor> [cmd...]
Override the flavor (ad-hoc), with optional command
sbx <flavor> Ad-hoc transient shell of <flavor> in cwd
sbx run Run `start` from .sbx/config.toml in a fresh container
sbx sessions List running sbx containers (alias: ps)
sbx stop Stop containers, services, and network sidecars
sbx list List available flavors
sbx shell -f <flavor> [cmd...] lets you run a one-shot command inside
the matching flavor container from anywhere on the host, without
sbx init-ing the directory first. Pair it with shell aliases to
transparently route risky tools through their sandbox:
# ~/.bashrc / ~/.zshrc
alias npm='sbx shell -f npm npm'
alias npx='sbx shell -f npm npx'
alias bun='sbx shell -f bun bun'
alias bunx='sbx shell -f bun bunx'
alias cargo='sbx shell -f rust cargo'
alias rustc='sbx shell -f rust rustc'Now npm install in any directory runs npm install inside the npm
flavor's container with cwd bind-mounted at the same path. No host
node_modules postinstall scripts, no host cargo build.rs running with
your credentials. If the project has its own .sbx/config.toml, drop the
-f and the project's flavor is used automatically (sbx shell npm install).
Flavor name vs. binary: the -f arg picks the image, the rest is
the command. Useful when they differ, e.g. sbx shell -f rust cargo build (the rust flavor ships cargo, not rust). --flavor and
--flavour (British spelling) are both accepted as long forms of -f.
sbx build [flavor|all] Rebuild image(s)
sbx rebuild [flavor|all] Rebuild with --no-cache
sbx clean [flavor] Remove cache volumes
sbx purge [flavor] Remove caches + images (prompts)
sbx scan [fs|image] Full trivy scan
All per-project state lives under sbx config (aliases: cfg, conf).
A project's config.toml is resolved from up to three locations and
merged: a private copy under $SBX_PRIVATE_DIR (defaults to
~/dotfiles/env/.config/.nickInstall/install/configs/private/sbx/<path>),
the git common dir's .sbx/, and the working tree's .sbx/. Scalar
fields (flavor, start, name, port-offset) follow local-wins
precedence; list fields (mounts, caches, ports, [[tunnel]]) are
concatenated and deduped; boolean flags (ssh, docker, gui) are
OR'd; map fields (hostname, public) merge with the local key
overriding. sbx config <field> ... writes to the local file only.
sbx config port [list|add N|rm N]
sbx config mount [list|add SPEC|rm SPEC] [-g] SPEC: host[:container][:ro]; -g targets the global config
sbx config hostname [list|add HOST PORT|rm HOST] Map HOST.sbx.localhost via the proxy sidecar
sbx config tunnel [list|add DIR L R|rm DIR L] Forward TCP between host, sandbox, and remote (DIR: out/in/via/via-host)
sbx config env [list|set K=V|unset K] Manages ~/.config/sbx/env
sbx config start [show|set <cmd>|clear]
sbx config service [list|add NAME|rm NAME] Built-ins: redis, postgres, mongo, mysql, mailpit
sbx config ssh [on|off|status] Mount $SSH_AUTH_SOCK on next start
sbx config docker [on|off|status] Forward /var/run/docker.sock into the sandbox
sbx config gui [on|off|status] Forward host X11 / Wayland sockets so GUI apps can render on the host
Extra host paths can be made visible inside every sbx session (opt-in,
off by default). Three layered sources, plus claude's -m flag:
mounts = [...]in$XDG_CONFIG_HOME/sbx/flavors/<flavor>/config.toml, bound only when that flavor is active. Good for editor configs and other host paths a single flavor needs (e.g.~/.config/nvimfor thenvimflavor).mounts = [...]in$XDG_CONFIG_HOME/sbx/config.toml, global, applied to every sbx session regardless of flavor. Good for caches/tooling you always want (e.g.~/.m2,~/.gradle,~/.cache/pip).mounts = [...]in./.sbx/config.toml, per-project, layered on top of the global file.
Entry syntax (missing host paths are silently skipped):
"host" # same path on both sides
"host:container" # explicit container path
"host:container:ro" # read-only
"host::ro" # same-path bind, read-only
~/ on the host side expands to your host $HOME; ~/ on the
container side expands to the flavor's container home (e.g.
/home/dev, or ~/ for sbx claude which mirrors the host home).
Mounting on top of a named volume. Some flavors (e.g. java) bind a
named docker volume over a container path for cache reuse. To inject
your own config without losing that cache, mount the single file on
top of the volume:
~/.m2/settings.xml:~/.m2/settings.xml:ro
The container still gets the cache volume at ~/.m2/, but Maven now
picks up your host settings.xml (auth, mirrors, etc.).
Flavor authors declare caches in caches = [...] inside
~/.config/sbx/flavors/<flavor>/config.toml, alongside the Dockerfile. Each
entry names a host path or named docker volume that survives between
runs (e.g. ~/.npm, ~/.cargo, @sbx-maven-cache:/home/dev/.m2). Two
extra layers let you add to or override those without editing the
flavor's config:
caches = [...]in$XDG_CONFIG_HOME/sbx/config.toml, global, applied to every sbx session regardless of flavor. Good for caches you always want shared with the host (e.g..cargo/registry,.npm).caches = [...]in./.sbx/config.toml, per-project, layered on top of the global file.
All three layers use the same per-entry syntax:
.cache/pip # host bind: ~/.cache/pip -> /home/dev/.cache/pip
.m2:/home/dev/.m2 # host bind, explicit container path
@sbx-maven-mine:/home/dev/.m2 # named docker volume
Override semantics: entries are merged by container path, with the
project config winning over the global config winning over the flavor
config. So if the java flavor ships @sbx-maven-cache:/home/dev/.m2
and you'd rather use your host's ~/.m2, add ".m2:/home/dev/.m2" to
caches in ~/.config/sbx/config.toml (or just the project's
.sbx/config.toml for one project) and the host bind replaces the
named volume. Missing host paths are auto-created on first run.
User-defined volumes are user-owned: sbx clean / sbx purge only
remove volumes declared in the flavor's own caches list, so renaming
the active volume via an override won't trigger surprise deletions of
your data.
sbx config gui on sets gui = true in ./.sbx/config.toml; the next
container start mounts the host's Wayland and X11 sockets and forwards
DISPLAY, WAYLAND_DISPLAY, and XDG_RUNTIME_DIR so GUI apps inside the
sandbox render on the host (Electron apps, browsers, IDEs launched from a
flavor shell, etc.).
sbx config gui on # sets gui = true in ./.sbx/config.toml
sbx config gui status # shows whether forwarding is on + detected host sockets
sbx config gui off # sets gui = falsesbx config gui status prints whether forwarding is enabled and which
of WAYLAND_DISPLAY / DISPLAY were detected on the host, useful when
an app silently fails to open a window.
sbx net vpn [status|use SPEC|auth|inline|off]
sbx net tailscale [on [name]|off|status|auth [name]|list|rm name]
sbx proxy [status|routes|logs [-f]|stop]
sbx tunnel [status|logs [-f]|stop]
sbx public [list|add HOST PORT|rm HOST|login|status|logs [-f]|stop]
sbx host-proxy [on|off|status|list|allow HOST|disallow HOST|reload|logs [-f]|stop]
sbx proxy controls the shared Traefik sidecar that publishes
*.sbx.localhost routes from sbx config hostname and from any container labels.
The Traefik dashboard is at http://traefik.sbx.localhost/dashboard/ whenever
the sidecar is up.
| Scope | Setup | URL |
|---|---|---|
| Local plain HTTP | sbx config hostname add app.sbx.localhost 8080 |
http://app.sbx.localhost/ |
| Local HTTPS (mkcert) | sbx proxy mkcert (once, needs host mkcert), then sbx config hostname add app.sbx.localhost 8080 |
https://app.sbx.localhost/ |
| Local HTTPS (Let's Encrypt + Cloudflare DNS-01) | sbx config hostname add app.local.example.com 8080, plus CLOUDFLARE_DNS_API_TOKEN and SBX_ACME_EMAIL in ~/.config/sbx/env |
https://app.local.example.com/ |
| Public (Cloudflare Tunnel) | sbx public login (once), then sbx public add app.example.com 8080 |
https://app.example.com/ |
All four share the same internal Traefik proxy on sbx-proxy-net. You can mix
them in one project, e.g. app.sbx.localhost for fast local dev and
app.example.com for a public preview link to share with a teammate.
VPN/Tailscale settings are stored per-project under [network] in
.sbx/config.toml and applied on the next sbx shell start. Tailscale
supports multiple named profiles - each maps to its own
SBX_TAILSCALE_AUTHKEY[_<NAME>] env var.
sbx config tunnel forwards raw TCP between the host, the sandbox, and remote
services reachable via Tailscale/VPN. Four directions, written as [[tunnel]]
tables in .sbx/config.toml:
[[tunnel]] # sandbox :3000 -> host 127.0.0.1:3000
dir = "out"
left = 3000
right = 3000
[[tunnel]] # host :5432 -> sandbox localhost:5432
dir = "in"
left = 5432
right = 5432
[[tunnel]] # host :5432 -> remote :5432 through the sandbox netns
dir = "via"
left = 5432
right = "db.staging.tail-net.ts.net:5432"
[[tunnel]] # sandbox -> host.docker.internal:27017 -> remote (uses host's netns)
dir = "via-host"
left = 27017
right = "192.168.1.67:27017"via: is most useful with Tailscale/VPN on: the sandbox netns has tailnet routes
and MagicDNS, so host tools (TablePlus, psql, etc.) can reach tailnet-only services
without running Tailscale themselves. in: and via: spin up a small alpine/socat
sidecar joined to the session's netns; out: is published via -p on the netns
owner.
via-host: is the inverse: the sandbox needs to reach a destination the host
can route to but the sandbox's own netns can't, most commonly a LAN/RFC1918
service when VPN is on (which has stolen the sandbox's default route). A separate
sbx-via-host-<project> sidecar runs on --network host (so it has the host's
full LAN/VPN routing) and listens on the docker bridge gateway at LEFT, forwarding
to RIGHT. The sandbox connects to host.docker.internal:LEFT; this traffic
exits the VPN netns via the bridge interface because gluetun's
FIREWALL_OUTBOUND_SUBNETS already exempts the docker bridge subnet. The listener
binds to the bridge IP specifically, so LAN-side machines can't reach the forward.
sbx tunnel status shows the configured tunnels and the state of the per-project
socat sidecars; sbx tunnel logs [-f] tails them; sbx tunnel stop tears them
down.
sbx host-proxy lets the sandbox reuse the host's outbound network for
HTTPS, typically to reach a service that is only routable via the host's
Tailscale/VPN when you can't (or don't want to) run Tailscale inside the
container. A shared sbx-host-proxy sidecar runs tinyproxy on the host
network and the sandbox is given https_proxy=http://host.docker.internal:8118
so any tool that honours https_proxy (curl, maven, npm, pip, go, …) goes
through it. TLS is end-to-end, tinyproxy uses HTTP CONNECT, never
terminates the TLS.
sbx host-proxy on # sets [host_proxy] enabled = true
sbx host-proxy allow repo.internal.example.com # add a host to the allowlist
sbx host-proxy allow '*.maven.org' # wildcard (subdomains)
sbx host-proxy list # show this project's allowlist
sbx host-proxy status # marker + sidecar + merged allowlist
sbx run # sidecar auto-starts on first sessionThe [host_proxy] table in .sbx/config.toml carries enabled (the
on/off marker) and allow (the per-project allowlist). An empty
allow with enabled = true means "unrestricted for this project".
A non-empty list restricts proxied traffic to only those hosts (matched
by a tinyproxy Filter with FilterDefaultDeny Yes). Wildcards:
foo.com matches foo.com exactly; *.foo.com matches any subdomain.
Changes to the allowlist hot-reload, sbx host-proxy allow|disallow
rewrites the filter file and sends SIGHUP to the running sidecar. No
container restart, no session disruption.
When to use it instead of via: tunnels: via: is best for raw TCP to a
single known host:port; host-proxy is best when the sandbox already
talks HTTPS by URL (e.g. Maven repos behind a private Nexus) and you'd
rather not enumerate every endpoint.
Shared-sidecar caveat. A single sbx-host-proxy container serves
every project, so the active allowlist is the union of every active
project's entries. If project A allows only repo.example.com and
project B's allow is empty (meaning "unrestricted for B"),
the sidecar is still restricted to [repo.example.com] because A
demanded restrictions, B effectively inherits A's allowlist for the
duration. To run a truly unrestricted host-proxy, none of the active
projects can have a non-empty allowlist. The same goes the other way:
adding an entry anywhere strictens the proxy for everyone.
Tinyproxy listens on 0.0.0.0:8118 of the host network and only accepts
RFC1918 + loopback clients (Allow rules in the generated config). TLS
stays end-to-end (CONNECT tunnel, the proxy never terminates TLS).
The sidecar is reference-counted: it auto-starts when a session with
the marker spins up, and stop_sidecar_if_idle tears it down once no
sandbox container still has https_proxy set in its env.
sbx public exposes a project on the public internet through a Cloudflare
Tunnel, no inbound ports, no DNS records to manage by hand. One-time setup:
sbx public login # browser flow; writes ~/.config/sbx/cloudflared/cert.pem
sbx public add app.example.com 8080 # in your project dir, adds to [public] in .sbx/config.toml
sbx runFirst start creates a single shared sbx-public Cloudflare tunnel, registers
the CNAME (HOST → sbx-public.cfargotunnel.com), spins up a global
sbx-public cloudflared sidecar on the proxy network, and routes traffic
through the existing Traefik proxy, so sbx config hostname and
sbx public share the same internal HTTP plane (CF terminates TLS at the
edge). Multiple projects can register their own hostnames; the sidecar's
config.yml is merged from per-project fragments.
sbx public status shows sidecar / login / tunnel state and merged hostnames
across all active sessions. sbx public logs [-f] tails cloudflared;
sbx public stop force-stops it. Hostnames added under [public] in
./.sbx/config.toml are registered on the next sbx run and
unregistered on session exit.
The cert.pem produced by sbx public login is the Cloudflare API
credential (account-scoped); the per-tunnel credentials.json next to it
is what cloudflared actually uses at runtime. CF's dashboard makes you pick
a zone during login, but the resulting cert works for every zone in your
account.
sbx claude is just an agent
like the rest, sharing the exact same CLI surface. Its [agent] config
opts into two extra features: --dangerously-skip-permissions autonomy and
remote-control = true.
sbx claude [-m PATH]... [-p PROFILE] [-s] [--rc] [--docker] [args...]
sbx claude shell Drop to bash inside the sandbox
sbx claude build|rebuild Build/rebuild the claude image
sbx claude profile [list|add NAME|rm NAME|current]
Flags (-m, -p, -s, --rc, --docker) come before the
subcommand: sbx claude -m ~/projects/foo -p work shell.
sbx claude is independent of the project's flavor, you can launch it
on an npm/bun/rust/uninitialised project. It bind-mounts cwd at the same
path it lives at on the host and the host's ~/.claude rw (so auth,
config, and history are shared). The image bundles node + bun + rust +
python and the Claude Code CLI.
Because the container is already a sandbox, sbx claude auto-passes
--dangerously-skip-permissions to claude so prompts don't get in the
way. Pass -s / --safe to opt out for a single invocation, or pass
--dangerously-skip-permissions yourself and it won't be duplicated.
In addition to the global / per-project mount files,
sbx claude accepts -m / --mount <SPEC> repeated per invocation for
ad-hoc mounts: sbx claude -m ~/projects/foo -m ~/.m2/settings.xml:~/.m2/settings.xml:ro.
sbx claude can opt a session into Remote Control so it's reachable
from claude.ai/code and the Claude mobile app. When enabled, sbx
appends --remote-control "<project>-<pid>" to the inner claude
invocation. It's off by default, opt in with --rc for a single
run, set SBX_REMOTE_CONTROL=1 in ~/.config/sbx/env to default-enable
it persistently, or pass your own --remote-control / --rc flag (sbx
won't double up).
sbx claude profile add work creates an isolated ~/.claude clone under
$XDG_CONFIG_HOME/sbx/claude-profiles/work/ seeded from your host
.claude.json. Use it with sbx claude -p work, or pin a project to a
profile by setting [claude] profile = "..." in ./.sbx/config.toml. Useful for
separating personal/work logins or for keeping different MCP setups apart.
sbx config docker on sets docker = true in ./.sbx/config.toml; every
container start for that project then bind-mounts /var/run/docker.sock
from the host and --group-adds the host docker GID so the unprivileged
in-container user can talk to it. The base image ships the docker client
binary.
sbx claude intentionally does not follow the project docker flag,
opt in per-session with --docker, or globally with SBX_DOCKER=1 in
~/.config/sbx/env.
Security: mounting the docker socket is effectively root on the host -
anything inside the container can docker run --privileged -v /:/host ... and
escape the sandbox. Only enable this when you trust what's running inside.
An agent is any flavor whose config.toml declares an [agent] block.
Such a flavor launches its CLI directly, with no per-agent Rust code, just a
flavor directory:
sbx opencode [args...] # opencode
sbx copilot [args...] # GitHub Copilot CLI
sbx <agent> [-m PATH]... [-p PROFILE] [-s|--safe] [--docker] [args...]
sbx <agent> shell | bash # drop to bash inside the sandbox
sbx <agent> build | rebuild # build/rebuild the agent image
sbx <agent> profile [list|add NAME|rm NAME|current]
sbx's own flags (-m/--mount, -p/--profile, -s/--safe, --docker,
--shell) come before any passthrough args, which are forwarded verbatim to
the inner CLI. The reserved verbs above (shell/bash, build, rebuild,
profile) are claimed by sbx when they appear first, so they shadow any
same-named subcommand of the inner CLI. Agents are independent of the project's
flavor and bind-mount cwd at its host path. sbx claude is one of these agents
(it just enables a couple of extra features in its config, see
above).
Add [agent] to a flavor's config.toml:
[agent]
bin = "opencode" # binary to exec; defaults to the flavor name
persist = [".config/opencode", ".local/share/opencode"] # host dirs bind-mounted for auth/config
autonomy = ["--allow-all"] # flags injected unless --safe / already present
autonomy-detect = ["--yolo"] # extra flags that count as "already autonomous"
forward-env = ["GH_TOKEN"] # host env vars forwarded when set
profiles = true # enable named logins (sbx <agent> profile ...)Pair it with a Dockerfile that installs the CLI and sbx <name> just works.
Since the container is already a sandbox, agents auto-inject their autonomy
flags so prompts don't get in the way. sbx copilot injects --allow-all;
use --safe to opt out for one invocation, or pass your own
--allow-all / --yolo and it won't be duplicated. opencode declares no
autonomy flag; it's autonomous by default and its prompting is configured via
the permission key in opencode.json (shared from the host through the
mounted ~/.config/opencode).
The persist dirs are bind-mounted from the host, so logins are shared with
the host install:
- opencode persists
~/.config/opencodeand~/.local/share/opencode(provider creds inauth.json, sessions). Log in once on the host withopencode auth login, or do it inside the sandbox (sbx opencode auth login); it persists either way. - copilot persists
~/.copilot(auth + state) and forwardsCOPILOT_GITHUB_TOKEN/GH_TOKEN/GITHUB_TOKENfrom the host env when set, so token auth works without an interactive/login.
Agents honor the global / per-project mount files plus ad-hoc
-m / --mount <SPEC>, and the opt-in docker socket via --docker /
SBX_DOCKER=1 (same caveats as sbx claude).
Agents with profiles = true support multiple named logins, so you can keep
e.g. a work and a personal account side by side:
sbx opencode profile add work # create an empty profile
sbx opencode profile list # list profiles (* marks the active one)
sbx opencode profile current # print the active profile
sbx opencode profile rm work # delete a profile
sbx opencode -p work [args...] # run using that profile
Each profile lives at $XDG_CONFIG_HOME/sbx/<agent>-profiles/<name>/ and holds
its own copy of the agent's persist dirs. When a profile is active those dirs
bind from the profile instead of your host home, so logins are fully isolated.
A freshly added profile starts logged out, so sign in once inside it (sbx opencode -p work auth login) and it persists there. With no profile selected,
the agent binds from your host home and shares the host login.
Pin a project to a profile with a [profiles] entry in ./.sbx/config.toml:
[profiles]
opencode = "work"
copilot = "personal"A -p NAME flag overrides the pin for one invocation. (sbx claude has the
same feature via its own [claude] profile key, see above.)
Three TOML scopes, one schema per scope, layered project > global > flavor:
./.sbx/config.toml- per-project: every project knob (flavor,ports,mounts,caches,ssh,docker,gui,start,[network],[hostname],[public],[[tunnel]],[services],[host_proxy],[claude],[profiles], plusname/port-offsetworktree overrides).$XDG_CONFIG_HOME/sbx/config.toml- global:mountsandcachesapplied to every sbx session.$XDG_CONFIG_HOME/sbx/flavors/<flavor>/config.toml- flavor:mountsandcachesshipped with the flavor's Dockerfile.
Plus:
./.sbx/Dockerfile- optional, layers on top of the flavor image.$XDG_CONFIG_HOME/sbx/flavors/<flavor>/Dockerfile- base image source per flavor.$XDG_CONFIG_HOME/sbx/env- persistent env (KEY=value, chmod 600).$XDG_CONFIG_HOME/sbx/claude-profiles/<n>/- alternate~/.claudeper profile.$XDG_CONFIG_HOME/sbx/<agent>-profiles/<n>/- isolatedpersistdirs per agent profile.
See examples/sbx/ for an annotated project config.toml
and examples/config/ for the global + per-flavor layout.
Coming from an older sbx layout with separate files for flavor,
hostname, ports, mounts, caches, etc., or with flavors at the
top level of ~/.config/sbx/? Run sbx migrate once; it folds project
files into ./.sbx/config.toml, global ~/.config/sbx/{mounts,caches}
into ~/.config/sbx/config.toml, relocates each top-level flavor dir
into ~/.config/sbx/flavors/<flavor>/, and consolidates each flavor's
mounts and caches into a per-flavor config.toml. Project legacy originals land
in ./.sbx/legacy/ and global ones in ~/.config/sbx/legacy/.
In a git worktree, .sbx/config.toml is looked up in the worktree first,
then the shared bare/primary repo, then the private overlay
($SBX_PRIVATE_DIR/<rel-path>/.sbx/).
When sbx runs inside a linked git worktree it auto-derives a suffix from
the checked-out branch and applies it everywhere that needs to be unique
across worktrees:
project_namebecomes<repo>-<branch>, distinct containers, proxy routes, sidecars, etc., so two worktrees of the same repo can run side by side.- Hostnames under
[hostname]and[public]in.sbx/config.tomlare auto-prefixed with<branch>-. So a single shared"app.sbx.localhost" = 3000under[hostname]yieldshttps://app.sbx.localhost/in the main checkout andhttps://server-live-app.sbx.localhost/in a worktree on branchserver-live. The prefix is flat (dash, not dot) so the URL stays at the same DNS depth as the original, existing wildcard certs (*.sbx.localhost,*.example.com) keep working. - Published ports get a stable hash-derived offset in
[1, 9]so two worktrees of the same repo don't collide on the host (master/main/ non-worktree always use 0). Pin a specific offset by settingport-offset = Nin.sbx/config.toml.
The branch name is sanitized (feature/foo -> feature-foo). To use
something other than the branch name, set name = "exp1" in the worktree's
.sbx/config.toml; its value replaces the suffix (so
name = "exp1" -> exp1-app.sbx.localhost and project_name <repo>-exp1).
Project images (sbx-<flavor>-<repo>) are unaffected, they're shared
across worktrees.
| Var | Meaning |
|---|---|
SBX_PORTS=3000,8080 |
Extra ports to publish |
CLOUDFLARE_DNS_API_TOKEN=… |
CF API token used by Traefik for ACME DNS-01 (real-domain local HTTPS) |
SBX_ACME_EMAIL=… |
Contact email for Let's Encrypt registration. Required with CLOUDFLARE_DNS_API_TOKEN |
SOCKET_CLI_API_TOKEN=… |
socket.dev API token (forwarded into containers that ship socket) |
SOCKET_ORG_SLUG=… |
Default org for socket scan create |
SBX_VPN_DIR=… |
Directory for bare VPN names |
SBX_PRIVATE_DIR=… |
Read-only overlay for .sbx configs; also where sbx init -p writes |
SBX_PROJECT_DIR=… |
Override the detected project root (set inside the sandbox; usually auto) |
SBX_PROJECT |
Set inside sandboxes, full project name (e.g. myapp-master) |
SBX_PROJECT_BASE |
Set inside sandboxes, repo base name without worktree suffix (e.g. myapp) |
SBX_WORKTREE |
Set inside sandboxes, worktree suffix, empty in main checkout (e.g. master) |
SBX_HOSTNAME / SBX_HOSTNAMES |
Set inside sandboxes, primary / all hostnames (public preferred over local, useful for OAuth/SAML callbacks) |
SBX_LOCAL_HOSTNAME / SBX_LOCAL_HOSTNAMES |
Set inside sandboxes, first / all local hostnames from [hostname] (already prefixed) |
SBX_PUBLIC_HOSTNAME / SBX_PUBLIC_HOSTNAMES |
Set inside sandboxes, first / all public hostnames from [public] |
SBX_PORT |
Set inside sandboxes, primary published port (first entry in ports) |
SBX_DOCKER=1 |
Default sbx claude to mount the host docker socket |
SBX_REMOTE_CONTROL=1 |
Default sbx claude to enable --remote-control (off by default) |
SBX_TAILSCALE_AUTHKEY[_<NAME>]=… |
Auth key for the default / named tailscale profile |
SBX_TAILSCALE_EXTRA_ARGS=… |
Extra args appended to tailscale up |
SBX_BUILDX_BUILDER=default |
Buildx builder to use for sbx's own builds (default: default; set empty to inherit docker buildx use) |
Persist these in ~/.config/sbx/env (KEY=value lines, chmod 600). Host env
wins over the file.
Anything you put in ~/.config/sbx/env is also forwarded into every sbx
container. So sbx config env set MY_API_KEY=… (or editing the file
directly) is enough to make MY_API_KEY available to your app inside the
sandbox, no allowlist or prefix required. The sbx-internal vars above
(CLOUDFLARE_DNS_API_TOKEN, SBX_TAILSCALE_AUTHKEY*, etc.) are
forwarded too, treat the file as the single source of truth for "env I
want in my sandboxes" and keep host-only secrets out of it.

