{"id":50856187,"url":"https://github.com/nick22985/sbx","last_synced_at":"2026-06-14T18:39:50.143Z","repository":{"id":358020207,"uuid":"1238914028","full_name":"nick22985/sbx","owner":"nick22985","description":"Sandbox Everything","archived":false,"fork":false,"pushed_at":"2026-06-02T15:10:31.000Z","size":5446,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-06-14T18:39:31.267Z","etag":null,"topics":["ai","autoclaude","claude","development","profile","sandbox","shai-hulud"],"latest_commit_sha":null,"homepage":"","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/nick22985.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-14T15:18:38.000Z","updated_at":"2026-06-12T13:02:09.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/nick22985/sbx","commit_stats":null,"previous_names":["nick22985/sbx"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/nick22985/sbx","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nick22985%2Fsbx","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nick22985%2Fsbx/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nick22985%2Fsbx/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nick22985%2Fsbx/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/nick22985","download_url":"https://codeload.github.com/nick22985/sbx/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nick22985%2Fsbx/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34333806,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-14T02:00:07.365Z","response_time":62,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ai","autoclaude","claude","development","profile","sandbox","shai-hulud"],"created_at":"2026-06-14T18:39:49.608Z","updated_at":"2026-06-14T18:39:50.132Z","avatar_url":"https://github.com/nick22985.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# sbx\n\nSandboxed Docker dev environments. Pick a flavor (npm, bun, rust, java,\nclaude, ...), `sbx init` a project, then `sbx shell` (or `sbx run`) to drop\ninto a container with your repo bind-mounted at the same path it lives at\non the host. Single Rust binary; dynamic shell completions via\n`clap_complete`.\n\n\u003cimg alt=\"sbx demo: cd into a project, sbx init bun, sbx config port add 3000, sbx run, dev server up. One command to a working sandbox.\" src=\"docs/images/hero.gif\"\u003e\n\n## Why\n\nModern dev environments are full of code you didn't write and don't audit:\ntransitive npm/pip/cargo dependencies, postinstall scripts, AI coding agents\nrunning shell commands, language servers, build plugins. Any one of them\nruns with your full user privileges by default, meaning access to your\nSSH keys, browser cookies, cloud credentials, shell history, and every\nother project on disk.\n\nThat's the blast radius supply-chain worms keep exploiting. The\n**Shai-Hulud** npm worm (Sept 2025) propagated through a single\ncompromised package and exfiltrated GitHub tokens, npm tokens, and cloud\ncredentials from anyone who installed it, turning developer machines into\nthe spreading mechanism. It wasn't the first and won't be the last:\nsimilar credential-stealing payloads keep shipping through compromised\npackages and prompt-injected agents.\n\n`sbx` shrinks that blast radius. Each project runs in its own container\nwith only the files it needs: the repo, declared mounts, scoped caches.\nNo host home directory, no SSH agent unless you opt in, no docker socket\nunless you opt in, no host network unless you opt in. If something inside\nthe sandbox tries to read `~/.aws/credentials` or `~/.ssh/id_ed25519`,\nthere's nothing there to read. Outbound network access can be gated\nthrough an allow-listed `host-proxy`, public exposure goes through a\nseparately-managed Cloudflare tunnel, and TLS termination happens in a\nsidecar so per-project secrets stay per-project.\n\nIt's not a security boundary as strong as a VM, but it's a meaningful\ndefault-deny for the day-to-day \"I just ran `npm install`\" risks.\n\n## Quick start\n\n```sh\n./install.sh                    # one-time, places `sbx` on $PATH\nsbx build base                  # build the base image\nsbx build npm                   # …and one flavor\ncd ~/code/some-project\nsbx init npm                    # tag the project; writes .sbx/config.toml\nsbx config port add 3000        # publish 3000 to the host\nsbx run                         # spin up \u0026 run config.toml's `start` (or shell in)\n```\n\nThat's the 90% case. Everything else (HTTPS, VPN, public URLs, multi-service\nsidecars, claude profiles…) is covered below.\n\n## Install\n\n```sh\n./install.sh\n```\n\nThen for completions, add to your shell rc:\n\n```sh\n# Bash\nsource \u003c(COMPLETE=bash sbx)\n# Zsh\nsource \u003c(COMPLETE=zsh sbx)\n# Fish\nCOMPLETE=fish sbx | source\n```\n\n`sbx completions \u003cshell\u003e` also prints a static completion script if you'd\nrather check one in.\n\nFlavors live under `~/.config/sbx/flavors/\u003cflavor\u003e/Dockerfile`. The repo\nships a working set in [`examples/config/flavors/`](examples/config/flavors/),\n`base`, `npm`, `bun`, `rust`, `java`, and `claude`. Copy the ones you\nwant into `~/.config/sbx/flavors/`, then `sbx build base` and\n`sbx build \u003cflavor\u003e` to bring them up.\n\n\u003cimg alt=\"tree ~/.config/sbx/flavors/ showing one directory per flavor (base, npm, bun, rust, java, claude), each containing a Dockerfile.\" src=\"docs/images/flavors-tree.webp\"\u003e\n\n\n\n## Project lifecycle\n\n```\nsbx init [-p] \u003cflavor\u003e  Mark cwd as \u003cflavor\u003e and build the image\n                        -p stores the marker in $SBX_PRIVATE_DIR instead of ./.sbx\nsbx                     Print top-level help\nsbx shell [cmd...]      Enter the project's container (or run `cmd` in it)\nsbx shell -f \u003cflavor\u003e [cmd...]\n                        Override the flavor (ad-hoc), with optional command\nsbx \u003cflavor\u003e            Ad-hoc transient shell of \u003cflavor\u003e in cwd\nsbx run                 Run `start` from .sbx/config.toml in a fresh container\nsbx sessions            List running sbx containers (alias: ps)\nsbx stop                Stop containers, services, and network sidecars\nsbx list                List available flavors\n```\n\n### Shadowing host tools (npm, bun, cargo, …)\n\n`sbx shell -f \u003cflavor\u003e [cmd...]` lets you run a one-shot command inside\nthe matching flavor container from *anywhere* on the host, without\n`sbx init`-ing the directory first. Pair it with shell aliases to\ntransparently route risky tools through their sandbox:\n\n```sh\n# ~/.bashrc / ~/.zshrc\nalias npm='sbx shell -f npm npm'\nalias npx='sbx shell -f npm npx'\nalias bun='sbx shell -f bun bun'\nalias bunx='sbx shell -f bun bunx'\nalias cargo='sbx shell -f rust cargo'\nalias rustc='sbx shell -f rust rustc'\n```\n\nNow `npm install` in any directory runs `npm install` inside the `npm`\nflavor's container with cwd bind-mounted at the same path. No host\n`node_modules` postinstall scripts, no host `cargo build.rs` running with\nyour credentials. If the project has its own `.sbx/config.toml`, drop the\n`-f` and the project's flavor is used automatically (`sbx shell npm install`).\n\nFlavor name vs. binary: the `-f` arg picks the **image**, the rest is\nthe **command**. Useful when they differ, e.g. `sbx shell -f rust cargo\nbuild` (the `rust` flavor ships `cargo`, not `rust`). `--flavor` and\n`--flavour` (British spelling) are both accepted as long forms of `-f`.\n\n## Images\n\n```\nsbx build [flavor|all]    Rebuild image(s)\nsbx rebuild [flavor|all]  Rebuild with --no-cache\nsbx clean [flavor]        Remove cache volumes\nsbx purge [flavor]        Remove caches + images (prompts)\nsbx scan [fs|image]       Full trivy scan\n```\n\n## Per-project config\n\nAll per-project state lives under `sbx config` (aliases: `cfg`, `conf`).\n\nA project's `config.toml` is resolved from up to three locations and\n**merged**: a private copy under `$SBX_PRIVATE_DIR` (defaults to\n`~/dotfiles/env/.config/.nickInstall/install/configs/private/sbx/\u003cpath\u003e`),\nthe git common dir's `.sbx/`, and the working tree's `.sbx/`. Scalar\nfields (`flavor`, `start`, `name`, `port-offset`) follow local-wins\nprecedence; list fields (`mounts`, `caches`, `ports`, `[[tunnel]]`) are\nconcatenated and deduped; boolean flags (`ssh`, `docker`, `gui`) are\nOR'd; map fields (`hostname`, `public`) merge with the local key\noverriding. `sbx config \u003cfield\u003e ...` writes to the local file only.\n\n```\nsbx config port     [list|add N|rm N]\nsbx config mount    [list|add SPEC|rm SPEC] [-g]   SPEC: host[:container][:ro]; -g targets the global config\nsbx config hostname [list|add HOST PORT|rm HOST]   Map HOST.sbx.localhost via the proxy sidecar\nsbx config tunnel   [list|add DIR L R|rm DIR L]    Forward TCP between host, sandbox, and remote (DIR: out/in/via/via-host)\nsbx config env      [list|set K=V|unset K]         Manages ~/.config/sbx/env\nsbx config start    [show|set \u003ccmd\u003e|clear]\nsbx config service  [list|add NAME|rm NAME]        Built-ins: redis, postgres, mongo, mysql, mailpit\nsbx config ssh      [on|off|status]                Mount $SSH_AUTH_SOCK on next start\nsbx config docker   [on|off|status]                Forward /var/run/docker.sock into the sandbox\nsbx config gui      [on|off|status]                Forward host X11 / Wayland sockets so GUI apps can render on the host\n```\n\n### Mounts\n\nExtra host paths can be made visible inside every sbx session (opt-in,\noff by default). Three layered sources, plus claude's `-m` flag:\n\n- `mounts = [...]` in `$XDG_CONFIG_HOME/sbx/flavors/\u003cflavor\u003e/config.toml`,\n  bound only when that flavor is active. Good for editor configs and\n  other host paths a single flavor needs (e.g. `~/.config/nvim` for the\n  `nvim` flavor).\n- `mounts = [...]` in `$XDG_CONFIG_HOME/sbx/config.toml`, global,\n  applied to **every** sbx session regardless of flavor. Good for\n  caches/tooling you always want (e.g. `~/.m2`, `~/.gradle`,\n  `~/.cache/pip`).\n- `mounts = [...]` in `./.sbx/config.toml`, per-project, layered on top\n  of the global file.\n\nEntry syntax (missing host paths are silently skipped):\n\n```\n\"host\"                     # same path on both sides\n\"host:container\"           # explicit container path\n\"host:container:ro\"        # read-only\n\"host::ro\"                 # same-path bind, read-only\n```\n\n`~/` on the host side expands to your host `$HOME`; `~/` on the\ncontainer side expands to the flavor's container home (e.g.\n`/home/dev`, or `~/` for `sbx claude` which mirrors the host home).\n\n**Mounting on top of a named volume.** Some flavors (e.g. `java`) bind a\nnamed docker volume over a container path for cache reuse. To inject\nyour own config without losing that cache, mount the single file *on\ntop* of the volume:\n\n```\n~/.m2/settings.xml:~/.m2/settings.xml:ro\n```\n\nThe container still gets the cache volume at `~/.m2/`, but Maven now\npicks up your host `settings.xml` (auth, mirrors, etc.).\n\n### Caches\n\nFlavor authors declare caches in `caches = [...]` inside\n`~/.config/sbx/flavors/\u003cflavor\u003e/config.toml`, alongside the `Dockerfile`. Each\nentry names a host path or named docker volume that survives between\nruns (e.g. `~/.npm`, `~/.cargo`, `@sbx-maven-cache:/home/dev/.m2`). Two\nextra layers let you add to or override those without editing the\nflavor's config:\n\n- `caches = [...]` in `$XDG_CONFIG_HOME/sbx/config.toml`, global,\n  applied to **every** sbx session regardless of flavor. Good for\n  caches you always want shared with the host (e.g. `.cargo/registry`,\n  `.npm`).\n- `caches = [...]` in `./.sbx/config.toml`, per-project, layered on top\n  of the global file.\n\nAll three layers use the same per-entry syntax:\n\n```\n.cache/pip                            # host bind: ~/.cache/pip -\u003e /home/dev/.cache/pip\n.m2:/home/dev/.m2                     # host bind, explicit container path\n@sbx-maven-mine:/home/dev/.m2         # named docker volume\n```\n\nOverride semantics: entries are merged by **container path**, with the\nproject config winning over the global config winning over the flavor\nconfig. So if the `java` flavor ships `@sbx-maven-cache:/home/dev/.m2`\nand you'd rather use your host's `~/.m2`, add `\".m2:/home/dev/.m2\"` to\n`caches` in `~/.config/sbx/config.toml` (or just the project's\n`.sbx/config.toml` for one project) and the host bind replaces the\nnamed volume. Missing host paths are auto-created on first run.\n\nUser-defined volumes are user-owned: `sbx clean` / `sbx purge` only\nremove volumes declared in the flavor's own `caches` list, so renaming\nthe active volume via an override won't trigger surprise deletions of\nyour data.\n\n### GUI forwarding (opt-in)\n\n`sbx config gui on` sets `gui = true` in `./.sbx/config.toml`; the next\ncontainer start mounts the host's Wayland and X11 sockets and forwards\n`DISPLAY`, `WAYLAND_DISPLAY`, and `XDG_RUNTIME_DIR` so GUI apps inside the\nsandbox render on the host (Electron apps, browsers, IDEs launched from a\nflavor shell, etc.).\n\n```sh\nsbx config gui on        # sets gui = true in ./.sbx/config.toml\nsbx config gui status    # shows whether forwarding is on + detected host sockets\nsbx config gui off       # sets gui = false\n```\n\n`sbx config gui status` prints whether forwarding is enabled and which\nof `WAYLAND_DISPLAY` / `DISPLAY` were detected on the host, useful when\nan app silently fails to open a window.\n\n## Networking\n\n\u003cpicture\u003e\n  \u003csource media=\"(prefers-color-scheme: dark)\" srcset=\"docs/images/architecture-dark.svg\"\u003e\n  \u003cimg alt=\"sbx network architecture: one shared netns (vpn / tailscale / first service / tunnel sidecar / sbx-container, first present owns it); a separate sbx-proxy-net bridge hosting sbx-proxy (Traefik) and sbx-public (cloudflared); sbx-host-proxy reached via host.docker.internal.\" src=\"docs/images/architecture-light.svg\"\u003e\n\u003c/picture\u003e\n\n```\nsbx net vpn       [status|use SPEC|auth|inline|off]\nsbx net tailscale [on [name]|off|status|auth [name]|list|rm name]\nsbx proxy         [status|routes|logs [-f]|stop]\nsbx tunnel        [status|logs [-f]|stop]\nsbx public        [list|add HOST PORT|rm HOST|login|status|logs [-f]|stop]\nsbx host-proxy    [on|off|status|list|allow HOST|disallow HOST|reload|logs [-f]|stop]\n```\n\n`sbx proxy` controls the shared Traefik sidecar that publishes\n`*.sbx.localhost` routes from `sbx config hostname` and from any container labels.\nThe Traefik dashboard is at http://traefik.sbx.localhost/dashboard/ whenever\nthe sidecar is up.\n\n### Exposing a project (four flavors)\n\n| Scope | Setup | URL |\n|---|---|---|\n| **Local plain HTTP** | `sbx config hostname add app.sbx.localhost 8080` | `http://app.sbx.localhost/` |\n| **Local HTTPS** (mkcert) | `sbx proxy mkcert` (once, needs host `mkcert`), then `sbx config hostname add app.sbx.localhost 8080` | `https://app.sbx.localhost/` |\n| **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/` |\n| **Public** (Cloudflare Tunnel) | `sbx public login` (once), then `sbx public add app.example.com 8080` | `https://app.example.com/` |\n\nAll four share the same internal Traefik proxy on `sbx-proxy-net`. You can mix\nthem in one project, e.g. `app.sbx.localhost` for fast local dev and\n`app.example.com` for a public preview link to share with a teammate.\n\n\u003cpicture\u003e\n  \u003csource media=\"(prefers-color-scheme: dark)\" srcset=\"docs/images/exposure-dark.svg\"\u003e\n  \u003cimg alt=\"How a request reaches the sandbox, by scope: (1) HTTP: browser to 127.0.0.1:80, Traefik routes to sandbox; (2) HTTPS: browser to 127.0.0.1:443, Traefik terminates TLS (mkcert or Let's Encrypt) and forwards plain HTTP to sandbox; (3) Public: browser to Cloudflare edge, cloudflared sidecar dials out over QUIC, then HTTP through Traefik to sandbox.\" src=\"docs/images/exposure-light.svg\"\u003e\n\u003c/picture\u003e\n\nVPN/Tailscale settings are stored per-project under `[network]` in\n`.sbx/config.toml` and applied on the next `sbx` shell start. Tailscale\nsupports multiple named profiles - each maps to its own\n`SBX_TAILSCALE_AUTHKEY[_\u003cNAME\u003e]` env var.\n\n### Tunnels\n\n`sbx config tunnel` forwards raw TCP between the host, the sandbox, and remote\nservices reachable via Tailscale/VPN. Four directions, written as `[[tunnel]]`\ntables in `.sbx/config.toml`:\n\n```toml\n[[tunnel]]                            # sandbox :3000 -\u003e host 127.0.0.1:3000\ndir = \"out\"\nleft = 3000\nright = 3000\n\n[[tunnel]]                            # host :5432 -\u003e sandbox localhost:5432\ndir = \"in\"\nleft = 5432\nright = 5432\n\n[[tunnel]]                            # host :5432 -\u003e remote :5432 through the sandbox netns\ndir = \"via\"\nleft = 5432\nright = \"db.staging.tail-net.ts.net:5432\"\n\n[[tunnel]]                            # sandbox -\u003e host.docker.internal:27017 -\u003e remote (uses host's netns)\ndir = \"via-host\"\nleft = 27017\nright = \"192.168.1.67:27017\"\n```\n\n`via:` is most useful with Tailscale/VPN on: the sandbox netns has tailnet routes\nand MagicDNS, so host tools (TablePlus, psql, etc.) can reach tailnet-only services\nwithout running Tailscale themselves. `in:` and `via:` spin up a small `alpine/socat`\nsidecar joined to the session's netns; `out:` is published via `-p` on the netns\nowner.\n\n`via-host:` is the inverse: the *sandbox* needs to reach a destination the **host**\ncan route to but the sandbox's own netns can't, most commonly a LAN/RFC1918\nservice when VPN is on (which has stolen the sandbox's default route). A separate\n`sbx-via-host-\u003cproject\u003e` sidecar runs on `--network host` (so it has the host's\nfull LAN/VPN routing) and listens on the docker bridge gateway at `LEFT`, forwarding\nto `RIGHT`. The sandbox connects to `host.docker.internal:LEFT`; this traffic\nexits the VPN netns via the bridge interface because gluetun's\n`FIREWALL_OUTBOUND_SUBNETS` already exempts the docker bridge subnet. The listener\nbinds to the bridge IP specifically, so LAN-side machines can't reach the forward.\n\n`sbx tunnel status` shows the configured tunnels and the state of the per-project\nsocat sidecars; `sbx tunnel logs [-f]` tails them; `sbx tunnel stop` tears them\ndown.\n\n### Host proxy (HTTPS pass-through via host)\n\n`sbx host-proxy` lets the sandbox reuse the **host's** outbound network for\nHTTPS, typically to reach a service that is only routable via the host's\nTailscale/VPN when you can't (or don't want to) run Tailscale inside the\ncontainer. A shared `sbx-host-proxy` sidecar runs `tinyproxy` on the host\nnetwork and the sandbox is given `https_proxy=http://host.docker.internal:8118`\nso any tool that honours `https_proxy` (curl, maven, npm, pip, go, …) goes\nthrough it. TLS is end-to-end, tinyproxy uses HTTP `CONNECT`, never\nterminates the TLS.\n\n```sh\nsbx host-proxy on                                  # sets [host_proxy] enabled = true\nsbx host-proxy allow repo.internal.example.com     # add a host to the allowlist\nsbx host-proxy allow '*.maven.org'                 # wildcard (subdomains)\nsbx host-proxy list                                # show this project's allowlist\nsbx host-proxy status                              # marker + sidecar + merged allowlist\nsbx run                                            # sidecar auto-starts on first session\n```\n\nThe `[host_proxy]` table in `.sbx/config.toml` carries `enabled` (the\non/off marker) and `allow` (the per-project allowlist). An **empty\n`allow`** with `enabled = true` means \"unrestricted for this project\".\nA non-empty list restricts proxied traffic to only those hosts (matched\nby a tinyproxy `Filter` with `FilterDefaultDeny Yes`). Wildcards:\n`foo.com` matches `foo.com` exactly; `*.foo.com` matches any subdomain.\n\nChanges to the allowlist hot-reload, `sbx host-proxy allow|disallow`\nrewrites the filter file and sends `SIGHUP` to the running sidecar. No\ncontainer restart, no session disruption.\n\nWhen to use it instead of `via:` tunnels: `via:` is best for raw TCP to a\nsingle known `host:port`; `host-proxy` is best when the sandbox already\ntalks HTTPS by URL (e.g. Maven repos behind a private Nexus) and you'd\nrather not enumerate every endpoint.\n\n**Shared-sidecar caveat.** A single `sbx-host-proxy` container serves\nevery project, so the active allowlist is the **union of every active\nproject's entries**. If project A allows only `repo.example.com` and\nproject B's `allow` is empty (meaning \"unrestricted for B\"),\nthe sidecar is still restricted to `[repo.example.com]` because A\ndemanded restrictions, B effectively inherits A's allowlist for the\nduration. To run a truly unrestricted host-proxy, none of the active\nprojects can have a non-empty allowlist. The same goes the other way:\nadding an entry anywhere strictens the proxy for everyone.\n\nTinyproxy listens on `0.0.0.0:8118` of the host network and only accepts\nRFC1918 + loopback clients (`Allow` rules in the generated config). TLS\nstays end-to-end (`CONNECT` tunnel, the proxy never terminates TLS).\nThe sidecar is reference-counted: it auto-starts when a session with\nthe marker spins up, and `stop_sidecar_if_idle` tears it down once no\nsandbox container still has `https_proxy` set in its env.\n\n### Public URLs (Cloudflare Tunnel)\n\n`sbx public` exposes a project on the public internet through a Cloudflare\nTunnel, no inbound ports, no DNS records to manage by hand. One-time setup:\n\n```sh\nsbx public login                            # browser flow; writes ~/.config/sbx/cloudflared/cert.pem\nsbx public add app.example.com 8080         # in your project dir, adds to [public] in .sbx/config.toml\nsbx run\n```\n\nFirst start creates a single shared `sbx-public` Cloudflare tunnel, registers\nthe CNAME (`HOST → sbx-public.cfargotunnel.com`), spins up a global\n`sbx-public` cloudflared sidecar on the proxy network, and routes traffic\nthrough the existing Traefik proxy, so `sbx config hostname` and\n`sbx public` share the same internal HTTP plane (CF terminates TLS at the\nedge). Multiple projects can register their own hostnames; the sidecar's\n`config.yml` is merged from per-project fragments.\n\n`sbx public status` shows sidecar / login / tunnel state and merged hostnames\nacross all active sessions. `sbx public logs [-f]` tails cloudflared;\n`sbx public stop` force-stops it. Hostnames added under `[public]` in\n`./.sbx/config.toml` are registered on the next `sbx run` and\nunregistered on session exit.\n\nThe `cert.pem` produced by `sbx public login` is the Cloudflare API\ncredential (account-scoped); the per-tunnel `credentials.json` next to it\nis what cloudflared actually uses at runtime. CF's dashboard makes you pick\na zone during login, but the resulting cert works for every zone in your\naccount.\n\n## sbx claude\n\n`sbx claude` is just an [agent](#agents-sbx-opencode--sbx-copilot--your-own)\nlike the rest, sharing the exact same CLI surface. Its `[agent]` config\nopts into two extra features: `--dangerously-skip-permissions` autonomy and\n`remote-control = true`.\n\n```\nsbx claude [-m PATH]... [-p PROFILE] [-s] [--rc] [--docker] [args...]\nsbx claude shell                  Drop to bash inside the sandbox\nsbx claude build|rebuild          Build/rebuild the claude image\nsbx claude profile [list|add NAME|rm NAME|current]\n```\n\nFlags (`-m`, `-p`, `-s`, `--rc`, `--docker`) come *before* the\nsubcommand: `sbx claude -m ~/projects/foo -p work shell`.\n\n`sbx claude` is independent of the project's flavor, you can launch it\non an npm/bun/rust/uninitialised project. It bind-mounts cwd at the same\npath it lives at on the host and the host's `~/.claude` rw (so auth,\nconfig, and history are shared). The image bundles node + bun + rust +\npython and the Claude Code CLI.\n\nBecause the container is already a sandbox, `sbx claude` auto-passes\n`--dangerously-skip-permissions` to `claude` so prompts don't get in the\nway. Pass `-s` / `--safe` to opt out for a single invocation, or pass\n`--dangerously-skip-permissions` yourself and it won't be duplicated.\n\nIn addition to the global / per-project [mount files](#mounts),\n`sbx claude` accepts `-m / --mount \u003cSPEC\u003e` repeated per invocation for\nad-hoc mounts: `sbx claude -m ~/projects/foo -m ~/.m2/settings.xml:~/.m2/settings.xml:ro`.\n\n`sbx claude` can opt a session into [Remote Control] so it's reachable\nfrom `claude.ai/code` and the Claude mobile app. When enabled, sbx\nappends `--remote-control \"\u003cproject\u003e-\u003cpid\u003e\"` to the inner `claude`\ninvocation. It's **off by default**, opt in with `--rc` for a single\nrun, set `SBX_REMOTE_CONTROL=1` in `~/.config/sbx/env` to default-enable\nit persistently, or pass your own `--remote-control` / `--rc` flag (sbx\nwon't double up).\n\n[Remote Control]: https://code.claude.com/docs/en/remote-control\n\n### Profiles\n\n`sbx claude profile add work` creates an isolated `~/.claude` clone under\n`$XDG_CONFIG_HOME/sbx/claude-profiles/work/` seeded from your host\n`.claude.json`. Use it with `sbx claude -p work`, or pin a project to a\nprofile by setting `[claude] profile = \"...\"` in `./.sbx/config.toml`. Useful for\nseparating personal/work logins or for keeping different MCP setups apart.\n\n### Docker socket forwarding (opt-in)\n\n`sbx config docker on` sets `docker = true` in `./.sbx/config.toml`; every\ncontainer start for that project then bind-mounts `/var/run/docker.sock`\nfrom the host and `--group-add`s the host docker GID so the unprivileged\nin-container user can talk to it. The base image ships the docker client\nbinary.\n\n`sbx claude` intentionally does *not* follow the project `docker` flag,\nopt in per-session with `--docker`, or globally with `SBX_DOCKER=1` in\n`~/.config/sbx/env`.\n\n**Security:** mounting the docker socket is effectively root on the host -\nanything inside the container can `docker run --privileged -v /:/host ...` and\nescape the sandbox. Only enable this when you trust what's running inside.\n\n## Agents (sbx opencode / sbx copilot / your own)\n\nAn **agent** is any flavor whose `config.toml` declares an `[agent]` block.\nSuch a flavor launches its CLI directly, with no per-agent Rust code, just a\nflavor directory:\n\n```\nsbx opencode [args...]                # opencode\nsbx copilot  [args...]                # GitHub Copilot CLI\nsbx \u003cagent\u003e  [-m PATH]... [-p PROFILE] [-s|--safe] [--docker] [args...]\nsbx \u003cagent\u003e  shell | bash             # drop to bash inside the sandbox\nsbx \u003cagent\u003e  build | rebuild          # build/rebuild the agent image\nsbx \u003cagent\u003e  profile [list|add NAME|rm NAME|current]\n```\n\nsbx's own flags (`-m`/`--mount`, `-p`/`--profile`, `-s`/`--safe`, `--docker`,\n`--shell`) come *before* any passthrough args, which are forwarded verbatim to\nthe inner CLI. The reserved verbs above (`shell`/`bash`, `build`, `rebuild`,\n`profile`) are claimed by sbx when they appear first, so they shadow any\nsame-named subcommand of the inner CLI. Agents are independent of the project's\nflavor and bind-mount cwd at its host path. `sbx claude` is one of these agents\n(it just enables a couple of extra features in its config, see\n[above](#sbx-claude)).\n\n### Defining an agent\n\nAdd `[agent]` to a flavor's `config.toml`:\n\n```toml\n[agent]\nbin = \"opencode\"        # binary to exec; defaults to the flavor name\npersist = [\".config/opencode\", \".local/share/opencode\"]  # host dirs bind-mounted for auth/config\nautonomy = [\"--allow-all\"]            # flags injected unless --safe / already present\nautonomy-detect = [\"--yolo\"]          # extra flags that count as \"already autonomous\"\nforward-env = [\"GH_TOKEN\"]            # host env vars forwarded when set\nprofiles = true                       # enable named logins (sbx \u003cagent\u003e profile ...)\n```\n\nPair it with a `Dockerfile` that installs the CLI and `sbx \u003cname\u003e` just works.\n\n### Autonomy\n\nSince the container is already a sandbox, agents auto-inject their `autonomy`\nflags so prompts don't get in the way. `sbx copilot` injects `--allow-all`;\nuse `--safe` to opt out for one invocation, or pass your own\n`--allow-all` / `--yolo` and it won't be duplicated. opencode declares no\nautonomy flag; it's autonomous by default and its prompting is configured via\nthe `permission` key in `opencode.json` (shared from the host through the\nmounted `~/.config/opencode`).\n\n### Auth and config\n\nThe `persist` dirs are bind-mounted from the host, so logins are shared with\nthe host install:\n\n- **opencode** persists `~/.config/opencode` and `~/.local/share/opencode`\n  (provider creds in `auth.json`, sessions). Log in once on the host with\n  `opencode auth login`, or do it inside the sandbox (`sbx opencode auth\n  login`); it persists either way.\n- **copilot** persists `~/.copilot` (auth + state) and forwards\n  `COPILOT_GITHUB_TOKEN` / `GH_TOKEN` / `GITHUB_TOKEN` from the host env when\n  set, so token auth works without an interactive `/login`.\n\nAgents honor the global / per-project [mount files](#mounts) plus ad-hoc\n`-m / --mount \u003cSPEC\u003e`, and the opt-in docker socket via `--docker` /\n`SBX_DOCKER=1` (same caveats as `sbx claude`).\n\n### Profiles\n\nAgents with `profiles = true` support multiple named logins, so you can keep\ne.g. a work and a personal account side by side:\n\n```\nsbx opencode profile add work        # create an empty profile\nsbx opencode profile list            # list profiles (* marks the active one)\nsbx opencode profile current         # print the active profile\nsbx opencode profile rm work         # delete a profile\nsbx opencode -p work [args...]       # run using that profile\n```\n\nEach profile lives at `$XDG_CONFIG_HOME/sbx/\u003cagent\u003e-profiles/\u003cname\u003e/` and holds\nits own copy of the agent's `persist` dirs. When a profile is active those dirs\nbind from the profile instead of your host home, so logins are fully isolated.\nA freshly added profile starts logged out, so sign in once inside it (`sbx\nopencode -p work auth login`) and it persists there. With no profile selected,\nthe agent binds from your host home and shares the host login.\n\nPin a project to a profile with a `[profiles]` entry in `./.sbx/config.toml`:\n\n```toml\n[profiles]\nopencode = \"work\"\ncopilot = \"personal\"\n```\n\nA `-p NAME` flag overrides the pin for one invocation. (`sbx claude` has the\nsame feature via its own `[claude] profile` key, see [above](#profiles).)\n\n## Files\n\nThree TOML scopes, one schema per scope, layered project \u003e global \u003e flavor:\n\n- `./.sbx/config.toml`                       - per-project: every project knob\n  (`flavor`, `ports`, `mounts`, `caches`, `ssh`, `docker`, `gui`, `start`,\n  `[network]`, `[hostname]`, `[public]`, `[[tunnel]]`, `[services]`,\n  `[host_proxy]`, `[claude]`, `[profiles]`, plus `name` / `port-offset`\n  worktree overrides).\n- `$XDG_CONFIG_HOME/sbx/config.toml`                  - global: `mounts` and `caches`\n  applied to every sbx session.\n- `$XDG_CONFIG_HOME/sbx/flavors/\u003cflavor\u003e/config.toml` - flavor: `mounts` and\n  `caches` shipped with the flavor's Dockerfile.\n\nPlus:\n\n- `./.sbx/Dockerfile`                                  - optional, layers on top of the flavor image.\n- `$XDG_CONFIG_HOME/sbx/flavors/\u003cflavor\u003e/Dockerfile`   - base image source per flavor.\n- `$XDG_CONFIG_HOME/sbx/env`                           - persistent env (KEY=value, chmod 600).\n- `$XDG_CONFIG_HOME/sbx/claude-profiles/\u003cn\u003e/`          - alternate `~/.claude` per profile.\n- `$XDG_CONFIG_HOME/sbx/\u003cagent\u003e-profiles/\u003cn\u003e/`         - isolated `persist` dirs per agent profile.\n\nSee [`examples/sbx/`](examples/sbx/) for an annotated project `config.toml`\nand [`examples/config/`](examples/config/) for the global + per-flavor layout.\n\nComing from an older sbx layout with separate files for `flavor`,\n`hostname`, `ports`, `mounts`, `caches`, etc., or with flavors at the\ntop level of `~/.config/sbx/`? Run `sbx migrate` once; it folds project\nfiles into `./.sbx/config.toml`, global `~/.config/sbx/{mounts,caches}`\ninto `~/.config/sbx/config.toml`, relocates each top-level flavor dir\ninto `~/.config/sbx/flavors/\u003cflavor\u003e/`, and consolidates each flavor's\n`mounts` and `caches` into a per-flavor `config.toml`. Project legacy originals land\nin `./.sbx/legacy/` and global ones in `~/.config/sbx/legacy/`.\n\nIn a git worktree, `.sbx/config.toml` is looked up in the worktree first,\nthen the shared bare/primary repo, then the private overlay\n(`$SBX_PRIVATE_DIR/\u003crel-path\u003e/.sbx/`).\n\n### Worktrees\n\nWhen `sbx` runs inside a linked git worktree it auto-derives a suffix from\nthe checked-out branch and applies it everywhere that needs to be unique\nacross worktrees:\n\n- `project_name` becomes `\u003crepo\u003e-\u003cbranch\u003e`, distinct containers, proxy\n  routes, sidecars, etc., so two worktrees of the same repo can run side\n  by side.\n- Hostnames under `[hostname]` and `[public]` in `.sbx/config.toml` are\n  auto-prefixed with `\u003cbranch\u003e-`. So a single shared\n  `\"app.sbx.localhost\" = 3000` under `[hostname]` yields\n  `https://app.sbx.localhost/` in the main checkout and\n  `https://server-live-app.sbx.localhost/` in a worktree on branch\n  `server-live`. The prefix is flat (dash, not dot) so the URL stays at the\n  same DNS depth as the original, existing wildcard certs\n  (`*.sbx.localhost`, `*.example.com`) keep working.\n- Published ports get a stable hash-derived offset in `[1, 9]` so two\n  worktrees of the same repo don't collide on the host (`master` / `main`\n  / non-worktree always use 0). Pin a specific offset by setting\n  `port-offset = N` in `.sbx/config.toml`.\n\nThe branch name is sanitized (`feature/foo` -\u003e `feature-foo`). To use\nsomething other than the branch name, set `name = \"exp1\"` in the worktree's\n`.sbx/config.toml`; its value replaces the suffix (so\n`name = \"exp1\"` -\u003e `exp1-app.sbx.localhost` and project_name `\u003crepo\u003e-exp1`).\nProject images (`sbx-\u003cflavor\u003e-\u003crepo\u003e`) are unaffected, they're shared\nacross worktrees.\n\n## Environment\n\n| Var | Meaning |\n|-----|---------|\n| `SBX_PORTS=3000,8080` | Extra ports to publish |\n| `CLOUDFLARE_DNS_API_TOKEN=…` | CF API token used by Traefik for ACME DNS-01 (real-domain local HTTPS) |\n| `SBX_ACME_EMAIL=…` | Contact email for Let's Encrypt registration. Required with `CLOUDFLARE_DNS_API_TOKEN` |\n| `SOCKET_CLI_API_TOKEN=…` | socket.dev API token (forwarded into containers that ship `socket`) |\n| `SOCKET_ORG_SLUG=…` | Default org for `socket scan create` |\n| `SBX_VPN_DIR=…` | Directory for bare VPN names |\n| `SBX_PRIVATE_DIR=…` | Read-only overlay for `.sbx` configs; also where `sbx init -p` writes |\n| `SBX_PROJECT_DIR=…` | Override the detected project root (set inside the sandbox; usually auto) |\n| `SBX_PROJECT` | Set *inside* sandboxes, full project name (e.g. `myapp-master`) |\n| `SBX_PROJECT_BASE` | Set inside sandboxes, repo base name without worktree suffix (e.g. `myapp`) |\n| `SBX_WORKTREE` | Set inside sandboxes, worktree suffix, empty in main checkout (e.g. `master`) |\n| `SBX_HOSTNAME` / `SBX_HOSTNAMES` | Set inside sandboxes, primary / all hostnames (public preferred over local, useful for OAuth/SAML callbacks) |\n| `SBX_LOCAL_HOSTNAME` / `SBX_LOCAL_HOSTNAMES` | Set inside sandboxes, first / all local hostnames from `[hostname]` (already prefixed) |\n| `SBX_PUBLIC_HOSTNAME` / `SBX_PUBLIC_HOSTNAMES` | Set inside sandboxes, first / all public hostnames from `[public]` |\n| `SBX_PORT` | Set inside sandboxes, primary published port (first entry in `ports`) |\n| `SBX_DOCKER=1` | Default `sbx claude` to mount the host docker socket |\n| `SBX_REMOTE_CONTROL=1` | Default `sbx claude` to enable `--remote-control` (off by default) |\n| `SBX_TAILSCALE_AUTHKEY[_\u003cNAME\u003e]=…` | Auth key for the default / named tailscale profile |\n| `SBX_TAILSCALE_EXTRA_ARGS=…` | Extra args appended to `tailscale up` |\n| `SBX_BUILDX_BUILDER=default` | Buildx builder to use for sbx's own builds (default: `default`; set empty to inherit `docker buildx use`) |\n\nPersist these in `~/.config/sbx/env` (KEY=value lines, chmod 600). Host env\nwins over the file.\n\n**Anything you put in `~/.config/sbx/env` is also forwarded into every sbx\ncontainer.** So `sbx config env set MY_API_KEY=…` (or editing the file\ndirectly) is enough to make `MY_API_KEY` available to your app inside the\nsandbox, no allowlist or prefix required. The sbx-internal vars above\n(`CLOUDFLARE_DNS_API_TOKEN`, `SBX_TAILSCALE_AUTHKEY*`, etc.) are\nforwarded too, treat the file as the single source of truth for \"env I\nwant in my sandboxes\" and keep host-only secrets out of it.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnick22985%2Fsbx","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnick22985%2Fsbx","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnick22985%2Fsbx/lists"}