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:

  1. Confirms the repo exists (via gh repo view for GitHub).
  2. 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.
  3. Ensures a persistent tmux session named dev is running.
  4. If the repo isn't cloned yet, kicks off /usr/local/bin/dev-bootstrap inside 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.).
  5. 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::BoxServicecompute.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.