I wanted my projects off the host. Every install script, every dev tool, every AI CLI that decides to curl | bash something, all of it inside a VM. Not on the laptop where I also have a browser logged into email, GitHub, and my bank.
The thing I keep picturing: I npm install something, it has a postinstall hook, and the hook does what it wants for the next few seconds. On the host that hook can read every project I have ever cloned, every SSH key on my keyring, every token in my shell. I want it to see one project, one VM, one VM-local SSH key that only works for that project. Nothing else.
I looked around first. Nothing fit.
- Dev Containers mount the project workspace into the container by default. The whole point for me was not mounting.
- nono and zerobox sandbox a process on the host. The host is still where the project lives.
- A shared Linux VM through OrbStack or raw Lima puts every project in one place. One bad install can still touch all of them.
- AI-only sandboxes like sbx isolate the agent, but the source still tends to sit on the host. I wanted the whole project off the laptop, not only the model.
So I wrote DVM. The README puts it as “keep your friends close, your supply chain in a VM.”
What it is
A Bash wrapper around Lima. Each project gets its own Fedora VM. Code, dev tools, AI CLIs, and credentials live inside the VM. The host stays small: Lima, Bash, and a config directory under ~/.config/dvm.
How it works
dvm init app writes ~/.config/dvm/vms/app.sh. dvm sync app renders one Lima YAML, starts the VM, runs the baseline, then runs the recipes you picked. dvm sh app opens a shell in the project directory inside the VM. dvm rm app --yes deletes the VM, but only after a dirty-work check on nested Git repos and loose files in the code directory.
Host project directories are not mounted by default. Code is cloned from Git or created inside the VM. If I need it on the host, I commit it or dvm cp it out. The ~ in DVM config always means the guest home, never the host home, so a typo cannot accidentally point a recipe at host files.
What a compromised package sees
This is the part I cared about. Inside a VM, a malicious dependency, postinstall hook, or AI tool can reach:
- The code for that one project.
- The VM’s local SSH key for Git access. It is generated in the guest and never leaves it. Pair it with a single GitHub deploy key and the blast radius is one repo.
- A separate VM-local key for Git commit signing. Splitting auth and signing means a stolen auth key cannot sign commits in my name.
- Whatever the recipes installed: zsh, helix, lazygit, Node, Python, and so on.
What it does not reach:
- Any other project. Other projects are other VMs.
- My host SSH or GPG keys. Host keys are never copied in.
- My host filesystem. Nothing is mounted.
- Cloudflare or Tailscale tokens beyond what one VM needs. Those are staged through mode
0600temp files during sync and not passed throughlimactl shell env, where they would show up in host process listings.
The AI layer
AI CLIs (Codex, Claude, OpenCode, Mistral) get a second wall. The wrappers always run them as a separate user, dvm-agent, inside Bubblewrap. The sandbox mounts the project at /workspace, but it does not mount the main VM user’s home. So even inside the VM, the agent cannot read my dotfiles, my shell history, or anything outside the project tree.
By default the agents run unattended, so I do not have to babysit prompts. DVM_CLAUDE_BYPASS=0 and DVM_CODEX_YOLO=0 turn approval back on when I want it.
Bubblewrap is the inner ring; the VM is the outer ring. Guest root or a bad sudo policy can bypass Bubblewrap. That is why the VM, not the sandbox, is the real boundary.
What it ships with
App VMs with configurable CPU, memory, disk, ports, and recipes. Bundled recipes for the baseline, zsh, git, helix, lazygit, starship, fzf, bat, git-delta, just, tmux, zellij, yazi, Node, Python, and chezmoi. AI recipes for Codex, Claude, OpenCode, and Mistral. Dedicated service VMs for llama (local models), cloudflared (tunnels), and tailscale (private hostnames between VMs, optional Funnel for public URLs). VMs reach each other over lima-dvm-<name>.internal. Local recipes in ~/.config/dvm/recipes override bundled ones, so I can pin a fork without forking DVM itself.
What I gave up
VMs cost disk and memory. Code in a guest has to leave through Git or dvm cp if I want it on the host. DVM does not review what AI tools write, does not back up VM work, and is not stronger than Lima. Guest root or weak sudo inside a VM can bypass the in-guest sandbox.
I am fine with that. The blast radius of “I let an agent run for two hours” used to be my whole laptop. Now it is one VM I can delete.