Worktree Sandboxes (wt)
heyvm wt pairs a git worktree with a sandbox whose /workspace is that
worktree. One command creates the branch + checkout + VM and drops you
into a shell. Designed for the agent / parallel-branch workflow where
you want to iterate on a change in isolation without stomping on the
main working directory.
On macOS the default backend is apple_virt;
on Linux it's libvirt/firecracker/bubblewrap depending on what's available.
Quickstart
# From inside any git repo. Creates ../<slugified-branch>, checks out
# the branch, boots a VM with that directory mounted at /workspace,
# drops you into an SSH shell.
heyvm wt feature/my-change
When you exit the shell, the VM shuts down. The worktree directory
remains so you can re-enter or delete it with git worktree remove.
Detached mode (--detach)
If you want the VM to outlive the shell — for example, an agent working
in the background, or a long build — add --detach:
heyvm wt feature/my-change --detach
# → spawns a small `heyvm __sandbox_owner` process that owns the VM
# → writes ~/.heyo/sandboxes/<id>/owner.json with the pid + guest IP
# → CLI returns in ~15s; VM keeps running
Reattach from any terminal:
heyvm wt feature/my-change --shell
# or by sandbox id:
heyvm sh sb-xxxxxxxx
Stop the VM:
heyvm stop sb-xxxxxxxx
Running heyvm wt feature/my-change --detach again for the same branch
is idempotent — it reuses the live owner instead of spawning a duplicate.
Inspecting running sandboxes
heyvm wt --list # only wt-*-named sandboxes
heyvm list # all sandboxes, across backends
heyvm list --all # include stopped
heyvm logs sb-xxxxxxxx # tail ~/.heyo/sandboxes/<id>/owner.log
heyvm logs sb-xxxxxxxx -f # follow modeDeploying a worktree to the cloud
Two variants. Both take the local worktree and spin up a cloud sandbox; they differ in what state carries over.
1. Workspace sync (default, libvirt + ubuntu:24.04)
Archives the worktree directory, uploads it, starts a cloud sandbox
whose /workspace mirrors the local tree. No VM state sync — binaries
you installed inside your local apple_virt sandbox don't carry over.
heyvm wt feature/x --deploy
# Optional: override driver/image/region/ttl:
heyvm wt feature/x --deploy \
--deploy-driver libvirt \
--deploy-image ubuntu:24.04 \
--deploy-region US \
--deploy-ttl-seconds 3600 \
--deploy-port 8080
Output ends with Deployed successfully! ID: dep-xxxxxxxx. Use
heyvm exec dep-xxxxxxxx <cmd> or heyvm sh dep-xxxxxxxx to interact.
heyvm stop dep-xxxxxxxx tears it down.
2. Custom image deploy (--publish-image, firecracker)
Builds a full firecracker rootfs from a Dockerfile, publishes it to the
cloud image registry, and deploys the worktree using that image. This
is how you capture installed packages, apt/yum setup, config files —
anything your Dockerfile expresses. Cross-arch (arm64 → amd64) is
handled by docker buildx.
heyvm wt feature/x --deploy --publish-image \
--deploy-dockerfile ./Dockerfile.firecracker \
--deploy-image-name my-agent-image \
--deploy-image-size-mb 4096
Requires Docker Desktop. The Dockerfile must produce a firecracker-
bootable rootfs: see third-party/Dockerfile.firecracker-nginx for a
working template (systemd-free init script at /init.sh, sshd on
boot, etc).
Caveat: firecracker deploy-mode ignores the uploaded workspace archive,
so /workspace is not auto-populated. If you want both a custom image
AND workspace sync, add COPY . /workspace to your Dockerfile.
Custom apple_virt images (local)
To run a worktree sandbox on top of your own base image rather than the default Alpine Linux:
heyvm images build --backend-type apple_virt \
--file ./my-base.Dockerfile \
--name my-base \
--size-mb 4096
heyvm wt feature/x --image my-base
The image lands at ~/.heyo/images/apple_virt/my-base/. Build requires
Docker Desktop. If your Dockerfile installs linux-lts via apk, that
kernel + initrd are used; otherwise the default image's kernel is
injected into /boot/vmlinuz-lts and /boot/initramfs-lts.
See heyvm images build --help for full flag
reference.
Command cheat sheet
| Command | Does |
|---|---|
heyvm wt <branch> | Create worktree + sandbox + attach shell |
heyvm wt <branch> --detach | Same, but background the VM and exit |
heyvm wt <branch> --shell | Attach shell to existing wt sandbox |
heyvm wt --list | List only wt-*-named sandboxes |
heyvm stop <id> | Stop the VM (detached or attached) |
heyvm logs <id> [-f] | Tail the owner process log |
heyvm sh <id> | SSH shell into any sandbox |
heyvm exec <id> <cmd> | Run a command, return output |
heyvm wt <branch> --deploy | Workspace-sync deploy to cloud (libvirt) |
heyvm wt <branch> --deploy --publish-image ... | Build custom image + deploy (firecracker) |
heyvm images build --backend-type apple_virt ... | Custom local base image |
Troubleshooting
VM start failed: ... storage device attachment is invalid— the cached rootfs.img is corrupt. Remove the sandbox dir (rm -rf ~/.heyo/sandboxes/<id>) and letheyvm wtre-create it.- Shell attaches but doesn't respond — the guest's hvc0 shell isn't
up. Check
heyvm logs <id>forssh ready/ serial probe output. If--publish-imageproduced an image that never boots, rebuild it with thethird-party/Dockerfile.firecracker-nginxtemplate as a base. heyvm sh <id>from a fresh terminal says "not found" — the wt name might have slashes that slugify unexpectedly. Use the full sandbox id (sb-xxxxxxxx) fromheyvm wt --list.--publish-imagefails with "Not authenticated" — runheyvm loginfirst, or setHEYO_ARCHIVE_TOKEN.