Make VMs Great Again

I have trust issues.

When Claude Code was released last year I was interested in playing with it, but struggled to find a way to run it in a secure, isolated manner. Trusting its own sandbox was obviously not in the cards. I explored other people’s solutions – mostly using things like Bubblewrap and Docker – but none of them fully satisfied me. It also quickly became evident that most of the value from using Claude Code comes from the --dangerously-skip-permissions argument, which gives it the ability to pursue a targeted task without constant permission requests. This requires even stricter isolation.

Eventually I landed on a full virtual machine being the only viable option. I reinstalled Vagrant for the first time in almost a decade and was off to the races.

I was a heavy user of Vagrant in the twenty-teens. I still really like the interface. But when revisiting it today the software felt rather heavy and clunky. It defaults to using VirtualBox, which has its own set of issues. There is a community provided libvirt plugin, but that seems largely abandoned. “Abandoned” seems to be the word for most of the Vagrant community – probably caused in part by the license change.

So for the past month I’ve been building Migrant, a lightweight VM management tool for running assumed-malicious AI agents in ephemeral environments. The heavy lifting is done by libvirt and QEMU. Migrant started out as just a way to get a more Vagrant-like interface with modern tooling. I use cloud-init to initialize the image, Ansible to configure it, and libvirt for the VM management. But because the whole raison d’ĂȘtre of the project is the fact that non-deterministic systems are inherently untrustworthy, Migrant expanded to have a suite of security features. It has network isolation, so the agent can’t compromise the rest of your LAN. It has shared folder isolation, so that the agent can’t exhaust the host disk or engage in any symlink traversal shenanigans. It has WireGuard tunnel support, implemented host-side such that the VM cannot bypass it (because why wouldn’t you want to run all your agents through Mullvad).

I think it’s pretty great. I use it regularly.

Migrant also serves as my testament as to how agentic coding should work. I’ve written it using Claude Code (initially running in a Vagrant-managed VM, but since the first public commit I’ve been building Migrant-in-Migrant), but it is the antithesis of “vibe coding”. I design the systems. I tell the agent how things should work. I review every line of code it produces. Most of the time I reject its first attempts. I take ownership of and responsibility for commits. The result, I think, is a pretty reasonable looking codebase.

My conclusion thus far is that coding agents are useful tools. They’re an accelerant. They’re great for exploring a problem space. There’s no going back to software development without them, but if they’re not being actively driven by an opinionated human with domain-knowledge and expertise, what they produce is mostly crap. Maybe that will change the future. For now, if you’re not challenging every line of output from the clankers, you’re doing it wrong. I suspect this applies equally to the application of LLMs in other areas, but personally I haven’t found LLMs to be useful for anything other than writing code.