boxer

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 writingUse
A Go program that owns sandbox lifecyclepkg/boxer
A program in any other language, or a script--json
An integration for a new harnessboxer 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/boxer

pkg/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.ts

Errors

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 check

A 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 code

boxer 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

  • Ensure and boxer up do 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() or boxer rm. Use auto_reclaim and boxer gc for idle ones.
  • Use auto: ports in any boxer.toml you generate. A fixed host port fails in the second worktree.
  • boxer removes its own shim directories from PATH before it runs anything. Do not add them back around a boxer call. A uname shim would then call boxer inside boxer and deadlock the session.

On this page