sbx-go-sdk

module
v0.1.9 Latest Latest
Warning

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

Go to latest
Published: Jul 28, 2026 License: MIT

README

sbx-go-sdk

Go 1.25 sbx v0.37.0 Go Reference

A Go SDK for automating Docker Sandboxes (sbx) — isolated micro-VM environments provisioned for AI coding agents. Drive sandbox creation, command execution, interactive agent sessions, file transfer, ports, templates, network policy, and the daemon itself, all from Go.

ctx := context.Background()

c, _ := client.New(ctx, client.WithAutoStart())                 // talk to (or start) sandboxd
sb, _ := sandbox.Create(ctx, c,                                 // provision a sandbox
    sandbox.WithAgent("claude"),
    sandbox.WithWorkspace("."),
)
defer sb.Remove(ctx)

code, out, _ := exec.Exec(ctx, sb,                              // run a command inside it
    []string{"claude", "-p", "summarise the repo"},
    exec.WithAutoStart(),
)
body, _ := io.ReadAll(out)
fmt.Printf("exit %d:\n%s", code, body)

Contents


What is sbx?

sbx (Docker Sandboxes) provisions disposable, isolated micro-VMs for AI coding agents (claude, codex, copilot, cursor, gemini, opencode, shell, …). Each sandbox mounts one or more host directories as workspaces, runs behind a network-egress proxy, and is managed by a local background daemon, sandboxd.

sbx ships as a single binary that is both the CLI and the daemon (the dockerd model). This SDK talks to that daemon over its local unix socket and shells out to the binary where the daemon has no REST path — giving you the full sbx surface from Go without parsing CLI output.

Domain model

The SDK deliberately mirrors sbx's own vocabulary. Getting these distinctions right is the key to using the API correctly (full glossary in CONTEXT.md):

Term Meaning Don't confuse with
Sandbox The isolated micro-VM. The central resource. container, VM, box
Agent The AI tool running inside a sandbox. A sandbox is created for an agent. assistant, bot
Workspace A host directory mounted into the sandbox (append :ro for read-only). mount, volume
Create Provision a sandbox without attaching. (sbx create) run, new
Run Launch and interactively attach to the agent (creating the sandbox if needed). (sbx run) start, exec
Exec Run an arbitrary command inside the sandbox — not the agent. (sbx exec) run, shell
Start / Stop Bring the micro-VM up/down without removing it. pause, resume
Template A saved sandbox image new sandboxes can be created from. image, snapshot
Daemon (sandboxd) The local process the SDK talks to over a unix socket. server, engine

⚠️ Run ≠ "create + start". In this SDK, Run means the interactive agent session. To bring a stopped VM up, use Start. To run a one-off command, use exec.Exec.

How it works

The transport is hybrid (see docs/adr/0001):

  • REST over a unix socket for everything the daemon exposes — list, inspect, start, stop, remove, exec/attach (via a hijacked Docker stdcopy stream), ports (publish/unpublish), copying a file from a sandbox, policy list/check, templates, network log.
  • Shell-out to the sbx binary for orchestration-heavy operations with no REST client path — Create, agent Run, template save, copying a file to a sandbox, skills import, every kit operation, and policy/secret mutations.

The socket path is resolved with this precedence: client.WithSocketPath(...) > $DOCKER_SANDBOXES_API > the XDG default (~/.local/state/sandboxes/sandboxes/sandboxd/sandboxd.sock). You normally don't set it.

Set up sbx (prerequisite)

This SDK automates an existing Docker Sandboxes install — it does not bundle sbx. Install the CLI, sign in, and bring the daemon up before running any SDK code.

📚 Official docs: Get started with Docker Sandboxes · sbx CLI reference · Architecture · Releases

1. Install the CLI

Platform Command(s)
macOS (Apple silicon, macOS 14+) brew install docker/tap/sbx
Windows 11 (x86_64) winget install -h Docker.sbx
Linux (Ubuntu 24.04+, x86_64) curl -fsSL https://get.docker.com | sudo REPO_ONLY=1 sh then sudo apt-get install docker-sbx

2. Enable hardware virtualization (sandboxes are micro-VMs)

# Linux: verify KVM, then add yourself to the kvm group (log out/in afterwards)
lsmod | grep kvm
sudo usermod -aG kvm "$USER"
# Windows: enable the Hypervisor Platform once, from an elevated PowerShell
Enable-WindowsOptionalFeature -Online -FeatureName HypervisorPlatform -All

3. Sign in — opens a browser for Docker OAuth and prompts for a default network policy (Balanced is recommended). You also need an API key for your agent's model provider (e.g. Anthropic for claude) to actually run an agent.

sbx login

4. Verify the CLI and daemon are healthy:

sbx version    # CLI + daemon version (target this SDK against v0.37.0)
sbx diagnose   # diagnose install / daemon issues
sbx ls         # list sandboxes (an empty list means the daemon is reachable)

The sandboxd daemon starts automatically once you're authenticated. The SDK's client.WithAutoStart() also brings it up if it's down (it shells out to sbx daemon start --detach).

Install the SDK

go get github.com/squall-chua/sbx-go-sdk

Requires:

  • Go 1.25+.
  • The sbx binary on PATH (or set client.WithBinaryPath) — see Set up sbx above. The SDK shells out to it for create/run/cp/etc.
  • A reachable sandboxd — pass client.WithAutoStart() and the SDK will start it for you.

This SDK is built and live-verified against sbx / sandboxd v0.37.0 (daemon API 0.24.0); see Version alignment for how it tracks newer sbx releases.

Quick start

package main

import (
	"context"
	"fmt"
	"io"
	"log"

	"github.com/squall-chua/sbx-go-sdk/client"
	"github.com/squall-chua/sbx-go-sdk/exec"
	"github.com/squall-chua/sbx-go-sdk/sandbox"
)

