The official Python SDK for xShellz sandboxes - spin up a real Linux box from your code, run anything in it, throw it away.
What is a sandbox? A sandbox is a small, isolated Linux computer that lives in the cloud and belongs only to you: it has a root shell, a package manager, its own files and network, and it is walled off from everything else by gVisor kernel isolation. Because it's disposable, it's the safe place to run untrusted or AI-generated code, heavy builds, or experiments you don't want anywhere near your own machine.
-
Install the SDK:
pip install xshellz
-
Get an API key. Sign up at app.xshellz.com, then create a personal access token with
readandwritescopes (Settings -> API tokens, or via the API:POST /v1/auth/tokens). Export it:export XSHELLZ_API_KEY="your-token"
-
Run your first command in a sandbox:
from xshellz import Sandbox with Sandbox.create() as sbx: result = sbx.run("echo hello from $(hostname)") print(result.stdout) # the box is destroyed when the block exits
Sandbox.create() returns once the box is running - typically a few seconds.
with Sandbox.create() as sbx:
r = sbx.run("apt-get update && apt-get install -y jq", timeout=300)
print(r.exit_code, r.stdout, r.stderr)
# A non-zero exit code does NOT raise - it's data, like subprocess:
assert sbx.run("false").exit_code == 1
# Working directory and environment variables:
sbx.run("make test", cwd="/srv/app", env={"CI": "1"})
# Stream long-running output as it happens:
sbx.run("npm run build", on_stdout=print, on_stderr=print)get_or_create gives you the same box back every time you call it with the
same name - from any process, any day. The SSH private key is saved to a local
keystore (~/.xshellz/keys/, file permissions 0600) on first creation and
loaded from there on every reconnect. If the box was idle-stopped, it is
started for you.
sbx = Sandbox.get_or_create("my-dev-box")
sbx.run("echo 'this file survives' >> ~/notes.txt")
sbx.close() # closes connections; the box stays aliveSecurity note: the key sits in plaintext on your disk (0600, owner-only).
Delete the file (or Keystore().delete("my-dev-box")) to revoke local access.
Pass keystore=None to disable persistence, or keystore="/path" to relocate
it.
sbx = Sandbox.get_or_create("worker-box")
job = sbx.spawn("python3 train.py", name="train")
job.is_running() # True while the process is alive
print(job.logs(50)) # last 50 lines of its combined output
job.stop() # SIGTERM, then SIGKILL after a grace period
for info in sbx.jobs(): # every job's log file + liveness
print(info.id, info.pid, info.running)Jobs survive your script exiting; they do not survive the box stopping or restarting.
run_code writes the code to a temp file inside the sandbox, runs the right
interpreter, and always cleans the file up. The code executes in the sandbox,
never on your machine.
llm_output = 'print(sum(range(101)))'
with Sandbox.create() as sbx:
result = sbx.run_code("python", llm_output, timeout=30)
print(result.stdout) # "5050"Supported languages: python, node, bash, ruby, php. Anything else
raises UnsupportedLanguageError.
sbx.write_file("/tmp/config.json", b'{"debug": true}') # bytes -> box
data = sbx.read_file("/tmp/config.json") # box -> bytes
sbx.upload("local.txt", "/tmp/remote.txt") # local file -> box
sbx.download("/tmp/results.csv", "results.csv") # box -> local filestats = sbx.stats()
print(f"mem {stats.mem_used_mb}/{stats.mem_allowed_mb} MB, cpu {stats.cpu_percent}%")
top = sbx.procs()
for p in top.procs:
print(p.pid, p.comm, p.cpu, p.mem)url = sbx.terminal_url() # fresh signed URL, valid ~1 hour
print(f"Open this in a browser: {url}")The URL grants a root shell until it expires - treat it like a password. Mint a fresh one each time instead of storing it.
The account-level boxfile is a provisioning manifest applied when a new box is created - use it to preinstall your dependencies so destroy+recreate reproduces your environment:
Sandbox.set_boxfile("apt: jq ripgrep\npip: httpx rich")
print(Sandbox.get_boxfile())
Sandbox.set_boxfile(None) # clear itEvery public class, method, parameter, return shape, and error is documented in docs/API.md.
| Environment variable | Meaning | Default |
|---|---|---|
XSHELLZ_API_KEY |
Your personal access token | (required) |
XSHELLZ_API_URL |
Control-plane base URL | https://api.xshellz.com/v1 |
Precedence: explicit argument > environment variable > default.
All SDK errors inherit from XshellzError, so except XshellzError catches
everything.
| Error | When it's raised |
|---|---|
AuthError |
401/403: missing/invalid token, scopes, account gates |
QuotaError |
Plan sandbox limit reached, or plan has no sandbox entitlement |
SandboxNotRunningError |
The operation needs a running box |
CommandTimeoutError |
run(timeout=...) exceeded its deadline |
MissingKeyError |
get_or_create found the box but no private key |
UnsupportedLanguageError |
run_code got an unknown language |
ApiError |
Any other 4xx/5xx (carries .status_code and .body) |
- Control plane: HTTPS to
api.xshellz.com/v1(create / list / start / destroy / stats), authenticated with your token. - Data plane: SSH directly to the box as
root.Sandbox.create()generates an in-memory ed25519 keypair per sandbox; the private key never leaves your machine and the server only ever sees the public half. - Host keys are auto-accepted. Sandbox host keys are generated at spawn
time, so there is no out-of-band fingerprint to pin. If your threat model
requires host-key verification, connect manually with your own SSH tooling
using
sbx.ssh_command.
- Free tier: 1 concurrent sandbox. A second
Sandbox.create()raisesQuotaError- attach to the existing box (Sandbox.list()+Sandbox.connect(), orget_or_create) orkill()it first. Paid plans raise the limit. - Free boxes idle-stop after ~30 minutes. The box (its
/homeand your key) is preserved;sbx.start()- or simplyget_or_create- resumes it. - Sandbox creation is throttled to 10 requests/minute per account.
python3 -m venv .venv && . .venv/bin/activate
pip install -e ".[dev]"
ruff check .
pytest --cov=xshellz --cov-fail-under=80No local Python needed - run the full lint + test + coverage suite in a
container (python:3.12-slim, pip cache persisted in a named volume):
docker compose run --rm testThe repo is mounted at /work; the container installs the package with the
[dev] extra, then runs ruff check . and pytest with the 80% coverage
gate. Your host .venv/ is masked inside the container, so host and container
environments never mix.
MIT