Run coding agents in a container, on a VM, on a git replica.
capsule gives an agent (Claude Code, Cursor CLI) a full Linux machine to work
on without giving it your machine. It works on a copy of your repository that
lives in a VM, commits to its own branch, and hands the result back to you over
git. Your working tree is never touched; you review and merge the agent's
commits like you would a pull request.
It doubles as a per-project dev-environment manager: capsule init/add/rm set
up and maintain a Nix flake devshell so the same toolchain exists on your host
and inside the capsule.
Three layers of isolation, named by the tagline:
- git replica — your repo is pushed to a replica in the VM. The agent
commits there on the
capsulebranch. Nothing flows back until you runcapsule fetch/review/merge. - VM — the agent runs in a Fedora CoreOS VM (
capsule vmboots one with qemu on Apple Silicon), or on any Linux box you pointCAPSULE_VM_HOSTat. - container — inside the VM, the agent runs in a rootless podman container built from a Nix + home-manager image that carries the agent CLIs, direnv, ripgrep, git, and friends.
your repo ──push──▶ replica in VM ──mount──▶ podman container ──▶ agent
▲ │
└────── merge ◀── fetch ◀── commits on `capsule` branch
capsule shell wires this up on first use: it creates a vm git remote,
bootstraps the replica (receive.denyCurrentBranch=updateInstead, so a push
updates the checked-out tree), pushes your current HEAD, then starts the
container mounted on the project. Per-agent state (~/.claude, cursor config)
is kept per profile, so capsule shell work and capsule shell experiment
don't share logins or history.
Your git identity comes along for free: capsule shell copies your host's
user.name / user.email to the VM (once — a VM-side identity you set by hand
wins), and the VM's ~/.gitconfig is mounted read-only into every container. So
the agent's commits are attributed to you without any per-container git config.
Only name and email cross over — not the whole host config, whose commit signing
or credential helpers would only break the agent's commits inside the container.
- Nix with flakes, and direnv — for the devshell commands
- git, ssh
capsule vmonly (Apple Silicon host): qemu, butane, xz, jq, curl. The Nix package wires these intoPATHfor you.
The agent host can be anything reachable over ssh that runs rootless podman.
capsule vm is a convenience for people who don't have a spare Linux box.
With Nix:
nix run github:stevio89/capsule -- help
# or add to a profile / home-manager via the flake's `packages.default`Or just symlink the script — it resolves through symlinks to find share/:
ln -s "$PWD/bin/capsule" ~/.local/bin/capsule# 1. boot a VM (Apple Silicon), or skip this and point CAPSULE_VM_HOST at a Linux box
capsule vm
# 2. in a project under $CAPSULE_ROOT (~/code by default), set up a devshell
cd ~/code/myproject
capsule init go # scaffolds flake.nix, .envrc, .gitignore, justfile
# 3. hand the project to an agent
capsule shell # replica is bootstrapped and pushed on first run
# ...agent works, commits on the `capsule` branch...
# 4. bring the work back
capsule review # every commit + patch waiting in vm/capsule
capsule merge # diffstat, confirm, merge --no-ffProject (host and inside the capsule):
init [lang] [name] |
scaffold flake.nix, .envrc, .gitignore, git repo |
add <pkg>... |
add packages to the devshell flake |
rm <pkg>... |
remove packages |
update |
nix flake update, then reload |
reload |
direnv reload |
lang seeds language packages and a justfile — go and zig ship in
share/templates/.
Capsule (host only):
shell [profile] |
start the agent container for $PWD (default profile: default) |
vm |
boot the qemu VM, downloading the disk if needed |
ssh [cmd...] |
ssh into the VM, or run one command there |
size |
show the VM disk's actual vs virtual size |
pull |
pull the published image, dropping the stale nix volume |
image |
build the image locally instead |
gc |
prune containers/images, collect Nix garbage |
stop |
power off the VM (disk is kept) |
destroy |
delete the VM disk (asks first) |
Handoff (host only):
push |
git push vm HEAD:capsule |
fetch |
git fetch vm — refs only |
review |
show every commit and patch waiting in vm/capsule |
merge |
diffstat, confirm, merge --no-ff, then re-sync the replica |
After a merge into your default branch, capsule pushes the result back to the
replica so the agent's next run is based on the merge, not the stale tip it
committed on — handy when you keep a container open and just restart the agent.
The push is a fast-forward and updateInstead advances the replica's working
tree in place. It's best-effort: if the agent's tree is dirty mid-task (or the
replica has moved ahead of what you merged) the push is skipped with a note to
run capsule push when the agent is idle. Merges into any other branch leave the
replica untouched. Override which branch counts as "default" with
CAPSULE_MAIN_BRANCH (otherwise it's auto-detected from origin/HEAD, then
main/master).
Copy config.example to ~/.config/capsule/config. It is
sourced as a shell script; anything set there overrides the defaults. Common
knobs:
| variable | default | meaning |
|---|---|---|
CAPSULE_ROOT |
$HOME/code |
projects must live under this dir |
CAPSULE_VM_HOST |
core@localhost |
where the agent runs (must be user@host) |
CAPSULE_VM_PORT |
2222 |
ssh port |
CAPSULE_IMAGE |
ghcr.io/stevio89/capsule:latest |
container image |
CAPSULE_BRANCH |
capsule |
branch the agent commits on |
CAPSULE_MAIN_BRANCH |
auto-detected | branch whose merge re-syncs the replica |
CAPSULE_VM_CPUS / CAPSULE_VM_MEM |
4 / 6144 |
VM resources |
CAPSULE_DISK_SIZE |
80G |
VM disk size |
The container is a nixos/nix base with a home-manager profile
for the agent user. It is rebuilt weekly by CI and published as a multi-arch
image to ghcr.io. No flake.lock is committed for the image, so each build
re-locks nixpkgs and ships current agent CLIs — build it yourself with
capsule image if you want to pin.
The flake-rewriting logic (inject / strip / dedup) is covered by a framework-free script — no VM, no network:
bash test/capsule-test.shMIT — see LICENSE.