func main() {
	ctx := context.Background()

	// 1. Connect; start the daemon if it isn't already running.
	c, err := client.New(ctx, client.WithAutoStart())
	if err != nil {
		log.Fatal(err)
	}

	// 2. Provision a sandbox for the "shell" agent over the current directory.
	sb, err := sandbox.Create(ctx, c,
		sandbox.WithAgent("shell"),
		sandbox.WithWorkspace("."),
	)
	if err != nil {
		log.Fatal(err)
	}
	defer sb.Remove(ctx) // disposable: clean it up when done

	// 3. Run a command. WithAutoStart brings the VM up if Create left it stopped.
	code, out, err := exec.Exec(ctx, sb,
		[]string{"sh", "-c", "echo hello from $(hostname)"},
		exec.WithAutoStart(),
	)
	if err != nil {
		log.Fatal(err)
	}
	body, _ := io.ReadAll(out)
	fmt.Printf("[exit %d] %s", code, body)
}

Package map

Package Import What it covers
client …/client Daemon connection, lifecycle (start/stop/status/health/version), options.
sandbox …/sandbox Create, list, inspect, start/stop/remove, interactive Run, cp, ports, save-template.
exec …/exec Run commands: capture, streaming, interactive attach (TTY + resize), detached, resource stats.
template …/template List / inspect / remove / load template images.
policy …/policy Network egress policy: defaults, allow/deny rules, profiles, proxy log.
secret …/secret Stored proxy-injected secrets and credential import (experimental upstream).
settings …/settings Read/write persistent sandboxd settings via sbx settings … --json.
ssh …/ssh Experimental SSH endpoint: enable/disable, setup, connection targets — composed over settings.
skillstore …/skillstore Import host agent-skill folders into sandboxd's shared skills store (experimental upstream).
kit …/kit Read and package kit artifacts: inspect, validate, pack, push, pull (experimental upstream).

