xshellz
Official Ruby SDK for xShellz sandboxes: throwaway, gVisor-isolated Linux boxes you can spawn and run commands in from your own program - in three lines.
gem install xshellz
require "xshellz"
Xshellz::Sandbox.create do |sbx|
result = sbx.run("ruby -e 'puts 6*7'")
puts result.stdout # 42
end
# the box is destroyed when the block exits
Each sandbox is a real Linux box (root shell, package manager, network) running
under gVisor kernel isolation. Spawning is synchronous -
Sandbox.create returns once the box is running, typically in a few seconds.
Authentication
The SDK authenticates with an xShellz personal access token (PAT) carrying the
read and write scopes:
- Create a token from your xShellz dashboard
(Settings -> API tokens), or via the API:
POST /v1/auth/tokens. - Export it:
export XSHELLZ_API_KEY="your-token"
or pass it explicitly: Xshellz::Sandbox.create(api_key: "your-token").
Config precedence: explicit argument > XSHELLZ_API_KEY / XSHELLZ_API_URL
environment variables > default (https://api.xshellz.com/v1).
To target a staging or self-hosted control plane:
export XSHELLZ_API_URL="https://api.staging.example.com/v1"
Usage
Run commands
Xshellz::Sandbox.create(name: "build-box") 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:
r = sbx.run("false")
r.exit_code # => 1
r.ok? # => false
# cwd and env:
sbx.run("make test", cwd: "/srv/app", env: { "CI" => "1" })
# Stream long-running output as it arrives:
sbx.run("npm run build") do |stream, chunk|
warn(chunk) if stream == :stderr
print(chunk) if stream == :stdout
end
end
Files (SFTP)
sbx.write_file("/tmp/config.json", '{"debug": true}')
data = sbx.read_file("/tmp/config.json") # => String
sbx.upload("local.txt", "/tmp/remote.txt")
sbx.download("/tmp/remote.txt", "out.txt")
Lifecycle
sbx.uuid # sandbox id
sbx.ssh_host # e.g. "shellus1.xshellz.com"
sbx.ssh_port # e.g. 42001
sbx.ssh_command # ready-to-copy "ssh -p 42001 root@..."
sbx.status # "running", "stopped", ...
sbx.detach # keep the box alive after the create block
sbx.kill # destroy the box explicitly
sbx.start # resume an idle-stopped box
# Non-block form: you own the lifecycle.
sbx = Xshellz::Sandbox.create
sbx.run("uname -a")
sbx.kill
# Re-attach later (persist sbx.private_key_openssh + sbx.uuid for this):
sbx = Xshellz::Sandbox.connect(uuid, private_key: saved_private_key)
# Enumerate your sandboxes:
Xshellz::Sandbox.list.each do |info|
puts "#{info.uuid} #{info.status}"
end
Typed errors
begin
sbx = Xshellz::Sandbox.create
rescue Xshellz::QuotaError
# plan limit reached - attach to the existing box instead
existing = Xshellz::Sandbox.list.first
sbx = Xshellz::Sandbox.connect(existing.uuid, private_key: saved_key)
rescue Xshellz::AuthError => e
puts e. # missing/invalid token, scope, or account verification
end
Xshellz::Error- base class for everything the SDK raisesXshellz::AuthError- 401/403: bad or missing token, scopes, account gatesXshellz::QuotaError- plan sandbox limit reached / plan has no sandbox entitlementXshellz::SandboxNotRunningError- operation needs arunningboxXshellz::CommandTimeoutError-run(timeout: ...)exceededXshellz::ApiError- any other 4xx/5xx (carries.statusand.body)
How it works
- Control plane: HTTPS to
api.xshellz.com/v1(create / list / start / destroy), authenticated by your PAT. - 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.
v0 limits
- Free tier: 1 concurrent sandbox. A second
Sandbox.createraisesXshellz::QuotaErrorwhile one exists - useSandbox.list+Sandbox.connectto attach to the existing box, orkillit first. Paid plans raise the limit. - Free boxes idle-stop after ~30 minutes. The box (its
/homeand your key) is preserved; callsbx.startto resume it. - Sandbox creation is throttled to 10 requests/minute per account.
Local development (Docker)
No local Ruby toolchain is required - the whole test suite runs inside a
ruby:3.3 container (gems cached in a named volume):
docker compose run --rm test
License
MIT