Sandbox CLI
k8e-sandbox-cli is a standalone binary (~44MB) that gives AI agents direct access to K8E sandbox infrastructure â no server install needed.
AI Agent (claude code / codex / pi)
â shell command
âŧ
k8e-sandbox-cli run "print('hello')" --lang python
â gRPC (TLS)
âŧ
sandbox-grpc-gateway:50051
â
âŧ
Isolated Pod (gVisor / Kata / Firecracker)
Install the Skill
Three steps: give the server its public name â create an API key â connect from your agent machine.
1. Tell the server the name clients will dial (remote / cloud hosts only)
The sandbox gateway certificate (:50051, mTLS) is generated from the host's interface addresses â on AWS those are private VPC IPs, so a client dialing the public name fails the TLS handshake. Set the public name (KIP-22) in whichever way suits your setup:
# /etc/k8e/config.yaml (server daemon config â same keys as CLI flags)
sandbox-advertise-hostname: sandbox.example.com # or the Elastic IP, e.g. 203.0.113.10
# equivalent one-shot flag
k8e-server --sandbox-advertise-hostname sandbox.example.com
# legacy env fallback â inject into the k8e-server process (e.g. systemd),
# NOT a login shell; merged alongside the flag above
# K8E_SANDBOX_ADVERTISED_HOSTNAME=sandbox.example.com
A bare DNS name or a bare IP â nothing else. Schemes (
https://âĻ), ports (host:50051), paths and whitespace are rejected:k8e-serverrefuses to start withinvalid --sandbox-advertise-hostname: âĻ. A value that parses as an IP lands in the cert's IP SANs, anything else in its DNS SANs.tls-sanin/etc/k8e/config.yamlis not the knob for this â it only shapes the kube-apiserver serving cert.Restart
k8e-serverâ the gateway certificate regenerates automatically when the SAN set changes, so the new name works on the next connect. No manual deletion of/var/lib/k8e/server/tls/sandbox-server.{crt,key}.
2. Create an API key on the server (default TTL 30 days)
k8e sandbox-apikey create my-agent
# â {"name":"my-agent","key":"849a5302e66f98d1d5064ef8501703574af4053b7bf9cf5337f9533326ce2bc9",
# "e2b_key":"e2b_849a5302e66f98d1d5064ef8501703574af4053b7bf9cf5337f9533326ce2bc9",
# "ttl_days":30,"created_at":"...","expires_at":"..."}
# then, when the server's public name is known:
# mcp client config:
# {"url":"https://sandbox.example.com/mcp","headers":{"Authorization":"Bearer e2b_849a5302e66f98d1âĻ"}}
# Optional: longer or non-expiring bootstrap keys
# k8e sandbox-apikey create my-agent --ttl 90d # also: 720h, or a plain number of days
# k8e sandbox-apikey create my-agent --ttl never # also: none / 0
Pass key (bare hex) to connect --apikey. Hand e2b_key to the official e2b SDKs â they require the e2b_ prefix and the server strips it. The mcp client config block is copy-pasteable straight into an MCP client (Claude Code, Codex); it is printed only when the server's public name is known, so a host without one simply omits it.
3. On the client, download the CLI and connect
connect authenticates over mTLS, puts k8e-sandbox-cli on PATH, remembers the gateway, and installs the /k8e-sandbox skill into your harnesses â so later commands need no --endpoint at all.
# 1. Download the platform-specific binary (~44MB)
# pick your platform: linux-amd64 | linux-arm64 | darwin-amd64 | darwin-arm64 | windows-amd64.exe
curl -sLO https://github.com/xiaods/k8e/releases/latest/download/k8e-sandbox-cli-linux-amd64
chmod +x k8e-sandbox-cli-linux-amd64
# 2. Symlink the plain command name to the downloaded file (do not rename)
# optionally move both into a PATH dir, e.g. ~/.local/bin/
ln -s k8e-sandbox-cli-linux-amd64 k8e-sandbox-cli
# 3. Connect: mTLS auth + verify the gateway + install the /k8e-sandbox skill
# (Claude / Codex / Pi / dsh; --agent picks the target, default auto-detect)
# Note: --endpoint and --apikey are global flags, placed before the subcommand
./k8e-sandbox-cli --endpoint sandbox.example.com:50051 --apikey 849a5302e66f98d1d5064ef8501703574af4053b7bf9cf5337f9533326ce2bc9 connect
# Same command with the flags on the subcommand (both spellings work)
# ./k8e-sandbox-cli connect --endpoint sandbox.example.com:50051 --apikey 849aâĻ --ca-file ./sandbox-ca.crt
# Multi-cluster: use a named profile (see Configuration below)
# ./k8e-sandbox-cli --profile prod connect --apikey 849a5302e66f98d1d5064ef8501703574af4053b7bf9cf5337f9533326ce2bc9
On the same machine as the server? Just run
k8e-sandbox-cli connectâ the CLI auto-discovers the local gateway and its certs; no API key needed.
4. Get the gateway CA onto the client
The gateway presents a private CA that your machine does not trust, so the first remote connect needs that CA. connect and login verify the gateway via system trust, a cached CA, or --ca-file.
On the server host the CLI can export it directly:
k8e-sandbox-cli tls export-ca --output ~/sandbox-ca.crt
# or to stdout: k8e-sandbox-cli tls export-ca -
Without the CLI on the server, the same file is on disk:
sudo cat /var/lib/k8e/server/tls/sandbox-ca.crt > ~/sandbox-ca.crt
Copy it to the client machine and hand it to the first connect:
scp user@server:~/sandbox-ca.crt ./
./k8e-sandbox-cli --endpoint <server-ip>:50051 --apikey <key> \
connect --ca-file ./sandbox-ca.crt
The CA is a public certificate, not a secret â it only proves the gateway is this gateway. The API key is the secret; the client private key never leaves the client.
tls export-caalso looks for asandbox-matrix/sandbox-caConfigMap when the client has a kubeconfig â but the cluster does not publish that ConfigMap today, so the server-host path above is the one that works. If you are on the server host,k8e-sandbox-cli connectwith no arguments skips all of this: it auto-discovers the local gateway and its certs.
5. Verify the whole path works
One command proves the API key, the mTLS client certificate, the gRPC gateway, the warm pool and the runtime all line up:
k8e-sandbox-cli run 'echo hello from k8e'
# â {"duration_ms":âĻ,"exit_code":0,"session_id":"sess-âĻ","status":"completed",
# "stdout":"hello from k8e\n","stderr":"","âĻ":âĻ}
hello from k8e in stdout means you are running code inside an isolated gVisor pod. If it fails, the troubleshooting table below maps the symptom to the cause.
status does not create a session
k8e-sandbox-cli status only probes the gateway â it reports "session_id":"" until a run or create has actually made one. That is not an error; run something first.
Platform binaries:
| Platform | Download name |
|---|---|
| Linux amd64 | k8e-sandbox-cli-linux-amd64 |
| Linux arm64 | k8e-sandbox-cli-linux-arm64 |
| macOS amd64 | k8e-sandbox-cli-darwin-amd64 |
| macOS arm64 | k8e-sandbox-cli-darwin-arm64 |
| Windows amd64 | k8e-sandbox-cli-windows-amd64.exe (symlink via mklink k8e-sandbox-cli.exe k8e-sandbox-cli-windows-amd64.exe) |
One binary, two names: the downloaded platform-suffixed file is the
k8e-sandbox-clicommand â the symlink is just a plain-name alias. All examples below usek8e-sandbox-cli.
Then in your agent harness:
/k8e-sandbox <goal> # Claude Code
$k8e-sandbox <goal> # Codex (or pick it from /skills)
/skill:k8e-sandbox <goal> # Pi
dsh (DeepSeek Harness) loads the same skill through its skill tool, or you can name it directly in chat â see dsh + k8e-sandbox for the plugin-mounted path.
Or ask naturally: "Run this Python snippet in a sandbox" â the skill drives k8e-sandbox-cli run.
Skill not showing up? Run
k8e-sandbox-cli connect --skill-onlyto refresh the harness installs without dialing the gateway, then open a new agent session.k8e-sandbox-cli doctorchecks binary / PATH / profile / certs / skill install (add--jsonfor a machine-readable report,--fixto apply the safe local repairs).
Available Commands
| Command | Description |
|---|---|
k8e-sandbox-cli --profile <name> âĻ | Use named profile from ~/.k8e/sandbox/profiles.yaml (not server /etc/k8e/config.yaml) |
k8e-sandbox-cli connect | Connect local/remote gateway, verify it, ensure PATH, remember the gateway, and install the /k8e-sandbox skill |
k8e-sandbox-cli connect --agent <auto|claude|codex|pi|dsh|all> | Choose the skill install target (default auto: harnesses detected on this machine) |
k8e-sandbox-cli connect --skill-only | Re-install agent skills only (no gateway dial) |
k8e-sandbox-cli connect --skip-skill | Authenticate only; do not install agent skills |
k8e-sandbox-cli login | Authenticate only (mTLS cert; no skill install); optional --device-name |
k8e-sandbox-cli doctor | Diagnose CLI / gateway / certs / skill install / dsh plugin and print the fix for each failure (--json, --fix) |
k8e-sandbox-cli tls export-ca | Write the sandbox gateway CA to ~/.k8e/sandbox-ca.crt (--output <path> / - for stdout) so IP-only gateways, MCP and e2b clients can trust it |
k8e-sandbox-cli status | Check sandbox service availability and current session |
k8e-sandbox-cli run <code> | Run code or shell command (auto-creates/manages session; --lang, --timeout, --raw, --session-id, --tenant, --background, --manifest, --git-repo, --allowed-hosts) |
k8e-sandbox-cli create | Create a new session (custom runtime, egress, env, secrets, manifest, git-repo) |
k8e-sandbox-cli get <sid> | Inspect a session (phase, runtime, env keys) |
k8e-sandbox-cli sessions | List sessions |
k8e-sandbox-cli destroy <sid> | Destroy a session and free resources |
k8e-sandbox-cli write <sid> <path> | Write file to /workspace (content via stdin) |
k8e-sandbox-cli read <sid> <path> | Read file from /workspace |
k8e-sandbox-cli push <sid> <local> [remote] | Stream a local file INTO the sandbox â chunked 4MiB windows, constant memory, binary-safe, any size (--chunk-mb to tune) |
k8e-sandbox-cli pull <sid> <remote> [local] | Stream a sandbox file OUT to a local path (same chunked transfer) |
k8e-sandbox-cli list <sid> | List files in /workspace (filter by --since timestamp for changed-file diff) |
k8e-sandbox-cli log <sid> | Replay exec transcript (--offset, --limit, --follow) |
k8e-sandbox-cli events <sid> | Read daemon NDJSON event stream (--limit) |
k8e-sandbox-cli ps <sid> | List processes in the sandbox pod (pid, comm, state) |
k8e-sandbox-cli poll <run-id> | Poll a background run (--follow) |
k8e-sandbox-cli subagent <parent-sid> | Spawn child sandbox under parent session (shares parent pod + workspace, no new pod) |
k8e-sandbox-cli confirm <sid> <action> | Gate irreversible action on human approval (--timeout, --no-wait) |
k8e-sandbox-cli approve <approval-id> | Approve a pending confirm request (--reject, --reason) |
k8e-sandbox-cli snapshot save <sid> <name> | Save workspace snapshot (content-addressed, dedup'd) |
k8e-sandbox-cli snapshot list | List saved snapshots |
k8e-sandbox-cli snapshot restore <name> | New session from a snapshot (--base <snap> for incremental) |
k8e-sandbox-cli snapshot delete <name> | Delete a snapshot |
k8e-sandbox-cli expose <port> | Expose an in-sandbox service through the k8e API Gateway; returns a public URL (--host, --session-id) |
k8e-sandbox-cli unexpose <port> | Tear down an exposed port (idempotent; --session-id) |
k8e-sandbox-cli exposed | List live exposures for the session |
k8e-sandbox-cli allow-hosts | Freely configure the session egress allowlist, live (--hosts replace, --add, --remove, --clear) |
k8e-sandbox-cli mcp-serve | Serve the sandbox MCP endpoint for MCP-capable agents (--listen, --gateway, TLS + API-key flags, --bootstrap-api-key) |
k8e-sandbox-cli benchmark | Warm-pool latency metrics (--pool-size, --iterations) |
k8e-sandbox-cli catalog | Emit machine-readable command surface (SDK generation) |
k8e sandbox-apikey create <name> | Create API key (default TTL 30 days; --ttl 90d / 720h / 45 / never) â output includes key, e2b_key, and a paste-ready mcp client config block |
k8e sandbox-apikey list | List API key names + expiry (secrets not shown) |
k8e sandbox-apikey delete <name> | Delete an API key (server-side) |
See pkg/sandboxcli/skills/k8e-sandbox/SKILL.md for full usage, plus KIP-17 (profiles + API-key TTL) and KIP-22 (the advertise hostname above).
Examples â from one-liners to a Vue website
See Sandbox Examples â Driven by Your AI Agent for the hands-on walkthrough, where you drive everything from your agent harness (pi / Claude Code / Codex) through the /k8e-sandbox skill. This page is the raw command reference; the examples page shows the same commands being invoked by the skill on your behalf.
A quick taste of what the skill runs for you:
Quick Examples
# Shell command (default lang=bash)
k8e-sandbox-cli run "ls -la /workspace"
# TypeScript â type annotations run via tsx
k8e-sandbox-cli run "const nums: number[] = [1, 2, 3]; console.log(nums.reduce((a, b) => a + b, 0))" --lang ts
# Background exec (returns run_id immediately, then poll)
k8e-sandbox-cli run "sleep 30; echo done" --background
k8e-sandbox-cli poll <run-id>
# Tenant-based cross-process session reuse
k8e-sandbox-cli run "echo hello" --tenant my-project
# Stream long-running output
k8e-sandbox-cli run "python3 train.py" --session-id $SID --raw
# Sub-agent: child session sharing parent pod + workspace
k8e-sandbox-cli subagent <parent-sid>
# Snapshots: content-addressed save/restore
SID=$(k8e-sandbox-cli create | jq -r .session_id)
k8e-sandbox-cli run "echo work > /workspace/state.txt" --session-id $SID
k8e-sandbox-cli snapshot save $SID my-state
k8e-sandbox-cli snapshot list
# ... later, new session from the snapshot:
k8e-sandbox-cli snapshot restore my-state
# incremental restore from an existing snapshot (KIP-16 M2):
k8e-sandbox-cli snapshot restore my-state --base older-state
Expose a Service (KIP-24)
When your code starts a long-running service (web app, API server) inside the sandbox, expose it through the k8e API Gateway and get a URL you can open:
k8e-sandbox-cli run "python3 -m http.server 8080 --bind 127.0.0.1" --background
k8e-sandbox-cli expose 8080
# -> {"url":"http://<gateway>/k8e/expose/<sid>/8080/","port":8080,"session_id":"..."}
curl http://<gateway>/k8e/expose/<sid>/8080/
k8e-sandbox-cli exposed # list live exposures
k8e-sandbox-cli unexpose 8080 # stop exposing
The URL routes through the Cilium Gateway API â the embedded e2b HTTP server â a reverse proxy to the pod â no port-forwarding, no inbound pod exposure.
Troubleshooting
Start with k8e-sandbox-cli doctor â it checks the binary, PATH, profile, certs and skill install, and prints the fix for each failure (--json for a machine-readable report, --fix to apply the safe local repairs).
| Symptom | Cause | Fix |
|---|---|---|
connect fails with a TLS / certificate error | The gateway's private CA is not trusted by your machine | Export the CA on the server (k8e-sandbox-cli tls export-ca or sudo cat /var/lib/k8e/server/tls/sandbox-ca.crt) and pass --ca-file ./sandbox-ca.crt to the first connect |
connect fails to find the gateway | The client is dialling the wrong address or port | The gRPC gateway is :50051; the E2B HTTP surface is :3676 (or the Gateway's :80). k8e-sandbox-cli doctor prints the endpoint it will use |
connect succeeds but later calls fail with sandboxd âĻ: connection timed out | The session's pod was replaced (reclaim, restart, node drain) and the cached pod IP is stale | k8e-sandbox-cli destroy <sid> and run again â a new session claims a fresh pod. run without --session-id always uses a current session |
tls export-ca says sandbox CA not found | You are not on the server host and the client has no kubeconfig | Run it on the server host, or sudo cat /var/lib/k8e/server/tls/sandbox-ca.crt there and copy the file over |
run returns warm pool full: âĻ | The node has no room for another sandbox pod (CPU requests are what bind first on a small node) | Wait for a pod to be reclaimed, or add CPU to the node. kubectl -n sandbox-matrix get pods shows the live pods |
poll <run-id> returns empty stdout while the run is still going | poll returns a status snapshot; the output appears once the run completes | Poll again until status is completed (or use poll --follow) |
run --background output is empty but the process clearly ran | The sandboxd build in use emits raw ANSI escapes inside a JSON string, which makes the response unparsable â the control plane swallows the parse error and reports "no output" | Upgrade the sandbox image to v1.37.0-20261022-rc2 or newer |
| A file watch reports the watched directory instead of the changed file | The sandboxd in use mis-parses the inotify record, so the event name comes back empty | Same as above â the inotify parsing fix is in v1.37.0-20261022-rc2 |
template '<name>' not found | The E2B API's template field selects a Kubernetes RuntimeClass, not an E2B template | Use gvisor, kata, firecracker, crun or lunatic |
| The skill does not appear in your agent | The harness caches its skill list | k8e-sandbox-cli connect --skill-only, then open a new agent session |
Configuration & Auth
Config files (do not mix these up)
| Path | Owner | Purpose |
|---|---|---|
/etc/k8e/config.yaml | k8e server/agent | Daemon CLI flags only â including sandbox-advertise-hostname (step 1). Never read by the CLI. |
~/.k8e/sandbox/profiles.yaml | k8e-sandbox-cli | Named gateways / per-profile cert_dir, device_name |
~/.k8e/sandbox/config.json | k8e-sandbox-cli | Last successful connect / login stamp |
~/.k8e/sandbox-ca.crt | k8e-sandbox-cli | CA exported by k8e-sandbox-cli tls export-ca for IP-only gateways |
~/.k8e/sandbox/{ca.crt,client.crt,client.key} | k8e-sandbox-cli | mTLS material (90-day client cert; auto-renew when <30 days remain) |
/etc/k8e/config.yaml takes the same keys as CLI flags (cobra-style flag-name: value), so anything you can pass to k8e-server you can put in the file â which is what makes sandbox-advertise-hostname survive restarts.
mTLS bootstrap
Remote connect / login uses the API key once to issue a client certificate. The private key never leaves the machine. Later RPCs use mTLS only (no API key on the wire).
First authentication verifies the gateway using system trust, a cached CA, or an administrator-provided --ca-file â obtain that CA through a trusted channel. --insecure-bootstrap bypasses server verification only when no CA is selected; it exposes the API key to server impersonation, so it is not a default step. Concurrent CLI processes serialize credential initialization and renewal.
Client certificates last 90 days and auto-renew when fewer than 30 days remain. If a server is reinstalled or the CA rotated, re-run connect with fresh credentials and the new CA â --reset-certs validates them before replacing the cached files:
k8e-sandbox-cli --endpoint sandbox.example.com:50051 --apikey <key> connect \
--reset-certs --ca-file /path/to/trusted-new-ca.crt
Multi-cluster profiles
# ~/.k8e/sandbox/profiles.yaml â NOT /etc/k8e/config.yaml
version: 1
current_profile: default
profiles:
default:
endpoint: 10.0.0.1:50051
prod:
endpoint: sandbox.prod.example:50051
cert_dir: ~/.k8e/sandbox-prod
device_name: laptop-prod
k8e-sandbox-cli --profile prod connect --apikey <64-hex key>
k8e-sandbox-cli --profile prod run 'echo hi'
# or: export K8E_SANDBOX_PROFILE=prod
connect writes the gateway it authenticated against as the default profile, so later CLI calls dial it with no --endpoint at all.
Resolution order: flags â env â profile â last-connect stamp (config.json) â defaults. The cert dir has no flag: K8E_SANDBOX_CERT_DIR â profile cert_dir â ~/.k8e/sandbox.
Env: K8E_SANDBOX_ENDPOINT, K8E_SANDBOX_APIKEY, K8E_SANDBOX_CERT_DIR, K8E_SANDBOX_PROFILE, K8E_SANDBOX_CONFIG (explicit profiles file path), K8E_SANDBOX_DEVICE_NAME, K8E_SANDBOX_SESSION_ID, K8E_SANDBOX_TENANT.
Overrides
# Authenticate only (mTLS cert; no skill install) â later commands need no --apikey
k8e-sandbox-cli --endpoint sandbox.example.com:50051 --apikey <key> login
k8e-sandbox-cli run "echo hello"
# Or via environment variables:
K8E_SANDBOX_ENDPOINT=sandbox.example.com:50051 K8E_SANDBOX_APIKEY=<key> k8e-sandbox-cli login
# Override endpoint per-command:
K8E_SANDBOX_ENDPOINT=10.0.0.2:50051 k8e-sandbox-cli run "echo hello"