boxd
Ruby SDK for boxd.sh — fork a running Linux VM in ~160 milliseconds.
require "boxd"
box = Boxd::Compute.new.boxes.fork("dev-golden", name: "my-fork")
box.url # => "https://my-fork.boxd.sh"
box.exec!(["uptime"])
box.suspend # idle is ~free; resume is sub-millisecond
That's the whole pitch. The VM is real Linux on a real KVM hypervisor — sudo, kernel modules, Docker, persistent disk. The fork inherits live memory, open sockets, the running process tree. The URL is HTTPS at boot. Idle is sub-microsecond resume away.
Anything you'd build with a container, a serverless function, a CDE, or a code-interpreter sandbox — try it on a boxd VM and see if you stop reaching for the workaround.
Why this gem exists
Modal, Daytona, E2B, Vercel Sandbox, Cloudflare Sandbox, Fly Sprites — every AI-sandbox platform of 2026 ships a Python or TypeScript SDK and stops there. Ruby is left in the cold.
But the Ruby world hasn't gone anywhere. Rails is still the front door to a generation of startups, Heroku-shaped operators still want a real shell on a real machine, and a whole community of CLI-tool authors lives in bin/ directories Ruby still owns.
This gem is the boxd primitive in the idiom Ruby developers expect: snake_case, blocks for ephemeral resources, typed exceptions, frozen_string_literal: true on every file. Pair it with ruby-openai, anthropic, langchain-ruby, or whatever else you already trust — and you have a working agent loop in twenty lines.
Install
gem install boxd
Or in a Gemfile:
gem "boxd"
You'll need the boxd CLI on $PATH (this gem wraps it in v0 — see Status):
curl -fsSL https://boxd.sh/install.sh | sh
boxd login
That's the whole setup. The gem reads your CLI credentials automatically; no API keys to wrangle unless you want to pin one explicitly (Boxd::Compute.new(api_key: "bxd_…")).
boxd-dev — a one-liner dev box for any GitHub or GitLab repo
Installing the gem also drops a boxd-dev binary on your $PATH. Give it a GitHub repo and you're in a tmux session with the repo cloned, deps installed, and Claude one keystroke away:
boxd-dev chad/phoenix # `owner/repo` shorthand → GitHub
boxd-dev https://github.com/chad/phoenix.git # any HTTPS clone URL
boxd-dev git@github.com:chad/phoenix.git # SSH-style remotes
boxd-dev https://gitlab.com/group/proj # other forges work too
boxd-dev # no arg → connects to your default dev VM
What it actually does, end to end:
- Confirms the repo exists (via
gh repo viewfor GitHub). - Forks your golden VM into a per-repo box (
dev-<repo>), in ~160 ms — or resumes the existing one if you've used it before. - Ensures a persistent tmux session named
devis running. - If the repo isn't cloned yet, kicks off
/usr/local/bin/dev-bootstrapinside that tmux session — clones the repo and runs the project's setup script (or falls back to ecosystem defaults:npm install,cargo fetch,mix deps.get,bundle install,uv sync, etc.). - SSHes you straight into the tmux session so you see the bootstrap happen live, then land at a shell in the repo directory.
Subsequent runs against the same repo skip steps 1–4 and just attach. Suspended VMs cost ~$0 and resume in sub-millisecond; you can keep one per project indefinitely.
Want it as boxd dev <repo> instead of boxd-dev? When boxd's CLI grows git/gh-style plugin autoload (it doesn't today), no code change here — same binary becomes the subcommand.
A 15-minute tutorial, in five steps
The examples/ directory is the tutorial. Each file is a self-contained, runnable Ruby program that introduces one more capability. Read them in order and you've seen the whole surface.
Lesson 1 — Run untrusted code in a disposable VM
→ examples/01_run_untrusted_llm_code.rb
The classic "code interpreter" pattern. An LLM hands you a Ruby snippet. You don't trust it. So you fork a fresh box, write the file, run it with a timeout, and throw the VM away. The whole pattern is one block:
result = compute.boxes.ephemeral(fork: "dev-golden") do |box|
box.write_file("/tmp/snippet.rb", llm_generated_code)
box.exec(["ruby", "/tmp/snippet.rb"], timeout: 10)
end
ephemeral destroys the box at end of block — even if the code raises, even if you Ctrl-C out. The agent's malicious rm -rf / happens to a VM that's about to disappear anyway.
Lesson 2 — Fork an agent mid-thought
→ examples/02_fork_during_thinking.rb
Your agent is mid-run inside a box. It's holding state in memory — a parsed AST, a populated cache, a half-completed compile. You want to branch its execution and try two different next steps without losing the warm state.
parent = compute.boxes.fork("dev-golden", name: "parent")
parent.exec!(["bash", "-lc", "warm-up-stuff-here"])
branch_a = compute.boxes.fork(parent.name, name: "branch-a")
branch_b = compute.boxes.fork(parent.name, name: "branch-b")
# Both children inherit the parent's filesystem, memory, and open sockets.
This is the primitive nothing else ships. Vercel and Cloudflare snapshot the filesystem; Modal restores from a deploy-time image. boxd forks the running process tree.
Lesson 3 — Race three agents at the same prompt
→ examples/03_parallel_agent_bake_off.rb
Three agent personalities — architect, designer, hacker — each get their own VM, the same prompt, and a different system message. They build in parallel. You collect the URLs and pick the winner.
%w[architect designer hacker].map do |voice|
Thread.new do
box = compute.boxes.fork("dev-golden", name: "bake-#{voice}")
box.write_file("/tmp/prompt.txt", system_prompt_for(voice) + user_task)
box.exec(["bash", "-lc", 'claude -p "$(cat /tmp/prompt.txt)" --dangerously-skip-permissions'])
puts " #{voice} → #{box.url}"
end
end.each(&:join)
Every VM ships Claude Code pre-authenticated to your account, so there's no API key wrangling. Three boxes, three live URLs, three running apps to compare side by side.
Lesson 4 — Replace your CI runner with a forked golden
→ examples/04_ephemeral_ci_runner.rb
Your CI job needs Docker-in-Docker, sudo, and a warm Cargo cache. GitHub Actions runners are slow and don't trust you with that. Fork a golden that's already loaded with your toolchain instead:
compute.boxes.ephemeral(fork: "ci-golden", name: "ci-#{sha}") do |box|
box.exec!(["bash", "-lc", "git checkout #{sha} && ./script/test"]) do |stream, line|
print "[#{stream}] #{line}"
end
end
The fork inherits the warm cache. The job runs in ~the time it takes to actually compile. Box auto-suspends when idle. You pay for the CPU you actually used.
Lesson 5 — A multi-tenant code interpreter
→ examples/05_multi_tenant_code_interpreter.rb
You ship a Jupyter-style product where every customer session is its own isolated VM. You want cold-start in under a second, state that persists across sessions, and zero-ish cost between them. With fork_or_get plus auto-suspend, the whole pool fits in a class:
def session_for(customer_id)
box = compute.boxes.fork_or_get("interpreter-golden",
name: "ipy-#{customer_id}",
auto_suspend_timeout: 60)
box.resume if box.suspended?
box
end
First call forks the golden in ~160ms. Subsequent calls find the existing VM and resume it in sub-ms. Sixty seconds of network idle and it auto-suspends — disk persists, billing stops.
API reference
Boxd::Compute
compute = Boxd::Compute.new(api_key: "...", environment: "production")
compute.boxes # → BoxService
compute.whoami # → { user:, keys: [...] }
Process-wide config (Rails-friendly):
Boxd.configure do |c|
c.api_key = ENV["BOXD_API_KEY"]
c.environment = "production" # or "staging"
end
Boxd::BoxService — compute.boxes
| Method | Description |
|---|---|
#list |
Every box on the account |
#get(name) |
Fetch by name; raises NotFoundError |
#find(name) |
Fetch by name; returns nil if missing |
#create(name:, auto_suspend_timeout:, restart:) |
New empty VM |
#fork(source, name:, auto_suspend_timeout:) |
Fork an existing VM with full live state |
#fork_or_get(source, name:, **opts) |
Idempotent fork — returns existing box by name, or forks |
| `#ephemeral(fork: \ | create:, name:, **opts) { \ |
Boxd::Box
| Method | Description |
|---|---|
| `#exec(cmd, env:, tty:, timeout:, raise_on_error:) { \ | stream, line\ |
#exec!(cmd, **opts) |
Same; returns stdout String, raises on non-zero |
#write_file(path, content_or_io) |
Upload a file (string or anything #read-able) |
#read_file(remote, local=nil) |
Download a file; returns local path |
#suspend / #pause |
Pause the VM (sub-ms resume) |
#resume |
Wake from suspend |
#reboot |
Cold reboot |
#destroy |
Permanent — disk goes away |
#refresh! |
Refetch status from the API |
#wait_for(status, timeout:) |
Block until status matches |
#url / #name / #vm_id / #status / #image |
Cached attrs |
#running? / #suspended? |
Status helpers |
Exceptions
Boxd::Error
├── Boxd::AuthenticationError # bad token / not logged in
├── Boxd::NotFoundError # VM doesn't exist
├── Boxd::InvalidArgumentError # bad request to the API
├── Boxd::QuotaExceededError # account limit hit
├── Boxd::TimeoutError # exec or operation exceeded its budget
├── Boxd::ConnectionError # network/transport issue
├── Boxd::InternalError # CLI returned non-JSON / unexpected output
├── Boxd::CLIMissingError # `boxd` not on $PATH
└── Boxd::ExecError # exec exited non-zero (carries :exit_code, :stdout, :stderr)
Recipes summary
| File | What you learn |
|---|---|
| 01_run_untrusted_llm_code.rb | ephemeral, write_file, exec with timeout — the code-interpreter pattern |
| 02_fork_during_thinking.rb | Forking a running VM; preserving live state across branches |
| 03_parallel_agent_bake_off.rb | Parallel forks; pre-auth'd Claude Code; comparing strategies |
| 04_ephemeral_ci_runner.rb | Streaming exec; warm-cache forking; replacing your CI runner |
| 05_multi_tenant_code_interpreter.rb | fork_or_get; long-lived per-customer VMs; auto-suspend economics |
Status
v0 (this release) — wraps the boxd CLI. Works today; production-acceptable for back-end and DevOps use cases where shelling is fine. The public Ruby API is stable across the v1 transition.
v1 (planned) — native gRPC client via the grpc gem and the boxd.api.v1 proto. Same public surface; faster, no CLI dependency, first-class streaming.
If you're reaching for something the CLI doesn't expose yet (templates, disks, networks, domains, token management), let us know what you need — open an issue and we'll lift it into the gem.
Contributing
The codebase is small (~500 lines) and meant to stay that way. PRs welcome — especially for:
- Recipes showing real-world patterns
- Coverage of additional CLI surfaces
- A native gRPC backend for v1
Run the smoke test against your own boxd account:
export BOXD_API_KEY=...
export BOXD_GOLDEN=dev-golden # or any box you have
ruby -Ilib spec/smoke_test.rb
License
MIT — see LICENSE.