Two return types are re-exported aliases so external callers never need internal/*: sandbox.Info (daemon sandbox description) and client.Runner (the sbx-binary runner).

Feature matrix

Every sbx feature, and whether this SDK exposes it. Since is the sbx release the feature shipped in; v0.32.0 is the SDK's baseline, the release it debuted against.

Meaning
Supported — the SDK wraps it.
⚠️ Partly supported — works, with a caveat named in the row.
Not supported — no SDK surface.

Maintainers: docs/sbx-version-coverage.md is the authority on which release a feature shipped in — it is reconciled against upstream release notes and wired into the drift gate. If a Since value here ever disagrees with that table, the table wins.

Sandboxes

Feature sbx Since SDK
Create a sandbox create v0.32.0 sandbox.Create
Interactive agent session run v0.32.0 sandbox.Run, sb.Run
List ls v0.32.0 sandbox.List, sandbox.Get
Inspect inspect v0.32.0 sb.Inspect ⚠️ auth mode and active sessions are absent from SandboxInfo
Start / stop / remove stop, rm v0.32.0 sb.Start, sb.Stop, sb.Remove
Stable per-sandbox ID v0.33.0 sb.ID()
Mount-policy-denied flag v0.33.0 sb.MountPolicyDenied()
Force-remove an active session rm -f v0.35.0 sandbox.WithForce()
Remove every sandbox at once rm --all v0.37.0 ❌ loop over sandbox.List
Publish ports at create time -p/--publish v0.37.0 sandbox.WithPublish
Clone, CPUs, memory, template, profile, name flags v0.32.0 sandbox.With*
Skip skill sharing --no-share-skills v0.37.0 ❌ gated off upstream by a remote feature flag

Exec

Feature sbx Since SDK
Run a command, capture output exec v0.32.0 exec.Exec
Stream stdout/stderr live exec v0.32.0 exec.WithMultiplexed
Interactive attach, TTY + resize exec v0.32.0 exec.ExecInteractive
Detached exec + poll v0.32.0 exec.ExecDetached, exec.InspectExec
Workdir, env, user, privileged flags v0.32.0 exec.With*
CPU / memory / disk snapshot TUI only v0.32.0 exec.Stats ⚠️ no daemon endpoint; execs a probe inside the sandbox
Follow a log file exec.Logs ✅ SDK convenience over ExecInteractive

Files & ports

Feature sbx Since SDK
Copy host → sandbox cp v0.32.0 sb.CopyTo ✅ shell-out; no REST upload path
Copy sandbox → host cp v0.32.0 sb.CopyFrom ✅ REST as of v0.37.0; auto-starts the sandbox
Follow source symlinks cp -L v0.33.0 sandbox.WithFollowSymlinks
Publish / list ports ports v0.32.0 sb.PublishPort, sb.Ports
Unpublish a port ports v0.32.0 sb.UnpublishPort ✅ REST as of v0.37.0

Templates

Feature sbx Since SDK
Snapshot a sandbox template save v0.32.0 sb.SaveTemplate ⚠️ the daemon refuses a running sandbox — stop it first
List / inspect / remove / load template v0.32.0 template.List, Inspect, Remove, Load

Network policy

Feature sbx Since SDK
Allow / deny / remove a rule policy v0.32.0 policy.Allow, Deny, RemoveRule
Reset rules, read the proxy log policy reset, policy log v0.32.0 policy.Reset, policy.Log
Set the default profile policy init v0.34.0 policy.SetDefault ✅ renamed from set-default in v0.34.0
List rules policy ls v0.32.0 policy.List, policy.ListRaw ✅ REST as of v0.37.0, always type=all
ls filters: --wide, --source, --decision, --include-inactive policy ls v0.35.0 ❌ the fields are on PolicyRule; filter client-side
Check whether an access is allowed policy check network v0.35.0 policy.Check
Governance / org status on a check v0.37.0 Authorization.Governance ⚠️ fields decoded; live behaviour unverified, no governed org to test against
Inspect a policy or rule policy inspect v0.35.0 policy.InspectRaw ⚠️ raw text — no --json upstream
List profiles policy profile ls v0.32.0 policy.Profiles ⚠️ raw text — no --json upstream

Secrets (experimental upstream)

Feature sbx Since SDK
Store a service token secret set v0.32.0 secret.SetToken ✅ written to stdin, never the argument vector
Store registry credentials secret set --registry v0.32.0 secret.SetRegistry ✅ written to stdin
Store a custom host secret secret set-custom v0.32.0 secret.SetCustom ⚠️ the value passes as a CLI argument — visible in the host process list
Host wildcards on a custom secret --host v0.33.0 secret.SetCustom ✅ the pattern passes straight through
Import credentials from host env secret import v0.35.0 secret.Import, secret.ImportAll
List / remove secret ls, secret rm v0.32.0 secret.List, ListRaw, Remove, RemoveCustom ⚠️ List parses the CLI table — no --json upstream
Store an OAuth token secret set --oauth v0.37.0 ❌ interactive, and openai/global only

Kits (experimental upstream)

Feature sbx Since SDK
Inspect a kit kit inspect v0.34.0 kit.Inspect ⚠️ upstream omits the files/ payload from --json
Validate a kit kit validate v0.34.0 kit.Validate ✅ OCI references are refused by the CLI
Pack a kit into a ZIP kit pack v0.34.0 kit.Pack
Push / pull an OCI v2 artifact kit push, kit pull v0.34.0 kit.Push, kit.Pull ⚠️ never completed against a live registry
Attach kits at create time create --kit v0.34.0 sandbox.WithKit
Add a kit to an existing sandbox kit add v0.35.0 sb.AddKit ⚠️ the CLI refuses kits declaring credentials, ports, volumes or startup commands
Read a sandbox's kit list inspect v0.35.0 sb.Kits ⚠️ read from the com.docker.sandbox.kits label; empty if upstream renames it
Restrict kit sources kit.allowedSources v0.34.0 settings.Set ✅ via the generic settings API

Settings, SSH & skills

Feature sbx Since SDK
Get / list / set / unset a setting settings (hidden) v0.32.0 settings.Get, List, Set, Unset ✅ writes are fire-and-forget; the daemon reloads within ~5s
Enable / disable the SSH endpoint feature.ssh v0.34.0 ssh.Enable, Disable, Enabled ✅ enabled by default as of v0.37.0
Write the SSH host config setup ssh v0.37.0 ssh.Setup, ssh.TargetFor ✅ path renamed from ssh setup in v0.37.0; both still work
Import host skills into the shared store skills import v0.37.0 skillstore.Import

Daemon

Feature sbx Since SDK
Start / stop / status daemon (hidden) v0.32.0 client.WithAutoStart, c.StartDaemon, c.StopDaemon, c.DaemonStatus
Liveness and version v0.32.0 c.Health, c.DaemonHealth, c.Info /daemon/health; the standalone GET /health was removed in v0.35.0
Read / set log levels daemon log-level v0.32.0 c.LogLevels, c.SetLogLevel
Daemon self-check report v0.32.0 c.Diagnostics ⚠️ raw JSON; not the same as the sbx diagnose install checker
Reset all state reset v0.32.0 c.Reset ✅ wipes every sandbox and all daemon state
Client/daemon version negotiation POST /version removed v0.37.0 c.CheckVersion ❌ deprecated — upstream removed the route, so this always errors
Diagnose an install diagnose v0.32.0 ❌ never wrapped
Sign in / sign out login, logout v0.32.0 ❌ interactive browser OAuth
Interactive host-config wizard setup v0.34.0 ❌ interactive; distinct from secret import
Terminal dashboard tui v0.32.0 ❌ out of scope for a library

Create-request fields the daemon accepts but sbx create cannot pass — Environment, SecretsScope, PullPolicy, RootFilesystemSize, DindVolumeSize, EnableVirtiofsCache, Display, AgentOptions, BindingsPath, CredentialValues — are unreachable from the SDK too, because creation shells out. Use exec.WithEnv for environment variables.


Usage guide

1. Connect to the daemon
// Default: resolve the socket, don't start anything.
c, err := client.New(ctx)

// Common production setup: ensure the daemon is up.
c, err := client.New(ctx,
	client.WithAutoStart(),                 // start sandboxd if down, wait up to ~30s
	client.WithHTTPTimeout(30*time.Second), // per-request REST timeout
)

// Overrides (rarely needed):
client.WithSocketPath("/custom/sandboxd.sock")
client.WithBinaryPath("/opt/sbx/bin/sbx")

// Deprecated — see "Version alignment" / ADR 0004:
client.WithStrictVersion() // hard-fails on any api_version difference, which fires on every sbx release

Daemon introspection and control:

h, _ := c.Health(ctx)                  // liveness: status, version
st, _ := c.DaemonStatus(ctx)           // Running bool + Socket (down daemon => Running:false, nil err)
res, _ := c.CheckVersion(ctx)          // deprecated: POST /version was removed in sbx v0.37.0, always errors now
info, _ := c.Info(ctx)                 // socket paths
_ = c.EnsureRunning(ctx)               // start + wait-for-healthy if needed
_ = c.SetLogLevel(ctx, "proxy", "debug")
_ = c.StopDaemon(ctx)                  // shut sandboxd down

⚠️ Avoid c.Reset(ctx) unless you mean it: it wipes all sandboxes and daemon state.

2. Create a sandbox

Create provisions a sandbox and returns a hydrated handle. It does not attach an agent and may leave the VM stopped — pair it with exec.WithAutoStart() or sb.Start(ctx) before running commands.

sb, err := sandbox.Create(ctx, c,
	sandbox.WithAgent("claude"),            // required: the agent this sandbox is for
	sandbox.WithWorkspace("."),             // required: at least one; repeatable
	sandbox.WithWorkspace("../shared:ro"),  // ":ro" => read-only mount
	sandbox.WithName("review-bot"),         // optional; otherwise auto-named "<agent>-<dir>"
	sandbox.WithCPUs(4),
	sandbox.WithMemory("8g"),
	sandbox.WithTemplate("myimg:v1"),       // base on a saved template
	sandbox.WithProfile("balanced"),        // governance profile
	sandbox.WithClone(),                    // run on an in-container git clone, not a bind mount
	sandbox.WithPublish("8080"),            // publish ports atomically with create ("-p", sbx v0.37.0+)
	sandbox.WithKit("./my-kit"),            // attach kit artifacts at creation ("--kit", sbx v0.34.0+)
)

The SDK owns the sandbox's identity: when WithName is omitted it derives a sanitized, collision-free <agent>-<workspace-basename> name, so it never has to parse create output. Re-using an existing name returns client.ErrSandboxExists.

3. List & inspect
all, _ := sandbox.List(ctx, c)            // []*sandbox.Sandbox
sb, _ := sandbox.Get(ctx, c, "review-bot")

// Cheap accessors off the last-known state:
sb.Name()        // string
sb.Agent()       // "" if unset
sb.State()       // status string
sb.IsRunning()   // bool

// Inspect refreshes from the daemon and returns the full record (sandbox.Info):
info, _ := sb.Inspect(ctx)
fmt.Println(info.Status, info.Workspace, info.Ports)
4. Lifecycle: start / stop / remove
_ = sb.Start(ctx)   // bring the micro-VM up
_ = sb.Stop(ctx)    // bring it down, keep the sandbox
_ = sb.Remove(ctx)  // delete it (no confirmation prompt)

// Force-remove one with an active session (e.g. an open SSH connection):
_ = sb.Remove(ctx, sandbox.WithForce())
5. Exec commands

exec.Exec runs a command to completion. The sandbox must be running — pass exec.WithAutoStart() to transparently start a stopped one (otherwise you get client.ErrSandboxNotRunning).

Capture output:

code, out, err := exec.Exec(ctx, sb, []string{"go", "test", "./..."},
	exec.WithWorkdir("/workspace"),
	exec.WithEnv(map[string]string{"CGO_ENABLED": "0"}),
	exec.WithAutoStart(),
)
body, _ := io.ReadAll(out) // demuxed stdout; stderr is discarded in capture mode

Stream stdout and stderr live to your own writers (the returned reader is then empty):

code, _, err := exec.Exec(ctx, sb, []string{"npm", "run", "build"},
	exec.WithMultiplexed(os.Stdout, os.Stderr),
)

Other exec options: WithUser("root"), WithPrivileged(), WithTTY().

Interactive attach — a live bidirectional session (stdin/stdout + TTY resize):

sess, _ := exec.ExecInteractive(ctx, sb, []string{"bash"}, exec.WithTTY())
defer sess.Close()

io.WriteString(sess.Stdin(), "ls -la\n")
_ = sess.Resize(ctx, 120, 40)
go io.Copy(os.Stdout, sess.Stdout())
code, _ := sess.Wait(ctx)

Follow a log fileexec.Logs is a convenience wrapper that runs tail -F <path> under an interactive attach and hands back the live session. Read Stdout() to stream new lines until you Close(); -F keeps following across log rotation and waits for a not-yet-created file. For a full replay or different flags, call ExecInteractive with your own command.

sess, _ := exec.Logs(ctx, sb, "/var/log/app.log")
defer sess.Close()
io.Copy(os.Stdout, sess.Stdout()) // streams continuously

Detached — fire-and-forget; poll for completion:

id, _ := exec.ExecDetached(ctx, sb, []string{"sh", "-c", "long-job"})
state, _ := exec.InspectExec(ctx, sb, id) // State{ Running, ExitCode }

Resource stats — a point-in-time CPU/memory/disk/uptime snapshot, read from /proc and df inside the sandbox (the same metrics the sbx TUI shows). There is no daemon stats endpoint; Stats simply execs a tiny probe, so the sandbox must be running (or pass exec.WithAutoStart()). It samples CPU over a ~200ms window, so the call blocks briefly.

u, err := exec.Stats(ctx, sb)
// exec.Usage{ Cores, MemTotalKB, MemAvailableKB, MemUsedKB, CPUPercent, UptimeSeconds, DiskTotalGB, DiskUsedGB }
fmt.Printf("cpu %.1f%% / %d cores, mem %d/%d MiB, disk %.0f/%.0f GiB\n",
	u.CPUPercent, u.Cores, u.MemUsedKB/1024, u.MemTotalKB/1024, u.DiskUsedGB, u.DiskTotalGB)

CPUPercent is the mean utilization across all cores, clamped to 0–100; memory is in KiB, disk (root filesystem) in GB. UptimeSeconds and the Disk* fields are best-effort — they read 0 if the sandbox can't supply them (e.g. a busybox df without -BG), without failing the CPU/memory snapshot.

6. Run an agent interactively

sandbox.Run is the agent session: it creates the sandbox if missing, then attaches your terminal to the agent and blocks until it exits. A non-zero agent exit is (code, nil) — only spawn/wait failures return a non-nil error. It returns no handle (use Create/Get for that).

// Provision-if-missing + attach the agent to your terminal:
code, _ := sandbox.Run(ctx, c,
	sandbox.WithAgent("claude"),
	sandbox.WithWorkspace("."),
	sandbox.WithAgentArgs("-p", "fix the failing test"), // args after "--"
)

// Re-attach to an existing sandbox's agent:
code, _ = sb.Run(ctx, sandbox.WithAgentArgs("--continue"))

// Redirect stdio (default is os.Stdin/out/err):
sandbox.Run(ctx, c, sandbox.WithAgent("shell"), sandbox.WithWorkspace("."),
	sandbox.WithStdio(myIn, myOut, myErr))
7. Copy files

CopyTo (host → sandbox) shells out to the sbx binary. CopyFrom (sandbox → host) reads GET /sandbox/{name}/files over REST (this endpoint answered 404 through v0.35.0; it works as of v0.37.0) and extracts the tar stream itself. CopyFrom auto-starts a stopped sandbox to match sbx cp's own behaviour — unlike exec, which requires an explicit WithAutoStart().

_ = sb.CopyTo(ctx, "./config.json", "/home/user/config.json")
_ = sb.CopyTo(ctx, "./link", "/home/user/target", sandbox.WithFollowSymlinks())
_ = sb.CopyFrom(ctx, "/home/user/out.log", "./out.log")
8. Publish ports
// Publish one mapping (additive); returns the full published set.
ports, _ := sb.PublishPort(ctx, sandbox.Port{
	SandboxPort: 8080,
	HostIP:      "127.0.0.1",
	Protocol:    "tcp",
	// HostPort: 0 => the daemon assigns an ephemeral host port
})

ports, _ = sb.Ports(ctx)                          // list published ports
_ = sb.UnpublishPort(ctx, "127.0.0.1:18080:8080/tcp") // remove by CLI spec
9. Templates

Templates are images. Snapshot a sandbox, then create new sandboxes from it.

// The daemon refuses to snapshot a running sandbox — stop it first.
_ = sb.Stop(ctx)
_ = sb.SaveTemplate(ctx, "myimg:v1")

imgs, _ := template.List(ctx, c)                  // []template.Image
img, _ := template.Inspect(ctx, c, "myimg:v1")
_ = template.Remove(ctx, c, "myimg:v1")

f, _ := os.Open("image.tar")
_ = template.Load(ctx, c, f)                      // import a tar into the image store
10. Network policy

Governs which hosts a sandbox may reach. Scope "" is global; a sandbox name scopes a rule.

_ = policy.SetDefault(ctx, c, "balanced")            // "allow-all" | "balanced" | "deny-all"
_ = policy.Allow(ctx, c, "", "api.github.com")       // global allow
_ = policy.Deny(ctx, c, "review-bot", "evil.test")   // per-sandbox deny
_ = policy.RemoveRule(ctx, c, "review-bot", "evil.test")  // resource selector required
_ = policy.Reset(ctx, c)

rules, _ := policy.List(ctx, c, "")                  // []policy.PolicyRule (REST, always type=all)
raw, _ := policy.ListRaw(ctx, c, "")                 // unparsed text escape hatch
prof, _ := policy.Profiles(ctx, c)                   // raw text (no --json upstream)
log, _ := policy.Log(ctx, c)                         // structured: allowed/blocked hosts
for _, e := range log.BlockedHosts {
	fmt.Println("blocked:", e.Host, e.VMName)
}

// Ask whether the current policy would authorize an access, without connecting:
auth, _ := policy.Check(ctx, c, "evil.test")                          // *policy.Authorization
fmt.Println(auth.Allowed, auth.Rule, auth.Reason)
_, _ = policy.Check(ctx, c, "api.github.com", policy.WithCheckSandbox("review-bot"))

detail, _ := policy.InspectRaw(ctx, c, "review-bot-net-allow") // human-rendered detail; selector from List
11. Secrets

Stored, proxy-injected credentials. Experimental upstream — for headless agent credentials, prefer exec.WithEnv instead.

_ = secret.SetCustom(ctx, c, "", secret.CustomSecret{
	Host:  "api.example.com", // requests to this host get the real value
	Env:   "API_KEY",         // env var (set to a placeholder) inside the sandbox
	Value: "sk-...",          // the real secret
})

// SetToken/SetRegistry write the secret to the child process's stdin — it never
// appears in the argument vector, unlike SetCustom above.
_ = secret.SetToken(ctx, c, "", "anthropic", os.Getenv("ANTHROPIC_API_KEY"))
_ = secret.SetRegistry(ctx, c, "", secret.RegistryCredential{
	Host:     "ghcr.io",
	Username: "me",
	Password: os.Getenv("GHCR_TOKEN"),
})

// Detect and store credentials already present in the host environment:
_ = secret.Import(ctx, c, "openai")                    // one named service (OPENAI_API_KEY)
_ = secret.ImportAll(ctx, c, secret.WithDryRun())      // preview everything detected; drop WithDryRun to write

secrets, _ := secret.List(ctx, c, "")      // *secret.Secrets{Stored, Custom}
raw, _ := secret.ListRaw(ctx, c, "")       // unparsed text escape hatch
_ = secret.Remove(ctx, c, "", "openai")              // a service/registry secret (positional name)
_ = secret.RemoveCustom(ctx, c, "", "api.example.com") // a custom secret (keyed by host)

⚠️ SetCustom passes the value as a CLI argument, so it is briefly visible in host process listings. Don't use it for high-sensitivity secrets in shared environments.


12. Settings & SSH

settings wraps sbx settings … --json; ssh manages the experimental SSH endpoint (composed over settings). Both shell out to the sbx binary.

// Read a setting (typed JSON value).
s, _ := settings.Get(ctx, c, "ssh.autoCreate")
auto, _ := s.Bool()                          // or s.Text() for string settings

// Write / clear an override (fire-and-forget; daemon hot-reloads within ~5s).
_ = settings.Set(ctx, c, "kit.allowedSources", `["docker.io/","ghcr.io/"]`)
_ = settings.Unset(ctx, c, "kit.allowedSources")

// Enable and connect to the SSH endpoint.
_ = ssh.Enable(ctx, c)             // settings set feature.ssh true
_ = ssh.Setup(ctx, c)              // writes ~/.ssh/config "Host *.sbx" + known_hosts (no key)
tgt := ssh.TargetFor("mybox")      // {Host:"mybox.sbx"}
fmt.Println(tgt.Command())         // ssh mybox.sbx

Set/Enable/etc. are fire-and-forget — they write settings.json and return; the daemon reloads within ~5s. ssh.Enable toggles only feature.ssh; SSH also requires platform.allowExperimentalFeatures (default true).

13. Skills

skillstore imports the host's agent skill folders into sandboxd's shared skills store (sbx skills import, experimental upstream, added in v0.37.0). Imported skills survive sandbox deletion; sbx reset clears the store. Shells out to the sbx binary.

_ = skillstore.Import(ctx, c)                          // --force: replaces existing skills (CLI backs them up first)
_ = skillstore.Import(ctx, c, skillstore.WithDryRun()) // preview only, writes nothing
14. Kits

A kit is a declarative artifact — a spec.yaml plus an optional files/ directory — that contributes configuration to a sandbox. A kit of kind "mixin" extends an existing sandbox; one of kind "sandbox" supplies the base image instead. Experimental upstream. Every call shells out to the sbx binary; the daemon has no kit REST path.

Attach a kit to a sandbox:

// At creation — the only way to apply a kit in full.
sb, _ := sandbox.Create(ctx, c,
	sandbox.WithAgent("claude"),
	sandbox.WithWorkspace("."),
	sandbox.WithKit("./my-kit", "ghcr.io/me/kit:v1"), // repeatable
)

// Afterwards — recreates the container with the kit appended.
_ = sb.AddKit(ctx, "./my-kit")

refs, _ := sb.Kits(ctx) // []string, in the order they were added

⚠️ AddKit applies only part of a kit. The CLI refuses, before touching anything, any kit declaring credentials, publishedPorts, volumes, commands.startup or commands.initFiles — recreate the sandbox with WithKit for those. AddKit also starts a stopped sandbox and leaves it running. Local paths are made absolute first, because the daemon records the reference verbatim.

Read and package kits:

info, _ := kit.Inspect(ctx, c, "./my-kit")   // dir, ZIP, git repo, or OCI reference
fmt.Println(info.Manifest.Name, info.Manifest.Kind, info.Warnings)

err := kit.Validate(ctx, c, "./my-kit")      // dir, ZIP or git repo — NOT an OCI reference
if errors.Is(err, client.ErrKitRejected) {
	// the CLI marked the artifact INVALID
}

_ = kit.Pack(ctx, c, "./my-kit", "./my-kit.zip") // out is required, never derived
_ = kit.Pull(ctx, c, "ghcr.io/me/kit:v1", "./my-kit.zip")

pushCtx, cancel := context.WithTimeout(ctx, 2*time.Minute) // Push retries indefinitely
defer cancel()
_ = kit.Push(pushCtx, c, "./my-kit", "ghcr.io/me/kit:v1")

Inspect returns a report, not the kit — it omits the files/ payload that Pack writes. Struct-valued fields on kit.Info and kit.Manifest stay as json.RawMessage (ADR 0005); unmarshal one into a shape of your own when you need it. Push and Pull have never completed against a real registry — see Known deviations.


Error handling

Branch on sentinels with errors.Is; pull detail out of *client.APIError (REST) or client.CLIError (shell-out) with errors.As.

sb, err := sandbox.Get(ctx, c, "nope")
if errors.Is(err, client.ErrSandboxNotFound) {
	// handle missing sandbox
}

var apiErr *client.APIError
if errors.As(err, &apiErr) {
	fmt.Println(apiErr.Op, apiErr.Status, apiErr.Message)
}
Sentinel Raised when
client.ErrSandboxNotFound REST 404 (get/inspect/lifecycle on a missing sandbox).
client.ErrSandboxExists REST 409, or Create with a name that's already taken.
client.ErrSandboxNotRunning exec.* on a stopped sandbox without WithAutoStart.
client.ErrExecNotFound InspectExec for an unknown exec id.
client.ErrIncompatibleVersion WithStrictVersion and the daemon is incompatible.
client.ErrDaemonNotRunning EnsureRunning couldn't make the socket healthy in time.
client.ErrBinaryNotFound the sbx binary isn't on PATH (any shell-out op).
client.ErrKitRejected kit.Validate — the CLI marked a kit artifact INVALID:.

Runnable examples

Self-contained programs live in examples/ — each is go run-able against a live daemon:

Example Shows
examples/quickstart Connect → create → exec → remove.
examples/exec Capture, env/workdir, streaming, detached + poll, resource stats.
examples/run-agent Interactive agent session over your terminal.
examples/resources Ports, file copy, templates, policy, secrets.
go run ./examples/quickstart

Agent skill

A Claude Code skill ships with this repo at skills/sbx-go-sdk/SKILL.md. When an agent works in (or imports) this SDK, the skill teaches it the API surface, the Create-vs-Run-vs-Exec distinction, and the gotchas below — so it reaches for the right call without re-reading the source. Invoke it with /sbx-go-sdk.

Version alignment

This SDK is pinned to a tested sbx / sandboxd range. It is currently built and live-verified against sbx v0.37.0 with daemon REST api_version 0.24.0. Both values are exported constants you can read at runtime:

client.ClientVersion    // "v0.37.0" — the sbx/daemon version the SDK was built against
client.TestedAPIVersion // "0.24.0"  — the daemon REST api_version its wire types were generated from

Why a pin exists. The transport is hybrid: REST wire structs are generated from the sbx binary's DWARF, and orchestration ops shell out to versioned CLI flags. A daemon that changes either surface can drift from what the SDK expects, so the SDK targets a known-good range rather than promising forward-compatibility with every release.

Version checking is a development-time gate, not a runtime one — see ADR 0004. client.WithStrictVersion() and Client.CheckVersion are deprecated: api_version bumps on every sbx release with no documented compatibility contract, so a strict compare fires on every upgrade — including ones where nothing the SDK uses changed — and upstream removed POST /version entirely in v0.37.0, so CheckVersion can no longer succeed against a current daemon. Drift detection instead lives in TestContract_VersionAlignment, an integration test a maintainer runs deliberately, not something client.New enforces. For a runtime check, compare DaemonHealth().Version / .APIVersion against the constants yourself:

h, _ := c.DaemonHealth(ctx)
if h.Version != client.ClientVersion || h.APIVersion != client.TestedAPIVersion {
    log.Printf("daemon %s/api %s differs from SDK-tested %s/api %s — behaviour may have drifted",
        h.Version, h.APIVersion, client.ClientVersion, client.TestedAPIVersion)
}

How the SDK re-aligns when a newer sbx ships. Maintainers run a re-sync loop, guarded by a contract test:

  1. Install the new sbx, then run the drift gate: go test -tags integration -run TestContract_VersionAlignment ./internal/integration. It compares the live daemon's version / api_version to the pinned constants and fails on drift with a remediation hint (set SBX_ALLOW_VERSION_DRIFT=1 to downgrade to a warning while upgrading).
  2. Regenerate the wire types: go run ./internal/tools/dwarfgen -bin $(which sbx), then review the internal/api/types_gen.go diff. Re-apply the manual reconciliation listed in that file's header — generation reverts it, and the build then fails, because Sandbox.Kits depends on SandboxInfo.Labels being *map[string]string.
  3. Reconcile docs/sbx-version-coverage.md against the upstream release notes for the new version — the drift gate's failure message points here, so a sync that quietly misses features (as v0.35.0's did) gets caught.
  4. Run the full integration suite against the new daemon, migrate any stubbed endpoints that are now implemented, and bump client.ClientVersion / client.TestedAPIVersion.

So: pin to a tested range, negotiate leniently at runtime, and re-sync deliberately — the contract test is what tells maintainers a re-sync is due.

Known deviations & limitations

Verified live against sandboxd v0.37.0:

  • CopyFrom is REST and auto-starts the sandboxGET /sandbox/{name}/files?path=… works as of v0.37.0 (it was 404 through v0.35.0). CopyFrom starts a stopped sandbox first, matching sbx cp's own behaviour; this differs from exec, which requires an explicit WithAutoStart(). CopyTo still shells out — there is no REST upload path. CopyFrom places no cap on extracted size.
  • policy.List is RESTGET /policy/network/rules works as of v0.37.0 and always sends type=all (omitting it silently drops filesystem rules with no error). A shape change yields client.ErrUnexpectedFormat; use policy.ListRaw for the human table. secret.List still parses the CLI table (no --json upstream); policy.Profiles is raw text.
  • SaveTemplate requires a stopped sandbox — the daemon refuses to snapshot a running one, and the CLI would otherwise block on an interactive stop prompt.
  • UnpublishPort is REST — it first sends GET /sandbox/{name}/ports to resolve which keys match the spec, then POST /sandbox/{name}/ports/unpublish (body: a bare []PortKey array); works as of v0.37.0.
  • secret.SetCustom is experimental and exposes the value via the process list.
  • secret.SetToken/SetRegistry keep the secret off the argument vector — both write the value to the child process's stdin rather than passing it as a CLI argument, so unlike SetCustom it does not appear in the host process list. Confirmed by a live run: --force does work on this stdin path, despite upstream's --help claiming it applies only "when --token is used".
  • secret.SetToken, SetRegistry and Import reject an existing entry rather than overwriting it. Without --force, sbx secret set/secret import on an entry that already exists in the target scope prompts for confirmation; on non-interactive stdin that prompt reads EOF, cancels, and exits 0 having stored nothing. All three now check for an existing entry before invoking the CLI and return an error in that case instead. Pass WithOverwrite() (SetToken/SetRegistry) or WithOverwriteExisting() (Import) to opt into replacing it.
  • Two related paths may have the same silent-success shape, unverified. sbx has a second overwrite prompt — "%s OAuth token already exists. Overwrite? (y/N): " — for OAuth-configured services; if secret ls doesn't surface those as rows, SetToken against such a service could still reach that prompt and exit 0 silently. SetCustom has no pre-flight check at all and pipes nothing to stdin, so set-custom against an existing entry may have the same shape. Neither can be confirmed without running sbx secret against real credentials, so both are recorded as known limitations rather than fixed here.
  • settings/ssh mutations are fire-and-forgetsettings.Set/Unset and ssh.Enable/Disable/Setup write host state (settings.json, ~/.ssh/config) and return before the daemon's ~5s hot-reload; reads (settings.Get/List, ssh.Enabled) use --json. ssh.Enable sets only feature.ssh (SSH also needs platform.allowExperimentalFeatures, default true).
  • SSH connects by hostname — v0.35.0 dropped the ssh.port loopback model; ssh.Setup writes a wildcard Host *.sbx block and you connect with ssh <name>.sbx (no port, no key). ssh.TargetFor builds that hostname.
  • kit.Inspect does not report a kit's files. sbx kit inspect --json omits the files/ payload, although kit.Pack writes it into the artifact. Info is a report, not the kit.
  • kit.Push and kit.Pull are unverified. Neither has completed against a real OCI registry; both ship with argument-vector unit tests only. Pull does enforce kit.allowedSources (a forbidden reference is refused instantly, like Inspect); Push does not — it opens real network connections regardless of the reference's source and retries indefinitely against an unreachable target, with no output even under --debug. Callers of Push should pass a ctx with a deadline.
  • Cancelling a shell-out always returns, but not always cleanly. On cancellation the SDK sends the child an interrupt and, if it has not exited within a 10s grace period, kills it and closes its pipes. So a ctx deadline does return control — a child that ignores the interrupt, or a grandchild holding the output pipe, no longer blocks the call forever. The trade-off: if the command itself succeeds but leaves a grandchild on the pipe past that grace period, the call reports a client.CLIError with ExitCode -1 rather than success.
  • Local kit paths are made absolute. AddKit and WithKit rewrite a reference that exists on disk. The daemon records the kit list verbatim and resolves a relative path against its own working directory, which would record a path that does not exist and break every later add.
  • Sandbox.AddKit cannot apply every kit. The CLI refuses, pre-flight, any kit declaring credentials, publishedPorts, volumes, commands.startup or commands.initFiles; the CLI's own remedy is to recreate the sandbox from scratch via sbx rm + sbx create --kit to use this kit.
  • Sandbox.Kits reads a label. If upstream renames com.docker.sandbox.kits, it returns empty rather than an error.

See the design spec and Plan 2 doc under docs/ for the full rationale.

Directories

Path Synopsis
examples
exec command
Command exec demonstrates the exec surface against one sandbox: capture, live streaming, detached-with-polling, a resource-usage snapshot, and following a log file.
Command exec demonstrates the exec surface against one sandbox: capture, live streaming, detached-with-polling, a resource-usage snapshot, and following a log file.
quickstart command
Command quickstart connects to sandboxd, provisions a disposable sandbox over the current directory, runs one command inside it, and cleans up.
Command quickstart connects to sandboxd, provisions a disposable sandbox over the current directory, runs one command inside it, and cleans up.
resources command
Command resources demonstrates the Plan-2 surface: publishing ports, copying files in and out, saving a template, and reading network policy.
Command resources demonstrates the Plan-2 surface: publishing ports, copying files in and out, saving a template, and reading network policy.
run-agent command
Command run-agent launches an interactive agent session, attaching the agent to this terminal and blocking until it exits.
Command run-agent launches an interactive agent session, attaching the agent to this terminal and blocking until it exits.
Package exec runs commands inside sandboxes: capture, interactive attach, and detached, over the daemon's hijacking /exec/attach endpoint.
Package exec runs commands inside sandboxes: capture, interactive attach, and detached, over the daemon's hijacking /exec/attach endpoint.
internal
api
cli
Package cli drives the `sbx` binary for orchestration-heavy operations that have no daemon REST path (sandbox create, daemon start, etc.).
Package cli drives the `sbx` binary for orchestration-heavy operations that have no daemon REST path (sandbox create, daemon start, etc.).
coltable
Package coltable parses the fixed-width, whitespace-aligned tables the sbx CLI prints (e.g.
Package coltable parses the fixed-width, whitespace-aligned tables the sbx CLI prints (e.g.
stdcopy
Package stdcopy demultiplexes Docker's multiplexed stream format used by the sandboxd exec/attach endpoint: repeating [type, 0,0,0, size_be32][payload] frames where type 1 = stdout, 2 = stderr.
Package stdcopy demultiplexes Docker's multiplexed stream format used by the sandboxd exec/attach endpoint: repeating [type, 0,0,0, size_be32][payload] frames where type 1 = stdout, 2 = stderr.
tools/dwarfgen command
Command dwarfgen extracts sandboxapi request/response structs from an unstripped sbx binary's DWARF and emits Go definitions into internal/api/types_gen.go.
Command dwarfgen extracts sandboxapi request/response structs from an unstripped sbx binary's DWARF and emits Go definitions into internal/api/types_gen.go.
untar
Package untar extracts a tar stream into a destination directory, confined so that no entry can write outside it.
Package untar extracts a tar stream into a destination directory, confined so that no entry can write outside it.
Package kit reads and packages kit artifacts (`sbx kit`, EXPERIMENTAL upstream: "this command may change or be removed in future releases").
Package kit reads and packages kit artifacts (`sbx kit`, EXPERIMENTAL upstream: "this command may change or be removed in future releases").
Package policy manages sandbox access policies.
Package policy manages sandbox access policies.
Package sandbox manages sbx sandboxes: provisioning (shell-out) and lifecycle (REST), following the docker/go-sdk functional-options idiom.
Package sandbox manages sbx sandboxes: provisioning (shell-out) and lifecycle (REST), following the docker/go-sdk functional-options idiom.
Package secret manages stored sandbox secrets via shell-out to `sbx secret`.
Package secret manages stored sandbox secrets via shell-out to `sbx secret`.
Package settings reads and writes persistent sandboxd settings by shelling out to `sbx settings` (which edits settings.json; the daemon hot-reloads it within ~5s).
Package settings reads and writes persistent sandboxd settings by shelling out to `sbx settings` (which edits settings.json; the daemon hot-reloads it within ~5s).
Package skillstore manages the shared agent skills store (EXPERIMENTAL upstream, added in sbx v0.37.0).
Package skillstore manages the shared agent skills store (EXPERIMENTAL upstream, added in sbx v0.37.0).
Package ssh manages the sandboxd SSH endpoint (EXPERIMENTAL upstream).
Package ssh manages the sandboxd SSH endpoint (EXPERIMENTAL upstream).
Package template manages sandbox template images (templates ARE images) over the daemon's /docker/images* REST endpoints, plus the shell-out save path on Sandbox.
Package template manages sandbox template images (templates ARE images) over the daemon's /docker/images* REST endpoints, plus the shell-out save path on Sandbox.

Jump to

Keyboard shortcuts

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