The official Go SDK for xShellz sandboxes — spawn a real Linux box from your program, run commands in it, and throw it away (or keep it).
What is a sandbox? A sandbox is a small, private Linux machine that runs in the cloud, isolated from everyone else by a gVisor kernel — so whatever runs inside (including code an AI wrote) can't touch your computer or anyone else's. You get root, a real filesystem, and network access; when you're done you delete it.
1. Install the SDK
go get github.com/xshellz/xshellz-go@v0.2.02. Get an API key
Sign up at app.xshellz.com and create a personal
access token with read and write scopes in your account settings (the
API endpoint behind that page is POST /v1/auth/tokens). Then:
export XSHELLZ_API_KEY="<your token>"3. Hello, sandbox
sbx, err := xshellz.Create(ctx, nil) // reads XSHELLZ_API_KEY
if err != nil { log.Fatal(err) }
defer sbx.Close(ctx) // destroys the box; call sbx.Detach() first to keep it
res, _ := sbx.Run(ctx, "echo hello from $(hostname)", nil)
fmt.Print(res.Stdout)That's it — Create returns when the box is up, Run executes over SSH, and
Close destroys the box.
res, err := sbx.Run(ctx, "python3 -c 'print(6*7)'", nil)
fmt.Print(res.Stdout) // "42\n"
fmt.Print(res.ExitCode) // non-zero exit is DATA, not an errorPass &xshellz.RunOptions{Cwd: "/srv", Env: map[string]string{"K": "v"}} to
set a working directory or environment, and Stdout/Stderr writers to
stream output live.
GetOrCreate gives you the same box every time you run your program. The
first call creates a box named "my-agent" and saves its SSH key to the local
keystore (~/.xshellz/keys/my-agent.pem, chmod 0600); every later call
finds the box by name, loads the key, and reattaches — starting the box first
if it was idle-stopped.
sbx, err := xshellz.GetOrCreate(ctx, "my-agent", nil)
if err != nil { log.Fatal(err) }
defer sbx.Close(ctx) // a GetOrCreate box is already detached: Close just drops
// the SSH connection and leaves the box alive
// ...
// _ = sbx.Kill(ctx) // when you really want it gone: destroy it explicitlyA permanent box comes back already detached, so Close never destroys it —
call sbx.Kill(ctx) to actually tear it down.
Security note: the keystore is a plaintext private key on disk (0600, in a
0700 dir). Anyone who can read the file can SSH into your box — delete the
file to revoke local access. Set GetOrCreateOptions.KeystoreDir to relocate
it, or DisableKeystore: true to manage keys yourself (then attaching to an
existing box requires PrivateKeyPEM, and a missing key fails with
ErrMissingKey).
Spawn starts a command detached — it keeps running after Spawn returns and
after your program exits, for as long as the box is up:
job, err := sbx.Spawn(ctx, "python3 -m http.server 8000", &xshellz.SpawnOptions{Name: "web"})
running, _ := job.IsRunning(ctx) // kill -0 probe
out, _ := job.Logs(ctx, 100) // last 100 log lines (stdout+stderr)
_ = job.Stop(ctx) // SIGTERM, then SIGKILL after ~5s grace
jobs, _ := sbx.Jobs(ctx) // list all spawned jobs + livenessLogs live on the box under ~/.xshellz/jobs/<job-id>.log. Jobs do not survive
a box Restart or stop.
Hand untrusted code to the box, not to your machine. RunCode writes the
snippet to a temp file, runs the right interpreter, always deletes the file,
and returns the same result type as Run:
res, err := sbx.RunCode(ctx, "python", "print(sum(range(10)))", nil)
fmt.Print(res.Stdout) // "45\n"; a traceback shows up as res.ExitCode != 0Languages: python (python3), node, bash, ruby, php. Anything else
returns ErrUnsupportedLanguage listing the supported ones.
_ = sbx.WriteFile(ctx, "/srv/config.json", []byte(`{"debug": true}`)) // bytes → box
data, _ := sbx.ReadFile(ctx, "/srv/report.txt") // box → bytes
_ = sbx.Upload(ctx, "./model.bin", "/srv/model.bin") // local file → box
_ = sbx.Download(ctx, "/srv/out.csv", "./out.csv") // box → local fileBinary-safe both ways; whole files are buffered in memory (sized for configs and artifacts, not multi-GB blobs).
stats, _ := sbx.Stats(ctx)
fmt.Printf("mem %d/%d MB, cpu %.1f%%, disk %d/%d MB\n",
stats.MemUsedMB, stats.MemAllowedMB, stats.CPUPercent,
stats.DiskUsedMB, stats.DiskAllowedMB)
procs, _ := sbx.Procs(ctx) // top processes, SSH session count, detected agentsurl, _ := sbx.TerminalURL(ctx)
fmt.Println(url) // open this in a browser for a live shell on the boxThe URL embeds a signed token valid for about 1 hour — mint a fresh one each time instead of storing it.
The account-level xshellz.box manifest is seeded into ~/xshellz.box on
every newly created box — use it to preinstall dependencies so fresh boxes
come up ready:
_, _ = xshellz.SetBoxfile(ctx, "apt: ffmpeg jq\npip: requests", nil)
manifest, _ := xshellz.GetBoxfile(ctx, nil)
_, _ = xshellz.SetBoxfile(ctx, "", nil) // empty clears it_ = sbx.Start(ctx) // resume an idle-stopped box (/home preserved)
_ = sbx.Restart(ctx) // reboot a running box (/home preserved, processes are not)
sbx.Detach() // make Close keep the box
_ = sbx.Close(ctx) // drop SSH + destroy the box (unless detached)
_ = sbx.Kill(ctx) // drop SSH + destroy the box unconditionally (even if detached)Every public function, type, parameter, and error is documented in docs/API.md (and on pkg.go.dev).
| Env var | Meaning | Default |
|---|---|---|
XSHELLZ_API_KEY |
Personal access token (read+write scopes) |
— (required) |
XSHELLZ_API_URL |
Control-plane base URL | https://api.xshellz.com/v1 |
Precedence: explicit option (APIKey/APIURL on the options struct) >
environment variable > default.
All sentinels work with errors.Is; API failures also carry an
*xshellz.APIError (via errors.As) with the HTTP status, machine code, and
message:
| Sentinel | When |
|---|---|
ErrNoAPIKey |
no token in options or environment |
ErrAuth |
401/403 — bad token, missing scope, plan not entitled, verification required (APIError.Code == "verification_required") |
ErrQuota |
the plan's concurrent box limit is reached — Connect/GetOrCreate the existing box instead |
ErrNotFound |
404 — unknown UUID, or Start on a box that isn't stopped |
ErrNotRunning |
Run/Spawn/file ops on a stopped box — call sbx.Start(ctx) |
ErrMissingKey |
GetOrCreate found the named box but no private key (keystore file missing or keystore disabled) |
ErrUnsupportedLanguage |
RunCode with a language it doesn't know |
- Free tier: 1 concurrent box, which idle-stops after ~30 minutes.
Creation is throttled to 10/min.
CreatereturnsErrQuotawhile a box exists —Connect/GetOrCreateit (orCloseit). A stopped box resumes withsbx.Start(ctx);/homeis preserved. - Data plane is SSH (
root@ssh_host:ssh_port, fake-root inside gVisor). Host-key checking usesssh.InsecureIgnoreHostKey: boxes are ephemeral and mint a fresh host key each spawn, so there is nothing stable to pin yet. Treat the box as untrusted compute, not a secret store. - File transfer rides
catover SSH exec (binary-safe both ways) instead of SFTP, so the SDK works against any sshd and keepsgolang.org/x/cryptoas the only dependency. - Command execution:
Runpasses your command verbatim to the remote shell; withCwd/Envset it wraps ascd CWD && env K=V ... sh -c 'CMD'.
No local Go toolchain needed — run gofmt + go vet + the full test suite
(with the ≥80% coverage gate) in a container (golang:1.22, module and build
caches persisted in named volumes so re-runs are fast):
docker compose run --rm testThe repo is mounted at /work; GOMODCACHE and GOCACHE point at named
volumes, so nothing is written into the repo itself except cover.out
(gitignored).
MIT — see LICENSE.