Build on boxer
Drive sandboxes from your own program with the Go package, the JSON output, or the CLI.
For an orchestrator, dashboard or harness integration, boxer offers three interfaces.
| You are writing | Use |
|---|---|
| A Go program that owns sandbox lifecycle | pkg/boxer |
| A program in any other language, or a script | --json |
| An integration for a new harness | boxer hook, boxer mcp, boxer brief |
All three interfaces are covered by the 1.0 stability contract. Fields and flags are added, never renamed or removed.
Go
go get github.com/BarakChamo/boxerpkg/boxer is the only importable package. It runs the same code as the CLI.
import "github.com/BarakChamo/boxer/pkg/boxer"
b, err := boxer.Open(dir, boxer.Options{Harness: "my-orchestrator", SessionID: sid})
if err != nil {
var be *boxer.Error
if errors.As(err, &be) {
log.Fatalf("%s (%s); fix: %s", be.Reason, be.Cause, be.Fix)
}
log.Fatal(err)
}
for _, w := range b.Warnings() {
log.Println("boxer:", w) // for example, running in the main checkout
}
if _, err := b.Ensure(true, false); err != nil { // create if needed, then start
log.Fatal(err)
}
code, err := b.Run([]string{"sh", "-c", "npm test"}, boxer.RunOpts{
Stdin: os.Stdin, Stdout: os.Stdout, Stderr: os.Stderr,
})Run returns the guest command's exit code. A non-nil error means the command did not run. With
on_sandbox_unavailable = "passthrough", Run returns -1 and an error when the sandbox could not
be provisioned, and your program decides whether to run on the host.
Options is optional. Harness selects a [harness.<name>] override in boxer.toml, and
SessionID and AgentID matter only when isolation keys
the sandbox to a session or subagent.
A complete program
examples/go-embed runs one
shell line in several worktrees in parallel, each in its own sandbox, and reports every exit code:
go run ./examples/go-embed -- 'npm test' ~/code/app-wt1 ~/code/app-wt2/Users/me/code/app-wt1 ok
/Users/me/code/app-wt2 exit 1
FAIL src/cart.test.tsErrors
Every refusal is a *boxer.Error. Reason and Fix are for a person. Cause is for a program.
Scope names the sandbox. Match on Cause. Its values are listed in
JSON output and do not change.
Host-wide functions
machines, _ := boxer.List() // every boxer sandbox on this directory's backend
_ = boxer.Stop("sb-7e1852e4a3c3") // stop; stopping a stopped sandbox is not an error
_ = boxer.Delete("sb-7e1852e4a3c3")
t := boxer.LastUsed("sb-7e1852e4a3c3")These take a sandbox name because the worktree may already be gone. The Go reference lists every function and type.
JSON
--json prints exactly one JSON document on stdout. It works on ls, status, down, gc,
doctor, brief, tasks, logs, backends, integrations, stop, rm, pack ls, fork and
capsule inspect. BOXER_OUTPUT=json does the same for a caller that cannot add a flag.
boxer status --json # this worktree's sandbox; exit 0 running, 3 stopped, 4 absent
boxer ls --json --resources # every sandbox, with CPU, memory and disk
boxer doctor --json # the resolved configuration and every checkA health check can use the status exit code alone. To find sandboxes whose worktree has been
deleted, read git.exists from ls:
boxer ls --json | python3 -c '
import json, sys
for s in json.load(sys.stdin):
if not s["git"]["exists"]:
print(s["name"], s["worktree"])'boxer watch --json prints one object per line for each state change (created, running, stopped,
gone) as it happens, so a dashboard does not have to poll. JSON output has
every shape. Parse --json output, never a table. The human-readable columns are not part of the
contract.
A harness integration
A new harness needs these commands and no library:
boxer hook <harness> # reads the harness's hook JSON on stdin, answers in its dialect
boxer mcp # boxer_run and boxer_status over stdio
boxer brief --json # the mount, mode, intercept list and tasks, for a system prompt
boxer run -c '<line>' # a shell line in the guest, with its exit codeboxer hook supports nine hook formats. If your harness uses one of them, add a row to boxer's
tables instead of writing a bridge. See Adding a
harness. If your harness
has no hook API, put boxer run behind any shell tool. It passes stdio through.
Before you ship
Ensureandboxer updo nothing on a running sandbox. Call them before every run instead of tracking state yourself.- A sandbox uses about 700 MB of disk and around 1 GB of memory while running. If your tool opens one
per task, remove them with
Down()orboxer rm. Useauto_reclaimandboxer gcfor idle ones. - Use
auto:ports in anyboxer.tomlyou generate. A fixed host port fails in the second worktree. - boxer removes its own shim directories from
PATHbefore it runs anything. Do not add them back around a boxer call. Aunameshim would then call boxer inside boxer and deadlock the session.