xshellz
The official Ruby SDK for xShellz sandboxes: spawn a real Linux box from your program, run anything in it, throw it away.
What is a sandbox? A sandbox is a small, disposable Linux computer that runs in the cloud, isolated from everything else (including your machine), so whatever runs inside it cannot touch your files or your network. That makes it the safe place to run untrusted things: AI-generated code, build scripts, scrapers, other people's projects.
Each xShellz sandbox is a full Linux environment - root shell, package
manager, network access - running under
gVisor kernel isolation. Spawning is synchronous:
Sandbox.create returns once the box is reachable, typically in a few
seconds.
Quickstart
-
Install the gem:
gem install xshellz -
Get an API key. Sign up at app.xshellz.com, then create a personal access token with
readandwritescopes under Settings -> API tokens (or via the API:POST /v1/auth/tokens). Export it:export XSHELLZ_API_KEY="your-token" -
Hello world:
require "xshellz" Xshellz::Sandbox.create do |sbx| puts sbx.run("echo hello from $(hostname)").stdout end # the box is destroyed when the block exits
Recipes
Run a command
Xshellz::Sandbox.create do |sbx|
r = sbx.run("apt-get update && apt-get install -y jq", timeout: 300)
puts r.exit_code, r.stdout, r.stderr
# A non-zero exit code does NOT raise - it's data, like a local subprocess:
sbx.run("false").ok? # => false
# cwd, env, and live output streaming:
sbx.run("make test", cwd: "/srv/app", env: { "CI" => "1" }) do |stream, chunk|
print(chunk) if stream == :stdout
end
end
A permanent named box that survives restarts
get_or_create finds your box by name, or creates it, and remembers the SSH
key for you in ~/.xshellz/keys/ - so the same line works on every run, from
any process, forever:
sbx = Xshellz::Sandbox.get_or_create("my-dev-box")
sbx.run("echo persists across runs > ~/note.txt")
# ... later, in a different script or on a different day:
sbx = Xshellz::Sandbox.get_or_create("my-dev-box") # same box, same files
A stopped (idled) box is started automatically. Keys are plaintext files with
0600 permissions - delete the key file and kill the box to revoke, or pass
keystore: false to keep keys purely in memory.
Run a background job
spawn starts a process that keeps running after your script disconnects:
job = sbx.spawn("python3 train.py", name: "train")
job.pid # => 4242
job.running? # => true
job.logs(tail_lines: 20) # last 20 lines of combined stdout+stderr
job.stop # SIGTERM, then SIGKILL after a grace period
sbx.jobs.each { |j| puts "#{j.id} running=#{j.running?}" }
Run AI-generated code safely
run_code writes a snippet to a temp file in the sandbox, executes the right
interpreter, and always cleans the file up. If the code is malicious, it can
only harm a disposable box:
result = sbx.run_code("python", generated_code, timeout: 60)
puts result.stdout
Supported languages: python (python3), node, bash, ruby, php.
Files: up and down
sbx.write_file("/tmp/config.json", '{"debug": true}')
sbx.read_file("/tmp/config.json") # => String
sbx.upload("local.txt", "/tmp/remote.txt")
sbx.download("/tmp/remote.txt", "out.txt")
Check resource usage
stats = sbx.stats
puts "mem #{stats.mem_used_mb}/#{stats.mem_allowed_mb} MB, cpu #{stats.cpu_percent}%"
top = sbx.procs
top.procs.each { |p| puts "#{p.pid} #{p.comm} #{p.cpu}%" }
Open a web terminal
Get a browser-ready root shell for the box - no SSH client needed. The signed URL expires after about an hour; mint a fresh one each time:
puts sbx.terminal_url
Provision every new box the same way (boxfile)
The account-level boxfile is a package manifest applied whenever a NEW box is created - preinstall your dependencies once:
Xshellz::Sandbox.set_boxfile(<<~BOX)
apt jq ripgrep
pip requests
BOX
Xshellz::Sandbox.get_boxfile # => the saved manifest
API reference
Every public method, its parameters, return shapes, and errors are documented in docs/API.md.
Configuration
| Environment variable | Meaning | Default |
|---|---|---|
XSHELLZ_API_KEY |
Personal access token (read + write scopes) |
- (required) |
XSHELLZ_API_URL |
Control-plane base URL | https://api.xshellz.com/v1 |
Explicit arguments (api_key:, api_url:) always win over the environment.
Error types
| Error | Meaning |
|---|---|
Xshellz::Error |
Base class for everything the SDK raises |
Xshellz::AuthError |
401/403: bad or missing token, scopes, account gates |
Xshellz::QuotaError |
Plan sandbox limit reached / plan has no sandbox entitlement |
Xshellz::MissingKeyError |
get_or_create found the box but no private key for it |
Xshellz::SandboxNotRunningError |
Operation needs a running box |
Xshellz::CommandTimeoutError |
run(timeout: ...) exceeded |
Xshellz::UnsupportedLanguageError |
run_code got a language it doesn't know |
Xshellz::ApiError |
Any other 4xx/5xx (carries .status and .body) |
v0 limits
- Free tier: 1 concurrent sandbox. A second
Sandbox.createraisesXshellz::QuotaErrorwhile one exists - attach to the existing box instead (get_or_create/Sandbox.connect), orkillit first. - Free boxes idle-stop after ~30 minutes. The box (its
/homeand your key) is preserved;sbx.start- orget_or_create- resumes it. - Sandbox creation is throttled to 10 requests/minute per account.
How it works
- Control plane: HTTPS to
api.xshellz.com/v1(create / list / start / destroy / stats), authenticated by your token. - Data plane: SSH directly to the box as
root(net-ssh), files over SFTP (net-sftp).Sandbox.creategenerates an in-memory ed25519 keypair per sandbox; the private key never leaves your process and the server never sees it - only the public half is installed in the box'sauthorized_keys. - Host keys are auto-accepted (
verify_host_key: :never). 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 usingsbx.ssh_command.
Local development (Docker)
No local Ruby toolchain is required - the whole test suite (with the >= 80%
coverage gate) runs inside a ruby:3.3 container, gems cached in a named
volume:
docker compose run --rm test
License
MIT