A single-binary CLI for running AI coding agents in Docker Sandboxes. Two subcommands:
sbxgo setup: build or pull the Docker template image for your project, create the sandbox, and (after a confirmation prompt) attach to the agent. Run once when setting up, and again when the image source changes.sbxgo run: resume or create the sandbox for the current project, detecting drift in create-time config along the way. Your everyday command.
Inspired by maxkrivich/sbx-toolkit. sbxgo shifts to a project-level model: each project owns its own Dockerfile and config, committed to the repo, so the whole team gets the same environment without any per-machine setup step.
My hope is that sbx makes this project redundant over time as they add more functionality.
As of when creating this repo it was a bit of a pain to set up sbx on each new repository, especially when running with a deny-all policy for increased security.
So this tooling tries to make it easier to use sbx overall if you use it in a per-repository setup.
Requires sbx 0.34.0 or newer. Each
sbxgoinvocation checkssbx versionfirst and exits with a clear error if the installed client is too old. Upgrade from docker/sbx-releases.
Linux / macOS:
# latest
curl -fsSL https://raw.githubusercontent.com/HenrikPoulsen/sbxgo/main/install.sh | bash
# pin to a specific version
curl -fsSL https://raw.githubusercontent.com/HenrikPoulsen/sbxgo/main/install.sh | bash -s v0.3.0Windows (run from cmd.exe or PowerShell):
:: latest
powershell -c "irm https://raw.githubusercontent.com/HenrikPoulsen/sbxgo/main/install.ps1 | iex"
:: pin to a specific version
powershell -c "$env:SBXGO_VERSION='v0.3.0'; irm https://raw.githubusercontent.com/HenrikPoulsen/sbxgo/main/install.ps1 | iex"Both installers resolve the requested release (or the latest), download the platform-specific binary, and verify its SHA-256 against the release's checksums.txt. On mismatch the binary is deleted and the install aborts.
If you have Go installed, you can just do this:
go install github.com/HenrikPoulsen/sbxgo/cmd/sbxgo@latestPin to a version by replacing @latest with the tag (e.g. @v0.3.0). The binary lands in $GOBIN (or $GOPATH/bin); make sure that directory is on your PATH.
From your project repository:
sbxgo setupIf .sbxgo/config.toml does not exist, sbxgo setup creates one from a template and exits. Open the file and edit it to match your project, then run sbxgo setup again.
Commit .sbxgo/config.toml. Everyone on the team uses the same settings.
If your sandbox needs custom tooling, add [sandbox.docker] to .sbxgo/config.toml. Pick exactly one source:
Keep
[sandbox.docker]at the end of the file. In TOML, every key after a[table]header belongs to that table, so placing this section above plain[sandbox]keys (allowed_domains,kits, …) would pull them into[sandbox.docker]— sbxgo rejects such configs with an error pointing at the misplaced keys.
Build from a Dockerfile in your repo:
[sandbox]
agent = "claude"
[sandbox.docker.build]
# Both fields are optional and shown here with their defaults.
context = "."
dockerfile = ".sbxgo/Dockerfile"# .sbxgo/Dockerfile
ARG AGENT=claude-code
FROM docker/sandbox-templates:${AGENT}
ARG AGENT
USER root
RUN apt-get update && apt-get install -y ripgrep && rm -rf /var/lib/apt/lists/*
USER agentOr pull a pre-published image:
[sandbox]
agent = "claude"
[sandbox.docker]
image = "ghcr.io/acme/dev:1.4.0"Re-run sbxgo setup after changing the Dockerfile or bumping the image tag. Setup compares the resolved image ID against the last build and only reloads the sbx template when the ID has actually changed; rebuilding the same Dockerfile is fast.
Heads up: kits (see below) are usually a better fit for project-specific tooling than a custom image. Use kits unless you genuinely need a different base or layered system packages.
From your project repository:
sbxgo runResumes the existing sandbox, or creates a new one if it does not exist yet.
sbxgo run [flags]
Flags:
-D, --debug Pass --debug to every sbx invocation (verbose logging)
--dry-run Print what would happen without executing
--toml PATH Path to config.toml (default: .sbxgo/config.toml)
On each invocation, sbxgo run:
- Warns about any missing required secrets
- If the sandbox exists, checks for drift in create-time config (docker source, clone, extra_workspaces). On drift, prompts to recreate; if you decline, resumes the existing sandbox with a warning that the new config will not take effect until a recreate
- Resumes the existing sandbox, applying any
allowed_domains/denied_domainsrules that are not already in place. Kits are not re-applied on resume; kit content changes are caught by drift detection (step 2) instead - If no sandbox exists, creates one via
sbx create(which applies all configured kits), then applies sandbox-scoped policy rules, then attaches to the agent
The host-wide network policy is never modified by sbxgo, see the heads-up box below.
sbxgo setup [flags]
Flags:
-D, --debug Pass --debug to every sbx invocation (verbose logging)
--agent NAME Agent to use when scaffolding a new config (default: claude)
--force Skip both confirmation prompts (recreate + start-agent)
--dry-run Print what would happen without executing
--toml PATH Path to config.toml (default: .sbxgo/config.toml)
On each invocation, sbxgo setup:
- Warns about any missing required secrets
- If
docker.buildis set, builds the image and loads it into sbx as a named template, but only when the built image ID differs from the last setup. Ifdocker.imageis set instead, does nothing here: the reference goes straight tosbx create -tand sbx pulls it - If a sandbox already exists for this project, prompts to recreate it (skip with
--force); on confirm, removes it - Creates the sandbox via
sbx create - Applies
allowed_domains/denied_domainsfrom config as sandbox-scoped rules - Prompts "Start the agent now?" (defaults to yes;
--forceskips and attaches automatically). Decline to leave the sandbox dormant;sbxgo runlater will attach.
Kits are directories you commit to the repo that install tools and apply configuration inside the sandbox at startup. They are the preferred way to add project-specific tooling without baking a full custom Docker image.
A kit has two files:
.sbxgo/kits/my-kit/
spec.yaml # metadata and network rules
files/ # overlaid onto the sandbox filesystem root
.sbxgo/kits/tools/spec.yaml:
schemaVersion: "1"
kind: mixin
name: tools
description: Project dev tools
network:
allowedDomains:
- github.com
commands:
install:
- command: apt-get update -qq
user: "0"
description: Refresh apt index
- command: apt-get install -y --no-install-recommends ripgrep
user: "0"
description: Install ripgrep
- command: rm -rf /var/lib/apt/lists/*
user: "0"
description: Clean apt cacheThen reference it in .sbxgo/config.toml:
kits = [".sbxgo/kits/tools"]Kits are applied only when the sandbox is created (sbxgo setup or first sbxgo run). Network rules in spec.yaml are additive on top of network_policy.
Changing a kit's spec.yaml or files/ between runs counts as configuration drift: the next sbxgo run detects the change and prompts to recreate the sandbox. Re-applying a kit in a live sandbox is unsafe in practice (apt update races against existing locks, file overlays silently clobber in-sandbox state), so sbxgo deliberately refuses to do it.
Heads up: sbx has no
kit rm. Removing a kit from thekits = [...]list also counts as drift and prompts a recreate; until you accept, the kit's files and packages stay in the sandbox.
External kits (URL / OCI / ZIP): the drift check hashes the reference string only, not the remote payload. If the contents at that URL change without the URL changing, sbxgo cannot detect it; bump the version in the reference (or run
sbxgo setup) to force a recreate. Symlinks inside a local kit'sfiles/are similarly skipped from the hash; commit the resolved files instead if their contents need to drive drift.
Experimental upstream: sbx 0.31.0 marked kits as experimental and started emitting verbose errors on failure. Expect the kit API and behaviour to evolve; treat noisy output from
sbx createwhen applying kits as informational unless the command actually exits non-zero.
For more information and a collection of ready-made kits, see docker/sbx-kits-contrib.
The template written to .sbxgo/config.toml by sbxgo setup is config.toml.tmpl. The .sbxgo/.gitignore template is gitignore.tmpl.
| Field | Required | Default | Description |
|---|---|---|---|
agent |
yes | claude, codex, kiro, shell, etc. |
|
[sandbox.docker] |
Source of the template image. Set exactly one of image or [sandbox.docker.build], or omit the section to use sbx's default base. |
||
docker.image |
Registry reference, e.g. ghcr.io/acme/dev:1.4.0. Passed to sbx create -t verbatim; sbx pulls it. Needs no sbxgo setup. |
||
docker.build.context |
. |
Build context passed to docker build. |
|
docker.build.dockerfile |
.sbxgo/Dockerfile |
Path to the Dockerfile. | |
network_policy |
deny-all |
allow-all, balanced, or deny-all. Documentation only; sbxgo never changes the host-wide default. Set it with sbx policy init (renamed from set-default in sbx 0.34.0). |
|
clone |
false |
When true, pass --clone to sbx so the agent works in an in-container clone exposed back as the sandbox-<name> git remote (sbx 0.31.0+; replaces the removed branch field). |
|
required_secrets |
Secret names to check; missing ones warn, do not block | ||
allowed_domains |
Sandbox-scoped allow rules added on each run (sbx 0.29.0+). Re-add is idempotent. | ||
denied_domains |
Sandbox-scoped deny rules. Always wins over allow. | ||
kits |
Kit references applied at sandbox creation. Content changes count as drift and prompt a recreate on the next sbxgo run. |
||
extra_workspaces |
Extra host paths to mount into the sandbox |
Unrecognized keys are an error: a typo (allowd_domains) or a key swallowed by a mispositioned
[sandbox.docker] header fails sbxgo run/sbxgo setup with a message naming the key, instead of
being silently ignored. Configs still carrying the removed branch field get a migration hint
toward clone.
network_policy (base: allow-all / balanced / deny-all)
|
v
allowed_domains (additive)
|
v
denied_domains (always wins)
Heads up:
network_policyis the host-wide baseline (sbx policy init ...).sbxgodoesn't change it for you — it's documentation plus a reminder of the baseline to initialize on the host.allowed_domains/denied_domainsare scoped to this sandbox in sbx 0.29.0+ and applied on everysbxgo run; re-adding the same rule is a no-op (sbx reports "Already covered").
Secret values never appear in config. required_secrets lists names only. sbxgo run and sbxgo setup check sbx secret ls and warn if any are missing. Set them once per machine:
sbx secret set MY_SECRET_TOKENIssues and PRs welcome.
PR titles must follow Conventional Commits (e.g. feat: add foo, fix: handle nil case, feat!: rename Run to Start for breaking changes). The PR-title check enforces this. The repo is intentionally on 0.x, so feat! bumps the minor version, not the major (see .releaserc.yml); when we're ready to graduate to 1.0.0 we'll remove that override.
Design goals:
- Project-level: config and Dockerfiles live in the repo, not on the developer's machine
- Safe by default: secrets never touch the filesystem or image layers
- Testable: all external calls go through interfaces; no global state
Pushes to main trigger CI (lint + test) followed by semantic-release, which decides the next version from the conventional-commit history, creates the GitHub release with auto-generated notes, and then invokes goreleaser via successCmd to attach the binaries.
If goreleaser fails after semantic-release has already created the release (build flake, signing issue, transient network), the release will exist with notes but no binary assets. Recovery: re-run the release workflow on the same commit. goreleaser's release.mode: keep-existing setting preserves the body and just appends the missing artifacts on the next attempt, with no need to delete and re-tag.
MIT, see LICENSE. Third-party attributions are listed in THIRD_PARTY_LICENSES.md.