Public live-preview URLs for Rails apps you build inside a Claude Cloud VM (claude.ai/code) — or any sandbox that can't accept inbound connections.
A Cloud VM has no port-forwarding and its egress is a TLS-intercepting proxy, so
you can't just open localhost:3000 in a browser, and cloudflared/ngrok
don't get through. This gem exposes your running dev server at
https://<you>-<app>.firstdraft.io through a self-hosted
chisel reverse tunnel (WebSocket over 443,
which the proxy does forward). Each app you build gets its own preview URL and
its own credential.
Two moving parts, both installed for you:
- a Railtie that allows the tunnel's hosts and Action Cable origins, so
tunneled page loads and Turbo Streams / live reload aren't rejected — no
editing of
config/environments. - a keep-alive script (
bin/agent-vm-tunnel) plus Claude Code hooks that bring the database + the app + the tunnel up on session start and every turn, and self-heal after the VM reaps them on idle.
The tunnel server itself lives in firstdraft/agent-vm-tunnel; this gem is the client half you add to the Rails app you're previewing.
Add it to the app you want to preview:
# Gemfile
gem "agent_vm_tunnel", github: "firstdraft/agent_vm_tunnel"
# once it's released on RubyGems, this becomes just: gem "agent_vm_tunnel"bundle install
bin/rails generate agent_vm_tunnel:installThe generator adds:
| File | What it's for |
|---|---|
bin/agent-vm-tunnel |
Idempotent keep-alive: the database + the app + the chisel tunnel |
cloud-vm-setup.sh |
One-shot Cloud VM provisioning (Ruby, your database, gems, chisel) |
.claude/settings.json |
SessionStart + UserPromptSubmit hooks that run bin/agent-vm-tunnel |
It merges into an existing .claude/settings.json — your other hooks and
settings are left alone.
- Get access and create a preview. Open the invite link the tunnel operator
sent you, sign in with GitHub at https://firstdraft.io, and create a preview
for this app (one per app — name it, e.g.
blog). Copy the one line it shows:AGENT_VM_TUNNEL=<slot>:<password>. - In your Cloud VM (claude.ai/code → Add cloud environment), set three fields:
- Setup script → paste this. The field runs at build time and may not start
in your repo's directory, so it finds the repo first, then runs the generated
cloud-vm-setup.sh:(The dashboard shows this same snippet with a Copy button.)#!/bin/bash set -e repo="${CLAUDE_PROJECT_DIR:-}" [ -f "$repo/cloud-vm-setup.sh" ] || repo="$(find / -maxdepth 6 -name cloud-vm-setup.sh -printf '%h\n' 2>/dev/null | head -n1)" cd "$repo" bash cloud-vm-setup.sh
- Environment variables → paste that app's
AGENT_VM_TUNNELline (persists across sessions) - Network access → Full (the tunnel needs unrestricted egress; Trusted blocks it)
- Setup script → paste this. The field runs at build time and may not start
in your repo's directory, so it finds the repo first, then runs the generated
- Start a session. The hooks run
bin/agent-vm-tunnelevery turn, so the app + tunnel come up on their own. Openhttps://<you>-<app>.firstdraft.io.
Run bin/agent-vm-tunnel by hand any time to force a (re)start. The preview URL is
public — turn on Basic Auth from the dashboard if you want a lock on it.
By default everything targets the shared firstdraft.io tunnel. Nothing to configure to use it.
If you run your own agent-vm-tunnel
server, set the host once — the generator bakes it into bin/agent-vm-tunnel and
cloud-vm-setup.sh:
bin/rails generate agent_vm_tunnel:install --host preview.example.comAnd tell the Railtie the same host (so it allows the right hosts/origins), either with an env var:
AGENT_VM_TUNNEL_HOST=preview.example.comor an initializer:
# config/initializers/agent_vm_tunnel.rb
AgentVmTunnel.configure do |config|
config.host = "preview.example.com"
# config.environments = [:development] # where the Railtie applies
# config.allow_localhost = true # also allow http://localhost:* cable origins
# config.extra_allowed_hosts = [".example.dev"]
# config.extra_allowed_origins = [%r{\Ahttps://.*\.example\.dev\z}]
endThe generator also takes --fingerprint if your server's chisel fingerprint
isn't discoverable at https://<host>/tunnel on first connect.
For host firstdraft.io the Railtie is equivalent to adding this to
config/environments/development.rb:
config.hosts << ".firstdraft.io"
config.action_cable.allowed_request_origins = [
%r{\Ahttps://[a-z0-9-]+\.firstdraft\.io\z},
%r{\Ahttp://localhost:\d+\z}
]except it merges with (rather than replaces) any origins already configured, and only applies in the environments you list.
The Cloud VM runs cloud-vm-setup.sh under a ~5-minute build budget.
Installing Ruby is quick (a prebuilt tarball); the variable is bundle install —
a large Gemfile that fetches and compiles native extensions over the network can
blow the budget. If it does, vendor your gems so the install is offline and mostly
precompiled.
The VM is x86_64-linux, which is probably not your machine's platform, so add it
to the lockfile and cache every platform's gems:
bundle lock --add-platform x86_64-linux
bundle cache --all-platforms # writes .gem files into vendor/cache
git add -f Gemfile.lock vendor/cacheCommit that, and bundle install on the VM installs from vendor/cache with no
downloads — and no compilation for gems that ship precompiled x86_64-linux
builds, which most popular native gems (nokogiri, sqlite3, …) do.
For the absolute minimum VM time you can go further and commit a fully-installed
vendor/bundle built on a Linux machine (zero install on the VM), but that's
heavier to produce and keep in sync — reach for it only if vendor/cache isn't
enough.
- Rails 7.0+ (Railtie + generator via
railties) - Ruby 3.3+ (the app's own version is read from
.ruby-version) - An app whose
bin/dev(orbin/rails server) listens on:3000(override withAPP_PORT). SQLite, PostgreSQL, and MySQL/MariaDB all work — the generatedcloud-vm-setup.shdetects the adapter from your lockfile and provisions accordingly (SQLite needs no server).
bin/setup # install deps
bundle exec rake # tests + StandardReleased under the MIT License.