Coding Agent VMs on NixOS with microvm.nix

Michael Stapelberg Tools

Summary

A technical guide on using microvm.nix on NixOS to create ephemeral VMs for safely running coding agents without access to personal files.

<p>I have come to appreciate <a href="https://en.wikipedia.org/wiki/AI-assisted_software_development">coding agents</a> to be valuable tools for working with computer program code in any capacity, such as learning about any program’s architecture, diagnosing bugs or developing proofs of concept. Depending on the use-case, reviewing each command the agent wants to run can get tedious and time-consuming very quickly. To safely run a coding agent without review, I wanted a Virtual Machine (VM) solution where the agent has no access to my personal files and where it’s no big deal if the agent gets compromised by malware: I can just throw away the VM and start over.</p> <p>Instead of setting up a stateful VM and re-installing it when needed (ugh!), I prefer the model of ephemeral VMs where nothing persists on disk, except for what is explicitly shared with the host.</p> <p>The <a href="https://github.com/microvm-nix/microvm.nix"><code>microvm.nix</code> project</a> makes it easy to create such VMs on NixOS, and this article shows you how I like to set up my VMs.</p> <h2 id="see-also">See also</h2> <p>If you haven’t heard of NixOS before, check out <a href="https://en.wikipedia.org/wiki/NixOS">the NixOS Wikipedia page</a> and <a href="https://nixos.org/">nixos.org</a>. I <a href="/talks/#2025">spoke about why I switched to Nix in 2025</a> and have published a <a href="/posts/tags/nix/">few blog posts about Nix</a>.</p> <p>For understanding the threat model of AI agents, read <a href="https://simonwillison.net/2025/Jun/16/the-lethal-trifecta/">Simon Willison’s “The lethal trifecta for AI agents: private data, untrusted content, and external communication” (June 2025)</a>. This article’s approach to working with the threat model is to remove the “private data” part from the equation.</p> <p>If you want to learn about the whole field of sandboxing, check out <a href="https://www.luiscardoso.dev/blog/sandboxes-for-ai">Luis Cardoso’s “A field guide to sandboxes for AI” (Jan 2026)</a>. I will not be comparing different solutions in this article, I will just show you one possible path.</p> <p>And lastly, maybe you’re not in the mood to build/run sandboxing infrastructure yourself. Good news: Sandboxing is a hot topic and there are many commercial offerings popping up that address this need. For example, David Crawshaw and Josh Bleecher Snyder (I know both from the Go community) recently launched <a href="https://blog.exe.dev/meet-exe.dev">exe.dev</a>, an agent-friendly VM hosting service. Another example is <a href="https://fly.io/blog/code-and-let-live/">Fly.io, who launched Sprites</a>.</p> <h2 id="setting-up-microvmnix">Setting up microvm.nix</h2> <p>Let’s jump right in! The next sections walk you through how I set up my config.</p> <h3 id="step-1-network-prep">Step 1: network prep</h3> <p>First, I created a new <code>microbr</code> bridge which uses <code>192.168.33.1/24</code> as IP address range and NATs out of the <code>eno1</code> network interface. All <code>microvm*</code> interfaces will be added to that bridge:</p> <div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>systemd<span style="color:#666">.</span>network<span style="color:#666">.</span>netdevs<span style="color:#666">.</span><span style="color:#4070a0">&#34;20-microbr&#34;</span><span style="color:#666">.</span>netdevConfig <span style="color:#666">=</span> { </span></span><span style="display:flex;"><span> Kind <span style="color:#666">=</span> <span style="color:#4070a0">&#34;bridge&#34;</span>; </span></span><span style="display:flex;"><span> Name <span style="color:#666">=</span> <span style="color:#4070a0">&#34;microbr&#34;</span>; </span></span><span style="display:flex;"><span>}; </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span>systemd<span style="color:#666">.</span>network<span style="color:#666">.</span>networks<span style="color:#666">.</span><span style="color:#4070a0">&#34;20-microbr&#34;</span> <span style="color:#666">=</span> { </span></span><span style="display:flex;"><span> matchConfig<span style="color:#666">.</span>Name <span style="color:#666">=</span> <span style="color:#4070a0">&#34;microbr&#34;</span>; </span></span><span style="display:flex;"><span> addresses <span style="color:#666">=</span> [ { Address <span style="color:#666">=</span> <span style="color:#4070a0">&#34;192.168.83.1/24&#34;</span>; } ]; </span></span><span style="display:flex;"><span> networkConfig <span style="color:#666">=</span> { </span></span><span style="display:flex;"><span> ConfigureWithoutCarrier <span style="color:#666">=</span> <span style="color:#60add5">true</span>; </span></span><span style="display:flex;"><span> }; </span></span><span style="display:flex;"><span>}; </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span>systemd<span style="color:#666">.</span>network<span style="color:#666">.</span>networks<span style="color:#666">.</span><span style="color:#4070a0">&#34;21-microvm-tap&#34;</span> <span style="color:#666">=</span> { </span></span><span style="display:flex;"><span> matchConfig<span style="color:#666">.</span>Name <span style="color:#666">=</span> <span style="color:#4070a0">&#34;microvm*&#34;</span>; </span></span><span style="display:flex;"><span> networkConfig<span style="color:#666">.</span>Bridge <span style="color:#666">=</span> <span style="color:#4070a0">&#34;microbr&#34;</span>; </span></span><span style="display:flex;"><span>}; </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span>networking<span style="color:#666">.</span>nat <span style="color:#666">=</span> { </span></span><span style="display:flex;"><span> enable <span style="color:#666">=</span> <span style="color:#60add5">true</span>; </span></span><span style="display:flex;"><span> internalInterfaces <span style="color:#666">=</span> [ <span style="color:#4070a0">&#34;microbr&#34;</span> ]; </span></span><span style="display:flex;"><span> externalInterface <span style="color:#666">=</span> <span style="color:#4070a0">&#34;eno1&#34;</span>; </span></span><span style="display:flex;"><span>}; </span></span></code></pre></div><h3 id="step-2-flakenix">Step 2: <code>flake.nix</code></h3> <p>Then, I added the <code>microvm</code> module as a new input to my <code>flake.nix</code> (check out the <a href="https://microvm-nix.github.io/microvm.nix/">microvm.nix documentation</a> for details) and enabled the <code>microvm.nixosModules.host</code> module on the NixOS configuration for my PC (midna). I also created a new <code>microvm.nix</code> file, in which I declare all my VMs. Here’s what my <code>flake.nix</code> looks like:</p> <div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;display:grid;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>{ </span></span><span style="display:flex;"><span> inputs <span style="color:#666">=</span> { </span></span><span style="display:flex;"><span> nixpkgs <span style="color:#666">=</span> { </span></span><span style="display:flex;"><span> url <span style="color:#666">=</span> <span style="color:#4070a0">&#34;github:nixos/nixpkgs/nixos-25.11&#34;</span>; </span></span><span style="display:flex;"><span> }; </span></span><span style="display:flex; background-color:#d8d8d8"><span> <span style="color:#60a0b0;font-style:italic"># For more recent claude-code</span> </span></span><span style="display:flex; background-color:#d8d8d8"><span> nixpkgs-unstable <span style="color:#666">=</span> { </span></span><span style="display:flex; background-color:#d8d8d8"><span> url <span style="color:#666">=</span> <span style="color:#4070a0">&#34;github:nixos/nixpkgs/nixos-unstable&#34;</span>; </span></span><span style="display:flex; background-color:#d8d8d8"><span> }; </span></span><span style="display:flex;"><span> stapelbergnix <span style="color:#666">=</span> { </span></span><span style="display:flex;"><span> url <span style="color:#666">=</span> <span style="color:#4070a0">&#34;github:stapelberg/nix&#34;</span>; </span></span><span style="display:flex;"><span> inputs<span style="color:#666">.</span>nixpkgs<span style="color:#666">.</span>follows <span style="color:#666">=</span> <span style="color:#4070a0">&#34;nixpkgs&#34;</span>; </span></span><span style="display:flex;"><span> }; </span></span><span style="display:flex;"><span> zkjnastools <span style="color:#666">=</span> { </span></span><span style="display:flex;"><span> url <span style="color:#666">=</span> <span style="color:#4070a0">&#34;github:stapelberg/zkj-nas-tools&#34;</span>; </span></span><span style="display:flex;"><span> inputs<span style="color:#666">.</span>nixpkgs<span style="color:#666">.</span>follows <span style="color:#666">=</span> <span style="color:#4070a0">&#34;nixpkgs&#34;</span>; </span></span><span style="display:flex;"><span> }; </span></span><span style="display:flex; background-color:#d8d8d8"><span> microvm <span style="color:#666">=</span> { </span></span><span style="display:flex; background-color:#d8d8d8"><span> url <span style="color:#666">=</span> <span style="color:#4070a0">&#34;github:microvm-nix/microvm.nix&#34;</span>; </span></span><span style="display:flex; background-color:#d8d8d8"><span> inputs<span style="color:#666">.</span>nixpkgs<span style="color:#666">.</span>follows <span style="color:#666">=</span> <span style="color:#4070a0">&#34;nixpkgs&#34;</span>; </span></span><span style="display:flex; background-color:#d8d8d8"><span> }; </span></span><span style="display:flex;"><span> home-manager <span style="color:#666">=</span> { </span></span><span style="display:flex;"><span> url <span style="color:#666">=</span> <span style="color:#4070a0">&#34;github:nix-community/home-manager/release-25.11&#34;</span>; </span></span><span style="display:flex;"><span> inputs<span style="color:#666">.</span>nixpkgs<span style="color:#666">.</span>follows <span style="color:#666">=</span> <span style="color:#4070a0">&#34;nixpkgs&#34;</span>; </span></span><span style="display:flex;"><span> }; </span></span><span style="display:flex;"><span> configfiles <span style="color:#666">=</span> { </span></span><span style="display:flex;"><span> url <span style="color:#666">=</span> <span style="color:#4070a0">&#34;github:stapelberg/configfiles&#34;</span>; </span></span><span style="display:flex;"><span> flake <span style="color:#666">=</span> <span style="color:#60add5">false</span>; <span style="color:#60a0b0;font-style:italic"># repo is not a flake</span> </span></span><span style="display:flex;"><span> }; </span></span><span style="display:flex;"><span> }; </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span> outputs <span style="color:#666">=</span> </span></span><span style="display:flex;"><span> { </span></span><span style="display:flex;"><span> self<span style="color:#666">,</span> </span></span><span style="display:flex;"><span> stapelbergnix<span style="color:#666">,</span> </span></span><span style="display:flex;"><span> zkjnastools<span style="color:#666">,</span> </span></span><span style="display:flex;"><span> nixpkgs<span style="color:#666">,</span> </span></span><span style="display:flex;"><span> nixpkgs-unstable<span style="color:#666">,</span> </span></span><span style="display:flex; background-color:#d8d8d8"><span> microvm<span style="color:#666">,</span> </span></span><span style="display:flex;"><span> home-manager<span style="color:#666">,</span> </span></span><span style="display:flex;"><span> configfiles<span style="color:#666">,</span> </span></span><span style="display:flex;"><span> }<span style="color:#666">@</span>inputs: </span></span><span style="display:flex;"><span> <span style="color:#007020;font-weight:bold">let</span> </span></span><span style="display:flex;"><span> system <span style="color:#666">=</span> <span style="color:#4070a0">&#34;x86_64-linux&#34;</span>; </span></span><span style="display:flex;"><span> pkgs <span style="color:#666">=</span> <span style="color:#007020;font-weight:bold">import</span> nixpkgs { </span></span><span style="display:flex;"><span> <span style="color:#007020;font-weight:bold">inherit</span> system; </span></span><span style="display:flex;"><span> config<span style="color:#666">.</span>allowUnfree <span style="color:#666">=</span> <span style="color:#60add5">false</span>; </span></span><span style="display:flex;"><span> }; </span></span><span style="display:flex;"><span> pkgs-unstable <span style="color:#666">=</span> <span style="color:#007020;font-weight:bold">import</span> nixpkgs-unstable { </span></span><span style="display:flex;"><span> <span style="color:#007020;font-weight:bold">inherit</span> system; </span></span><span style="display:flex;"><span> config<span style="color:#666">.</span>allowUnfree <span style="color:#666">=</span> <span style="color:#60add5">true</span>; </span></span><span style="display:flex;"><span> }; </span></span><span style="display:flex;"><span> <span style="color:#007020;font-weight:bold">in</span> </span></span><span style="display:flex;"><span> { </span></span><span style="display:flex;"><span> nixosConfigurations <span style="color:#666">=</span> { </span></span><span style="display:flex;"><span> midna <span style="color:#666">=</span> nixpkgs<span style="color:#666">.</span>lib<span style="color:#666">.</span>nixosSystem { </span></span><span style="display:flex;"><span> system <span style="color:#666">=</span> <span style="color:#4070a0">&#34;x86_64-linux&#34;</span>; </span></span><span style="display:flex;"><span> specialArgs <span style="color:#666">=</span> { <span style="color:#007020;font-weight:bold">inherit</span> inputs; }; </span></span><span style="display:flex;"><span> modules <span style="color:#666">=</span> [ </span></span><span style="display:flex;"><span> (<span style="color:#007020;font-weight:bold">import</span> <span style="color:#235388">./configuration.nix</span>) </span></span><span style="display:flex;"><span> stapelbergnix<span style="color:#666">.</span>lib<span style="color:#666">.</span>userSettings </span></span><span style="display:flex;"><span> <span style="color:#60a0b0;font-style:italic"># Use systemd for network configuration</span> </span></span><span style="display:flex;"><span> stapelbergnix<span style="color:#666">.</span>lib<span style="color:#666">.</span>systemdNetwork </span></span><span style="display:flex;"><span> <span style="color:#60a0b0;font-style:italic"># Use systemd-boot as bootloader</span> </span></span><span style="display:flex;"><span> stapelbergnix<span style="color:#666">.</span>lib<span style="color:#666">.</span>systemdBoot </span></span><span style="display:flex;"><span> <span style="color:#60a0b0;font-style:italic"># Run prometheus node exporter in tailnet</span> </span></span><span style="display:flex;"><span> stapelbergnix<span style="color:#666">.</span>lib<span style="color:#666">.</span>prometheusNode </span></span><span style="display:flex;"><span> zkjnastools<span style="color:#666">.</span>nixosModules<span style="color:#666">.</span>zkjbackup </span></span><span style="display:flex; background-color:#d8d8d8"><span> microvm<span style="color:#666">.</span>nixosModules<span style="color:#666">.</span>host </span></span><span style="display:flex; background-color:#d8d8d8"><span> <span style="color:#235388">./microvm.nix</span> </span></span><span style="display:flex;"><span> ]; </span></span><span style="display:flex;"><span> }; </span></span><span style="display:flex;"><span> }; </span></span><span style="display:flex;"><span> }; </span></span><span style="display:flex;"><span>}</span></span></code></pre></div> <h3 id="step-3-microvmnix">Step 3: <code>microvm.nix</code></h3> <p>The following <code>microvm.nix</code> declares two microvms, one for Emacs (about which I wanted to learn more) and one for Go Protobuf, a code base I am familiar with and can use to understand Claude’s capabilities:</p> <div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>{ </span></span><span style="display:flex;"><span> config<span style="color:#666">,</span> </span></span><span style="display:flex;"><span> lib<span style="color:#666">,</span> </span></span><span style="display:flex;"><span> pkgs<span style="color:#666">,</span> </span></span><span style="display:flex;"><span> inputs<span style="color:#666">,</span> </span></span><span style="display:flex;"><span> <span style="color:#666">...</span> </span></span><span style="display:flex;"><span>}: </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">let</span> </span></span><span style="display:flex;"><span> <span style="color:#007020;font-weight:bold">inherit</span> (inputs) </span></span><span style="display:flex;"><span> nixpkgs-unstable </span></span><span style="display:flex;"><span> stapelbergnix </span></span><span style="display:flex;"><span> microvm </span></span><span style="display:flex;"><span> configfiles </span></span><span style="display:flex;"><span> home-manager </span></span><span style="display:flex;"><span> ; </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span> microvmBase <span style="color:#666">=</span> <span style="color:#007020;font-weight:bold">import</span> <span style="color:#235388">./microvm-base.nix</span>; </span></span><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">in</span> </span></span><span style="display:flex;"><span>{ </span></span><span style="display:flex;"><span> microvm<span style="color:#666">.</span>vms<span style="color:#666">.</span>emacsvm <span style="color:#666">=</span> { </span></span><span style="display:flex;"><span> autostart <span style="color:#666">=</span> <span style="color:#60add5">false</span>; </span></span><span style="display:flex;"><span> config <span style="color:#666">=</span> { </span></span><span style="display:flex;"><span> imports <span style="color:#666">=</span> [ </span></span><span style="display:flex;"><span> stapelbergnix<span style="color:#666">.</span>lib<span style="color:#666">.</span>userSettings </span></span><span style="display:flex;"><span> microvm<span style="color:#666">.</span>nixosModules<span style="color:#666">.</span>microvm </span></span><span style="display:flex;"><span> (microvmBase { </span></span><span style="display:flex;"><span> hostName <span style="color:#666">=</span> <span style="color:#4070a0">&#34;emacsvm&#34;</span>; </span></span><span style="display:flex;"><span> ipAddress <span style="color:#666">=</span> <span style="color:#4070a0">&#34;192.168.83.6&#34;</span>; </span></span><span style="display:flex;"><span> tapId <span style="color:#666">=</span> <span style="color:#4070a0">&#34;microvm4&#34;</span>; </span></span><span style="display:flex;"><span> mac <span style="color:#666">=</span> <span style="color:#4070a0">&#34;02:00:00:00:00:05&#34;</span>; </span></span><span style="display:flex;"><span> workspace <span style="color:#666">=</span> <span style="color:#4070a0">&#34;/home/michael/microvm/emacs&#34;</span>; </span></span><span style="display:flex;"><span> <span style="color:#007020;font-weight:bold">inherit</span> </span></span><span style="display:flex;"><span> nixpkgs-unstable </span></span><span style="display:flex;"><span> configfiles </span></span><span style="display:flex;"><span> home-manager </span></span><span style="display:flex;"><span> stapelbergnix </span></span><span style="display:flex;"><span> ; </span></span><span style="display:flex;"><span> }) </span></span><span style="display:flex;"><span> <span style="color:#235388">./microvms/emacs.nix</span> </span></span><span style="display:flex;"><span> ]; </span></span><span style="display:flex;"><span> }; </span></span><span style="display:flex;"><span> }; </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span> microvm<span style="color:#666">.</span>vms<span style="color:#666">.</span>goprotobufvm <span style="color:#666">=</span> { </span></span><span style="display:flex;"><span> autostart <span style="color:#666">=</span> <span style="color:#60add5">false</span>; </span></span><span style="display:flex;"><span> config <span style="color:#666">=</span> { </span></span><span style="display:flex;"><span> imports <span style="color:#666">=</span> [ </span></span><span style="display:flex;"><span> stapelbergnix<span style="color:#666">.</span>lib<span style="color:#666">.</span>userSettings </span></span><span style="display:flex;"><span> microvm<span style="color:#666">.</span>nixosModules<span style="color:#666">.</span>microvm </span></span><span style="display:flex;"><span> (microvmBase { </span></span><span style="display:flex;"><span> hostName <span style="color:#666">=</span> <span style="color:#4070a0">&#34;goprotobufvm&#34;</span>; </span></span><span style="display:flex;"><span> ipAddress <span style="color:#666">=</span> <span style="color:#4070a0">&#34;192.168.83.7&#34;</span>; </span></span><span style="display:flex;"><span> tapId <span style="color:#666">=</span> <span style="color:#4070a0">&#34;microvm5&#34;</span>; </span></span><span style="display:flex;"><span> mac <span style="color:#666">=</span> <span style="color:#4070a0">&#34;02:00:00:00:00:06&#34;</span>; </span></span><span style="display:flex;"><span> workspace <span style="color:#666">=</span> <span style="color:#4070a0">&#34;/home/michael/microvm/goprotobuf&#34;</span>; </span></span><span style="display:flex;"><span> <span style="color:#007020;font-weight:bold">inherit</span> </span></span><span style="display:flex;"><span> nixpkgs-unstable </span></span><span style="display:flex;"><span> configfiles </span></span><span style="display:flex;"><span> home-manager </span></span><span style="display:flex;"><span> stapelbergnix </span></span><span style="display:flex;"><span> ; </span></span><span style="display:flex;"><span> extraZshInit <span style="color:#666">=</span> <span style="color:#4070a0">&#39;&#39; </span></span></span><span style="display:flex;"><span><span style="color:#4070a0"> export GOPATH=$HOME/go </span></span></span><span style="display:flex;"><span><span style="color:#4070a0"> export PATH=$GOPATH/bin:$PATH </span></span></span><span style="display:flex;"><span><span style="color:#4070a0"> &#39;&#39;</span>; </span></span><span style="display:flex;"><span> }) </span></span><span style="display:flex;"><span> <span style="color:#235388">./microvms/goprotobuf.nix</span> </span></span><span style="display:flex;"><span> ]; </span></span><span style="display:flex;"><span> }; </span></span><span style="display:flex;"><span> }; </span></span><span style="display:flex;"><span>} </span></span></code></pre></div><h3 id="step-4-microvm-basenix">Step 4: <code>microvm-base.nix</code></h3> <p>The <code>microvm-base.nix</code> module takes these parameters and declares:</p> <ul> <li>Network settings: I like using <a href="https://manpages.debian.org/systemd-networkd.8"><code>systemd-networkd(8)</code></a> and <a href="https://manpages.debian.org/systemd-resolved.8"><code>systemd-resolved(8)</code></a> .</li> <li>Shared directories for: <ul> <li>the workspace directory, e.g. <code>~/microvm/emacs</code></li> <li>the host’s Nix store, so the VM can access software from cache (often)</li> <li>this VM’s SSH host keys</li> <li><code>~/claude-microvm</code>, which is a separate state directory, used only on the microvms.</li> </ul> </li> <li>an 8 GB disk overlay (var.img), stored in <code>/var/lib/microvms/&lt;name&gt;</code></li> <li><code>cloud-hypervisor</code> (QEMU also works well!) as the hypervisor, with 8 vCPUs and 4 GB RAM.</li> <li>A workaround for systemd trying to unmount <code>/nix/store</code> (which causes a deadlock).</li> </ul> <details> <summary>Expand full <code>microvm-base.nix</code> code</summary> <div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>{ </span></span><span style="display:flex;"><span> hostName<span style="color:#666">,</span> </span></span><span style="display:flex;"><span> ipAddress<span style="color:#666">,</span> </span></span><span style="display:flex;"><span> tapId<span style="color:#666">,</span> </span></span><span style="display:flex;"><span> mac<span style="color:#666">,</span> </span></span><span style="display:flex;"><span> workspace<span style="color:#666">,</span> </span></span><span style="display:flex;"><span> nixpkgs-unstable<span style="color:#666">,</span> </span></span><span style="display:flex;"><span> configfiles<span style="color:#666">,</span> </span></span><span style="display:flex;"><span> home-manager<span style="color:#666">,</span> </span></span><span style="display:flex;"><span> stapelbergnix<span style="color:#666">,</span> </span></span><span style="display:flex;"><span> extraZshInit <span style="color:#666">?</span> <span style="color:#4070a0">&#34;&#34;</span><span style="color:#666">,</span> </span></span><span style="display:flex;"><span>}: </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span>{ </span></span><span style="display:flex;"><span> config<span style="color:#666">,</span> </span></span><span style="display:flex;"><span> lib<span style="color:#666">,</span> </span></span><span style="display:flex;"><span> pkgs<span style="color:#666">,</span> </span></span><span style="display:flex;"><span> <span style="color:#666">...</span> </span></span><span style="display:flex;"><span>}: </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">let</span> </span></span><span style="display:flex;"><span> system <span style="color:#666">=</span> pkgs<span style="color:#666">.</span>stdenv<span style="color:#666">.</span>hostPlatform<span style="color:#666">.</span>system; </span></span><span style="display:flex;"><span> pkgsUnstable <span style="color:#666">=</span> <span style="color:#007020;font-weight:bold">import</span> nixpkgs-unstable { </span></span><span style="display:flex;"><span> <span style="color:#007020;font-weight:bold">inherit</span> system; </span></span><span style="display:flex;"><span> config<span style="color:#666">.</span>allowUnfree <span style="color:#666">=</span> <span style="color:#60add5">true</span>; </span></span><span style="display:flex;"><span> }; </span></span><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">in</span> </span></span><span style="display:flex;"><span>{ </span></span><span style="display:flex;"><span> imports <span style="color:#666">=</span> [ home-manager<span style="color:#666">.</span>nixosModules<span style="color:#666">.</span>home-manager ]; </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span> <span style="color:#60a0b0;font-style:italic"># home-manager configuration</span> </span></span><span style="display:flex;"><span> home-manager<span style="color:#666">.</span>useGlobalPkgs <span style="color:#666">=</span> <span style="color:#60add5">true</span>; </span></span><span style="display:flex;"><span> home-manager<span style="color:#666">.</span>useUserPackages <span style="color:#666">=</span> <span style="color:#60add5">true</span>; </span></span><span style="display:flex;"><span> home-manager<span style="color:#666">.</span>extraSpecialArgs <span style="color:#666">=</span> { <span style="color:#007020;font-weight:bold">inherit</span> configfiles stapelbergnix; }; </span></span><span style="display:flex;"><span> home-manager<span style="color:#666">.</span>users<span style="color:#666">.</span>michael <span style="color:#666">=</span> { </span></span><span style="display:flex;"><span> imports <span style="color:#666">=</span> [ <span style="color:#235388">./microvm-home.nix</span> ]; </span></span><span style="display:flex;"><span> microvm<span style="color:#666">.</span>extraZshInit <span style="color:#666">=</span> extraZshInit; </span></span><span style="display:flex;"><span> }; </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span> <span style="color:#60a0b0;font-style:italic"># Claude Code CLI (from nixpkgs-unstable, unfree)</span> </span></span><span style="display:flex;"><span> environment<span style="color:#666">.</span>systemPackages <span style="color:#666">=</span> [ </span></span><span style="display:flex;"><span> pkgsUnstable<span style="color:#666">.</span>claude-code </span></span><span style="display:flex;"><span> ]; </span></span><span style="display:flex;"><span> networking<span style="color:#666">.</span>hostName <span style="color:#666">=</span> hostName; </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span> system<span style="color:#666">.</span>stateVersion <span style="color:#666">=</span> <span style="color:#4070a0">&#34;25.11&#34;</span>; </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span> services<span style="color:#666">.</span>openssh<span style="color:#666">.</span>enable <span style="color:#666">=</span> <span style="color:#60add5">true</span>; </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span> <span style="color:#60a0b0;font-style:italic"># To match midna (host)</span> </span></span><span style="display:flex;"><span> users<span style="color:#666">.</span>groups<span style="color:#666">.</span>michael <span style="color:#666">=</span> { </span></span><span style="display:flex;"><span> gid <span style="color:#666">=</span> <span style="color:#40a070">1000</span>; </span></span><span style="display:flex;"><span> }; </span></span><span style="display:flex;"><span> users<span style="color:#666">.</span>users<span style="color:#666">.</span>michael <span style="color:#666">=</span> { </span></span><span style="display:flex;"><span> group <span style="color:#666">=</span> <span style="color:#4070a0">&#34;michael&#34;</span>; </span></span><span style="display:flex;"><span> }; </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span> services<span style="color:#666">.</span>resolved<span style="color:#666">.</span>enable <span style="color:#666">=</span> <span style="color:#60add5">true</span>; </span></span><span style="display:flex;"><span> networking<span style="color:#666">.</span>useDHCP <span style="color:#666">=</span> <span style="color:#60add5">false</span>; </span></span><span style="display:flex;"><span> networking<span style="color:#666">.</span>useNetworkd <span style="color:#666">=</span> <span style="color:#60add5">true</span>; </span></span><span style="display:flex;"><span> networking<span style="color:#666">.</span>tempAddresses <span style="color:#666">=</span> <span style="color:#4070a0">&#34;disabled&#34;</span>; </span></span><span style="display:flex;"><span> systemd<span style="color:#666">.</span>network<span style="color:#666">.</span>enable <span style="color:#666">=</span> <span style="color:#60add5">true</span>; </span></span><span style="display:flex;"><span> systemd<span style="color:#666">.</span>network<span style="color:#666">.</span>networks<span style="color:#666">.</span><span style="color:#4070a0">&#34;10-e&#34;</span> <span style="color:#666">=</span> { </span></span><span style="display:flex;"><span> matchConfig<span style="color:#666">.</span>Name <span style="color:#666">=</span> <span style="color:#4070a0">&#34;e*&#34;</span>; </span></span><span style="display:flex;"><span> addresses <span style="color:#666">=</span> [ { Address <span style="color:#666">=</span> <span style="color:#4070a0">&#34;</span><span style="color:#70a0d0">${</span>ipAddress<span style="color:#70a0d0">}</span><span style="color:#4070a0">/24&#34;</span>; } ]; </span></span><span style="display:flex;"><span> routes <span style="color:#666">=</span> [ { Gateway <span style="color:#666">=</span> <span style="color:#4070a0">&#34;192.168.83.1&#34;</span>; } ]; </span></span><span style="display:flex;"><span> }; </span></span><span style="display:flex;"><span> networking<span style="color:#666">.</span>nameservers <span style="color:#666">=</span> [ </span></span><span style="display:flex;"><span> <span style="color:#4070a0">&#34;8.8.8.8&#34;</span> </span></span><span style="display:flex;"><span> <span style="color:#4070a0">&#34;1.1.1.1&#34;</span> </span></span><span style="display:flex;"><span> ]; </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span> <span style="color:#60a0b0;font-style:italic"># Disable firewall for faster boot and less hassle;</span> </span></span><span style="display:flex;"><span> <span style="color:#60a0b0;font-style:italic"># we are behind a layer of NAT anyway.</span> </span></span><span style="display:flex;"><span> networking<span style="color:#666">.</span>firewall<span style="color:#666">.</span>enable <span style="color:#666">=</span> <span style="color:#60add5">false</span>; </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span> systemd<span style="color:#666">.</span>settings<span style="color:#666">.</span>Manager <span style="color:#666">=</span> { </span></span><span style="display:flex;"><span> <span style="color:#60a0b0;font-style:italic"># fast shutdowns/reboots! https://mas.to/@zekjur/113109742103219075</span> </span></span><span style="display:flex;"><span> DefaultTimeoutStopSec <span style="color:#666">=</span> <span style="color:#4070a0">&#34;5s&#34;</span>; </span></span><span style="display:flex;"><span> }; </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span> <span style="color:#60a0b0;font-style:italic"># Fix for microvm shutdown hang (issue #170):</span> </span></span><span style="display:flex;"><span> <span style="color:#60a0b0;font-style:italic"># Without this, systemd tries to unmount /nix/store during shutdown,</span> </span></span><span style="display:flex;"><span> <span style="color:#60a0b0;font-style:italic"># but umount lives in /nix/store, causing a deadlock.</span> </span></span><span style="display:flex;"><span> systemd<span style="color:#666">.</span>mounts <span style="color:#666">=</span> [ </span></span><span style="display:flex;"><span> { </span></span><span style="display:flex;"><span> what <span style="color:#666">=</span> <span style="color:#4070a0">&#34;store&#34;</span>; </span></span><span style="display:flex;"><span> where <span style="color:#666">=</span> <span style="color:#4070a0">&#34;/nix/store&#34;</span>; </span></span><span style="display:flex;"><span> overrideStrategy <span style="color:#666">=</span> <span style="color:#4070a0">&#34;asDropin&#34;</span>; </span></span><span style="display:flex;"><span> unitConfig<span style="color:#666">.</span>DefaultDependencies <span style="color:#666">=</span> <span style="color:#60add5">false</span>; </span></span><span style="display:flex;"><span> } </span></span><span style="display:flex;"><span> ]; </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span> <span style="color:#60a0b0;font-style:italic"># Use SSH host keys mounted from outside the VM (remain identical).</span> </span></span><span style="display:flex;"><span> services<span style="color:#666">.</span>openssh<span style="color:#666">.</span>hostKeys <span style="color:#666">=</span> [ </span></span><span style="display:flex;"><span> { </span></span><span style="display:flex;"><span> path <span style="color:#666">=</span> <span style="color:#4070a0">&#34;/etc/ssh/host-keys/ssh_host_ed25519_key&#34;</span>; </span></span><span style="display:flex;"><span> type <span style="color:#666">=</span> <span style="color:#4070a0">&#34;ed25519&#34;</span>; </span></span><span style="display:flex;"><span> } </span></span><span style="display:flex;"><span> ]; </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span> microvm <span style="color:#666">=</span> { </span></span><span style="display:flex;"><span> <span style="color:#60a0b0;font-style:italic"># Enable writable nix store overlay so nix-daemon works.</span> </span></span><span style="display:flex;"><span> <span style="color:#60a0b0;font-style:italic"># This is required for home-manager activation.</span> </span></span><span style="display:flex;"><span> <span style="color:#60a0b0;font-style:italic"># Uses tmpfs by default (ephemeral), which is fine since we</span> </span></span><span style="display:flex;"><span> <span style="color:#60a0b0;font-style:italic"># don&#39;t build anything in the VM.</span> </span></span><span style="display:flex;"><span> writableStoreOverlay <span style="color:#666">=</span> <span style="color:#4070a0">&#34;/nix/.rw-store&#34;</span>; </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span> volumes <span style="color:#666">=</span> [ </span></span><span style="display:flex;"><span> { </span></span><span style="display:flex;"><span> mountPoint <span style="color:#666">=</span> <span style="color:#4070a0">&#34;/var&#34;</span>; </span></span><span style="display:flex;"><span> image <span style="color:#666">=</span> <span style="color:#4070a0">&#34;var.img&#34;</span>; </span></span><span style="display:flex;"><span> size <span style="color:#666">=</span> <span style="color:#40a070">8192</span>; <span style="color:#60a0b0;font-style:italic"># MB</span> </span></span><span style="display:flex;"><span> } </span></span><span style="display:flex;"><span> ]; </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span> shares <span style="color:#666">=</span> [ </span></span><span style="display:flex;"><span> { </span></span><span style="display:flex;"><span> <span style="color:#60a0b0;font-style:italic"># use proto = &#34;virtiofs&#34; for MicroVMs that are started by systemd</span> </span></span><span style="display:flex;"><span> proto <span style="color:#666">=</span> <span style="color:#4070a0">&#34;virtiofs&#34;</span>; </span></span><span style="display:flex;"><span> tag <span style="color:#666">=</span> <span style="color:#4070a0">&#34;ro-store&#34;</span>; </span></span><span style="display:flex;"><span> <span style="color:#60a0b0;font-style:italic"># a host&#39;s /nix/store will be picked up so that no</span> </span></span><span style="display:flex;"><span> <span style="color:#60a0b0;font-style:italic"># squashfs/erofs will be built for it.</span> </span></span><span style="display:flex;"><span> source <span style="color:#666">=</span> <span style="color:#4070a0">&#34;/nix/store&#34;</span>; </span></span><span style="display:flex;"><span> mountPoint <span style="color:#666">=</span> <span style="color:#4070a0">&#34;/nix/.ro-store&#34;</span>; </span></span><span style="display:flex;"><span> } </span></span><span style="display:flex;"><span> { </span></span><span style="display:flex;"><span> proto <span style="color:#666">=</span> <span style="color:#4070a0">&#34;virtiofs&#34;</span>; </span></span><span style="display:flex;"><span> tag <span style="color:#666">=</span> <span style="color:#4070a0">&#34;ssh-keys&#34;</span>; </span></span><span style="display:flex;"><span> source <span style="color:#666">=</span> <span style="color:#4070a0">&#34;</span><span style="color:#70a0d0">${</span>workspace<span style="color:#70a0d0">}</span><span style="color:#4070a0">/ssh-host-keys&#34;</span>; </span></span><span style="display:flex;"><span> mountPoint <span style="color:#666">=</span> <span style="color:#4070a0">&#34;/etc/ssh/host-keys&#34;</span>; </span></span><span style="display:flex;"><span> } </span></span><span style="display:flex;"><span> { </span></span><span style="display:flex;"><span> proto <span style="color:#666">=</span> <span style="color:#4070a0">&#34;virtiofs&#34;</span>; </span></span><span style="display:flex;"><span> tag <span style="color:#666">=</span> <span style="color:#4070a0">&#34;claude-credentials&#34;</span>; </span></span><span style="display:flex;"><span> source <span style="color:#666">=</span> <span style="color:#4070a0">&#34;/home/michael/claude-microvm&#34;</span>; </span></span><span style="display:flex;"><span> mountPoint <span style="color:#666">=</span> <span style="color:#4070a0">&#34;/home/michael/claude-microvm&#34;</span>; </span></span><span style="display:flex;"><span> } </span></span><span style="display:flex;"><span> { </span></span><span style="display:flex;"><span> proto <span style="color:#666">=</span> <span style="color:#4070a0">&#34;virtiofs&#34;</span>; </span></span><span style="display:flex;"><span> tag <span style="color:#666">=</span> <span style="color:#4070a0">&#34;workspace&#34;</span>; </span></span><span style="display:flex;"><span> source <span style="color:#666">=</span> workspace; </span></span><span style="display:flex;"><span> mountPoint <span style="color:#666">=</span> workspace; </span></span><span style="display:flex;"><span> } </span></span><span style="display:flex;"><span> ]; </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span> interfaces <span style="color:#666">=</span> [ </span></span><span style="display:flex;"><span> { </span></span><span style="display:flex;"><span> type <span style="color:#666">=</span> <span style="color:#4070a0">&#34;tap&#34;</span>; </span></span><span style="display:flex;"><span> id <span style="color:#666">=</span> tapId; </span></span><span style="display:flex;"><span> mac <span style="color:#666">=</span> mac; </span></span><span style="display:flex;"><span> } </span></span><span style="display:flex;"><span> ]; </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span> hypervisor <span style="color:#666">=</span> <span style="color:#4070a0">&#34;cloud-hypervisor&#34;</span>; </span></span><span style="display:flex;"><span> vcpu <span style="color:#666">=</span> <span style="color:#40a070">8</span>; </span></span><span style="display:flex;"><span> mem <span style="color:#666">=</span> <span style="color:#40a070">4096</span>; </span></span><span style="display:flex;"><span> socket <span style="color:#666">=</span> <span style="color:#4070a0">&#34;control.socket&#34;</span>; </span></span><span style="display:flex;"><span> }; </span></span><span style="display:flex;"><span>} </span></span></code></pre></div></details> <h3 id="step-5-microvm-homenix">Step 5: <code>microvm-home.nix</code></h3> <p><code>microvm-base.nix</code> in turn pulls in <code>microvm-home.nix</code>, which sets up home-manager to:</p> <ul> <li>Set up Zsh with my configuration</li> <li>Set up Emacs with my configuration</li> <li>Set up Claude Code in shared directory <code>~/claude-microvm</code>.</li> </ul> <details> <summary>Expand full <code>microvm-home.nix</code> code</summary> <div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>{ </span></span><span style="display:flex;"><span> config<span style="color:#666">,</span> </span></span><span style="display:flex;"><span> pkgs<span style="color:#666">,</span> </span></span><span style="display:flex;"><span> lib<span style="color:#666">,</span> </span></span><span style="display:flex;"><span> configfiles<span style="color:#666">,</span> </span></span><span style="display:flex;"><span> stapelbergnix<span style="color:#666">,</span> </span></span><span style="display:flex;"><span> <span style="color:#666">...</span> </span></span><span style="display:flex;"><span>}: </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span>{ </span></span><span style="display:flex;"><span> options<span style="color:#666">.</span>microvm <span style="color:#666">=</span> { </span></span><span style="display:flex;"><span> extraZshInit <span style="color:#666">=</span> lib<span style="color:#666">.</span>mkOption { </span></span><span style="display:flex;"><span> type <span style="color:#666">=</span> lib<span style="color:#666">.</span>types<span style="color:#666">.</span>lines; </span></span><span style="display:flex;"><span> default <span style="color:#666">=</span> <span style="color:#4070a0">&#34;&#34;</span>; </span></span><span style="display:flex;"><span> description <span style="color:#666">=</span> <span style="color:#4070a0">&#34;Extra lines to add to zsh initContent&#34;</span>; </span></span><span style="display:flex;"><span> }; </span></span><span style="display:flex;"><span> }; </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span> config <span style="color:#666">=</span> { </span></span><span style="display:flex;"><span> home<span style="color:#666">.</span>username <span style="color:#666">=</span> <span style="color:#4070a0">&#34;michael&#34;</span>; </span></span><span style="display:flex;"><span> home<span style="color:#666">.</span>homeDirectory <span style="color:#666">=</span> <span style="color:#4070a0">&#34;/home/michael&#34;</span>; </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span> programs<span style="color:#666">.</span>zsh <span style="color:#666">=</span> { </span></span><span style="display:flex;"><span> enable <span style="color:#666">=</span> <span style="color:#60add5">true</span>; </span></span><span style="display:flex;"><span> history <span style="color:#666">=</span> { </span></span><span style="display:flex;"><span> size <span style="color:#666">=</span> <span style="color:#40a070">4000</span>; </span></span><span style="display:flex;"><span> save <span style="color:#666">=</span> <span style="color:#40a070">10000000</span>; </span></span><span style="display:flex;"><span> ignoreDups <span style="color:#666">=</span> <span style="color:#60add5">true</span>; </span></span><span style="display:flex;"><span> share <span style="color:#666">=</span> <span style="color:#60add5">false</span>; </span></span><span style="display:flex;"><span> append <span style="color:#666">=</span> <span style="color:#60add5">true</span>; </span></span><span style="display:flex;"><span> }; </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span> initContent <span style="color:#666">=</span> <span style="color:#4070a0">&#39;&#39; </span></span></span><span style="display:flex;"><span><span style="color:#4070a0"> </span><span style="color:#70a0d0">${</span><span style="color:#007020">builtins</span><span style="color:#666">.</span>readFile <span style="color:#4070a0">&#34;</span><span style="color:#70a0d0">${</span>configfiles<span style="color:#70a0d0">}</span><span style="color:#4070a0">/zshrc&#34;</span><span style="color:#70a0d0">}</span><span style="color:#4070a0"> </span></span></span><span style="display:flex;"><span><span style="color:#4070a0"> export CLAUDE_CONFIG_DIR=/home/michael/claude-microvm </span></span></span><span style="display:flex;"><span><span style="color:#4070a0"> </span><span style="color:#70a0d0">${</span>config<span style="color:#666">.</span>microvm<span style="color:#666">.</span>extraZshInit<span style="color:#70a0d0">}</span><span style="color:#4070a0"> </span></span></span><span style="display:flex;"><span><span style="color:#4070a0"> &#39;&#39;</span>; </span></span><span style="display:flex;"><span> }; </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span> programs<span style="color:#666">.</span>emacs <span style="color:#666">=</span> { </span></span><span style="display:flex;"><span> enable <span style="color:#666">=</span> <span style="color:#60add5">true</span>; </span></span><span style="display:flex;"><span> package <span style="color:#666">=</span> stapelbergnix<span style="color:#666">.</span>lib<span style="color:#666">.</span>emacsWithPackages { <span style="color:#007020;font-weight:bold">inherit</span> pkgs; }; </span></span><span style="display:flex;"><span> }; </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span> home<span style="color:#666">.</span>file<span style="color:#666">.</span><span style="color:#4070a0">&#34;.config/emacs&#34;</span> <span style="color:#666">=</span> { </span></span><span style="display:flex;"><span> source <span style="color:#666">=</span> <span style="color:#4070a0">&#34;</span><span style="color:#70a0d0">${</span>configfiles<span style="color:#70a0d0">}</span><span style="color:#4070a0">/config/emacs&#34;</span>; </span></span><span style="display:flex;"><span> }; </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span> home<span style="color:#666">.</span>stateVersion <span style="color:#666">=</span> <span style="color:#4070a0">&#34;25.11&#34;</span>; </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span> programs<span style="color:#666">.</span>home-manager<span style="color:#666">.</span>enable <span style="color:#666">=</span> <span style="color:#60add5">true</span>; </span></span><span style="display:flex;"><span> }; </span></span><span style="display:flex;"><span>} </span></span></code></pre></div></details> <h3 id="step-6-goprotobufnix">Step 6: <code>goprotobuf.nix</code></h3> <p>The <code>goprotobuf.nix</code> makes available a bunch of required and convenient packages:</p> <div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span><span style="color:#60a0b0;font-style:italic"># Project-specific configuration for goprotobufvm</span> </span></span><span style="display:flex;"><span>{ pkgs<span style="color:#666">,</span> <span style="color:#666">...</span> }: </span></span><span style="display:flex;"><span>{ </span></span><span style="display:flex;"><span> <span style="color:#60a0b0;font-style:italic"># Development environment for Go Protobuf</span> </span></span><span style="display:flex;"><span> environment<span style="color:#666">.</span>systemPackages <span style="color:#666">=</span> <span style="color:#007020;font-weight:bold">with</span> pkgs; [ </span></span><span style="display:flex;"><span> <span style="color:#60a0b0;font-style:italic"># Go toolchain</span> </span></span><span style="display:flex;"><span> go </span></span><span style="display:flex;"><span> gopls </span></span><span style="display:flex;"><span> delve </span></span><span style="display:flex;"><span> protobuf </span></span><span style="display:flex;"><span> gnumake </span></span><span style="display:flex;"><span> gcc </span></span><span style="display:flex;"><span> git </span></span><span style="display:flex;"><span> ripgrep </span></span><span style="display:flex;"><span> ]; </span></span><span style="display:flex;"><span>} </span></span></code></pre></div><h3 id="running-the-vm">Running the VM</h3> <p>Let’s create the workspace directory and create an SSH host key:</p> <pre tabindex="0"><code>mkdir -p ~/microvm/emacs/ssh-host-keys ssh-keygen -t ed25519 -N &#34;&#34; \ -f ~/microvm/emacs/ssh-host-keys/ssh_host_ed25519_key </code></pre><p>Now we can start the VM:</p> <pre tabindex="0"><code>sudo systemctl start microvm@emacsvm </code></pre><p>It boots and responds to pings within a few seconds.</p> <p>Then, SSH into the VM (perhaps in a <a href="https://manpages.debian.org/tmux.1"><code>tmux(1)</code></a> session) and run Claude (or your Coding Agent of choice) without permission prompts in the shared workspace directory:</p> <pre tabindex="0"><code>% ssh 192.168.83.2 emacsvm% cd microvm/emacs emacsvm% claude --dangerously-skip-permissions </code></pre><p>This is what running Claude in such a setup looks like:</p> <a href="https://michael.stapelberg.ch/posts/2026-02-01-coding-agent-microvm-nix/2026-01-28-neofetch-featured.png"><img srcset="https://michael.stapelberg.ch/posts/2026-02-01-coding-agent-microvm-nix/2026-01-28-neofetch-featured_hu_51eebb097010a828.png 2x,https://michael.stapelberg.ch/posts/2026-02-01-coding-agent-microvm-nix/2026-01-28-neofetch-featured_hu_9f1cbedec722fa16.png 3x" src="https://michael.stapelberg.ch/posts/2026-02-01-coding-agent-microvm-nix/2026-01-28-neofetch-featured_hu_d06e7aa7176833b3.png" alt="Claude Code in “bypass permissions” mode" title="Claude Code in “bypass permissions” mode" width="600" height="479" style=" border: 1px solid #000; " loading="lazy"></a> <h2 id="creating-vms-with-claude">Creating VMs with Claude</h2> <p>After going through the process of setting up a MicroVM once, it becomes tedious.</p> <p>I was curious if <a href="https://code.claude.com/docs/en/skills">Claude Skills</a> could help with a task like this. Skills are markdown files that instruct Claude to do certain steps in certain situations.</p> <p>I created <code>.claude/skills/create-microvm/SKILL.md</code> as follows:</p> <div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-markdown" data-lang="markdown"><span style="display:flex;"><span>--- </span></span><span style="display:flex;"><span>name: create-microvm </span></span><span style="display:flex;"><span>description: Creates a new microvm Virtual Machine on midna for running Claude in, with source code repositories and build dependencies available inside the microvm. Use when the user asks to create a new microvm. </span></span><span style="display:flex;"><span>--- </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span>Inspect the existing structure at ~/machines/midna (NixOS configuration using Flakes), which includes several MicroVMs in the ~/machines/midna/microvms/ directory. </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span>Then, create a similar structure for the microvm the user asked to create. Be sure to consider: </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">1.</span> Create a new subdirectory for this microvm, named NAME (the microvm name). </span></span><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">2.</span> Create an entry in microvm.nix similar to an existing microvm&#39;s, but: </span></span><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">3.</span> Change hostname to NAME </span></span><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">4.</span> Change IP address (e.g., 192.168.83.3): find used ones and chose next free </span></span><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">5.</span> Change workspace share to /home/michael/microvm/NAME </span></span><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">6.</span> Include build dependencies for the new microvm based on user request </span></span><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">7.</span> Create ssh-host-keys directory </span></span><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">8.</span> Generate SSH host keys </span></span><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">9.</span> Clone/download source code repositories into this microvm based on user request </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span>Once the microvm is defined, update ~/machines/midna to pull it in. To verify, build the config (but do not deploy). </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span>You need to git-commit changes to the microvm before ~/machines/midna can see them. </span></span></code></pre></div><p>When using this skill with Claude Code (tested version: v2.0.76 and v2.1.15), with the <a href="https://www.anthropic.com/news/claude-opus-4-5">Opus 4.5 model</a>, I can send a prompt like this:</p> <blockquote> <p>please set up a microvm for Debian Code Search (dcs). see ~/dcs for the source code (but clone from <a href="https://github.com/Debian/dcs">https://github.com/Debian/dcs</a>)</p> </blockquote> <p>Now Claude churns for a few minutes, possibly asking a clarification question before that. Afterwards, Claude reports back with:</p> <blockquote> <p>The dcsvm microvm has been set up successfully. Here&rsquo;s what was created:</p> <p>Configuration:</p> <ul> <li>/home/michael/machines/midna/microvms/dcs.nix - Project-specific packages (Go toolchain, protobuf tools, uglify-js, yuicompressor, zopfli)</li> <li>Updated /home/michael/machines/midna/microvm.nix to include dcsvm with: <ul> <li>IP: 192.168.83.8</li> <li>tapId: microvm6</li> <li>mac: 02:00:00:00:00:07</li> </ul> </li> </ul> <p>Workspace:</p> <ul> <li>/home/michael/microvm/dcs/ - Workspace directory</li> <li>/home/michael/microvm/dcs/ssh-host-keys/ - SSH host keys (ed25519)</li> <li>/home/michael/microvm/dcs/dcs/ - Cloned repository from <a href="https://github.com/Debian/dcs">https://github.com/Debian/dcs</a></li> </ul> <p>Build verified - The configuration builds successfully.</p> <p>To start the microvm after deploying: <code>systemctl start microvm@dcsvm</code></p> <p>To SSH into it: <code>ssh [email protected]</code></p> </blockquote> <p>Wonderful! In my experience, Claude always got the VM creation correct.</p> <h2 id="replicating-this-setup-with-claude-code">Replicating this setup with Claude Code</h2> <p>In fact, you can go one step further: Instead of just asking Claude to create new MicroVMs, you can also ask Claude to replicate this entire setup into your NixOS configuration!</p> <p>Try a prompt like this:</p> <blockquote> <p>read <a href="https://michael.stapelberg.ch/posts/2026-02-01-coding-agent-microvm-nix/">https://michael.stapelberg.ch/posts/2026-02-01-coding-agent-microvm-nix/</a> — I want the exact same setup in my midna NixOS configuration please!</p> </blockquote> <h2 id="conclusion">Conclusion</h2> <p>NixOS has a reputation of being hard to adopt, but once you are using NixOS, you can do powerful things like spinning up ephemeral MicroVMs for a new project within minutes.</p> <p>The maintenance effort is minimal: When I update my personal PC, my MicroVM configurations start using the new software versions, too. Customization is easy if needed.</p> <p>This actually mirrors my experience with Coding Agents: I don’t feel like they’re <em>automatically</em> making existing tasks more efficient, I feel that they make things possible that were previously out of reach (similar to <a href="https://en.wikipedia.org/wiki/Jevons_paradox">Jevons paradox</a>).</p> <p>It was fascinating (and scary!) to experience the quality increase of Coding Agents during 2025. At the beginning of 2025 I thought that LLMs are an overhyped toy, and felt it was almost insulting when people showed me text or code produced by these models. But almost every new frontier model release got significantly better, and by now I have been positively surprised by Claude Code’s capabilities and quality many times. It has produced code that handles legitimate edge cases I would not have considered.</p> <p>With this article, I showed one possible way to run Coding Agents safely (or any workload that shouldn’t access your private data, really) that you can adjust in many ways for your needs.</p>
Original Article
View Cached Full Text

Cached at: 05/16/26, 03:32 AM

# Coding Agent VMs on NixOS with microvm.nix Source: [https://michael.stapelberg.ch/posts/2026-02-01-coding-agent-microvm-nix/](https://michael.stapelberg.ch/posts/2026-02-01-coding-agent-microvm-nix/) Table of contents- [See also](https://michael.stapelberg.ch/posts/2026-02-01-coding-agent-microvm-nix/#see-also) - [Setting up microvm\.nix](https://michael.stapelberg.ch/posts/2026-02-01-coding-agent-microvm-nix/#setting-up-microvmnix)- [Step 1: network prep](https://michael.stapelberg.ch/posts/2026-02-01-coding-agent-microvm-nix/#step-1-network-prep) - [Step 2:`flake\.nix`](https://michael.stapelberg.ch/posts/2026-02-01-coding-agent-microvm-nix/#step-2-flakenix) - [Step 3:`microvm\.nix`](https://michael.stapelberg.ch/posts/2026-02-01-coding-agent-microvm-nix/#step-3-microvmnix) - [Step 4:`microvm\-base\.nix`](https://michael.stapelberg.ch/posts/2026-02-01-coding-agent-microvm-nix/#step-4-microvm-basenix) - [Step 5:`microvm\-home\.nix`](https://michael.stapelberg.ch/posts/2026-02-01-coding-agent-microvm-nix/#step-5-microvm-homenix) - [Step 6:`goprotobuf\.nix`](https://michael.stapelberg.ch/posts/2026-02-01-coding-agent-microvm-nix/#step-6-goprotobufnix) - [Running the VM](https://michael.stapelberg.ch/posts/2026-02-01-coding-agent-microvm-nix/#running-the-vm) - [Creating VMs with Claude](https://michael.stapelberg.ch/posts/2026-02-01-coding-agent-microvm-nix/#creating-vms-with-claude) - [Replicating this setup with Claude Code](https://michael.stapelberg.ch/posts/2026-02-01-coding-agent-microvm-nix/#replicating-this-setup-with-claude-code) - [Conclusion](https://michael.stapelberg.ch/posts/2026-02-01-coding-agent-microvm-nix/#conclusion) I have come to appreciate[coding agents](https://en.wikipedia.org/wiki/AI-assisted_software_development)to be valuable tools for working with computer program code in any capacity, such as learning about any program’s architecture, diagnosing bugs or developing proofs of concept\. Depending on the use\-case, reviewing each command the agent wants to run can get tedious and time\-consuming very quickly\. To safely run a coding agent without review, I wanted a Virtual Machine \(VM\) solution where the agent has no access to my personal files and where it’s no big deal if the agent gets compromised by malware: I can just throw away the VM and start over\. Instead of setting up a stateful VM and re\-installing it when needed \(ugh\!\), I prefer the model of ephemeral VMs where nothing persists on disk, except for what is explicitly shared with the host\. The[`microvm\.nix`project](https://github.com/microvm-nix/microvm.nix)makes it easy to create such VMs on NixOS, and this article shows you how I like to set up my VMs\. ## See also If you haven’t heard of NixOS before, check out[the NixOS Wikipedia page](https://en.wikipedia.org/wiki/NixOS)and[nixos\.org](https://nixos.org/)\. I[spoke about why I switched to Nix in 2025](https://michael.stapelberg.ch/talks/#2025)and have published a[few blog posts about Nix](https://michael.stapelberg.ch/posts/tags/nix/)\. For understanding the threat model of AI agents, read[Simon Willison’s “The lethal trifecta for AI agents: private data, untrusted content, and external communication” \(June 2025\)](https://simonwillison.net/2025/Jun/16/the-lethal-trifecta/)\. This article’s approach to working with the threat model is to remove the “private data” part from the equation\. If you want to learn about the whole field of sandboxing, check out[Luis Cardoso’s “A field guide to sandboxes for AI” \(Jan 2026\)](https://www.luiscardoso.dev/blog/sandboxes-for-ai)\. I will not be comparing different solutions in this article, I will just show you one possible path\. And lastly, maybe you’re not in the mood to build/run sandboxing infrastructure yourself\. Good news: Sandboxing is a hot topic and there are many commercial offerings popping up that address this need\. For example, David Crawshaw and Josh Bleecher Snyder \(I know both from the Go community\) recently launched[exe\.dev](https://blog.exe.dev/meet-exe.dev), an agent\-friendly VM hosting service\. Another example is[Fly\.io, who launched Sprites](https://fly.io/blog/code-and-let-live/)\. ## Setting up microvm\.nix Let’s jump right in\! The next sections walk you through how I set up my config\. ### Step 1: network prep First, I created a new`microbr`bridge which uses`192\.168\.33\.1/24`as IP address range and NATs out of the`eno1`network interface\. All`microvm\*`interfaces will be added to that bridge: ``` systemd.network.netdevs."20-microbr".netdevConfig = { Kind = "bridge"; Name = "microbr"; }; systemd.network.networks."20-microbr" = { matchConfig.Name = "microbr"; addresses = [ { Address = "192.168.83.1/24"; } ]; networkConfig = { ConfigureWithoutCarrier = true; }; }; systemd.network.networks."21-microvm-tap" = { matchConfig.Name = "microvm*"; networkConfig.Bridge = "microbr"; }; networking.nat = { enable = true; internalInterfaces = [ "microbr" ]; externalInterface = "eno1"; }; ``` ### Step 2:`flake\.nix` Then, I added the`microvm`module as a new input to my`flake\.nix`\(check out the[microvm\.nix documentation](https://microvm-nix.github.io/microvm.nix/)for details\) and enabled the`microvm\.nixosModules\.host`module on the NixOS configuration for my PC \(midna\)\. I also created a new`microvm\.nix`file, in which I declare all my VMs\. Here’s what my`flake\.nix`looks like: ``` { inputs = { nixpkgs = { url = "github:nixos/nixpkgs/nixos-25.11"; }; # For more recent claude-code nixpkgs-unstable = { url = "github:nixos/nixpkgs/nixos-unstable"; }; stapelbergnix = { url = "github:stapelberg/nix"; inputs.nixpkgs.follows = "nixpkgs"; }; zkjnastools = { url = "github:stapelberg/zkj-nas-tools"; inputs.nixpkgs.follows = "nixpkgs"; }; microvm = { url = "github:microvm-nix/microvm.nix"; inputs.nixpkgs.follows = "nixpkgs"; }; home-manager = { url = "github:nix-community/home-manager/release-25.11"; inputs.nixpkgs.follows = "nixpkgs"; }; configfiles = { url = "github:stapelberg/configfiles"; flake = false; # repo is not a flake }; }; outputs = { self, stapelbergnix, zkjnastools, nixpkgs, nixpkgs-unstable, microvm, home-manager, configfiles, }@inputs: let system = "x86_64-linux"; pkgs = import nixpkgs { inherit system; config.allowUnfree = false; }; pkgs-unstable = import nixpkgs-unstable { inherit system; config.allowUnfree = true; }; in { nixosConfigurations = { midna = nixpkgs.lib.nixosSystem { system = "x86_64-linux"; specialArgs = { inherit inputs; }; modules = [ (import ./configuration.nix) stapelbergnix.lib.userSettings # Use systemd for network configuration stapelbergnix.lib.systemdNetwork # Use systemd-boot as bootloader stapelbergnix.lib.systemdBoot # Run prometheus node exporter in tailnet stapelbergnix.lib.prometheusNode zkjnastools.nixosModules.zkjbackup microvm.nixosModules.host ./microvm.nix ]; }; }; }; } ``` ### Step 3:`microvm\.nix` The following`microvm\.nix`declares two microvms, one for Emacs \(about which I wanted to learn more\) and one for Go Protobuf, a code base I am familiar with and can use to understand Claude’s capabilities: ``` { config, lib, pkgs, inputs, ... }: let inherit (inputs) nixpkgs-unstable stapelbergnix microvm configfiles home-manager ; microvmBase = import ./microvm-base.nix; in { microvm.vms.emacsvm = { autostart = false; config = { imports = [ stapelbergnix.lib.userSettings microvm.nixosModules.microvm (microvmBase { hostName = "emacsvm"; ipAddress = "192.168.83.6"; tapId = "microvm4"; mac = "02:00:00:00:00:05"; workspace = "/home/michael/microvm/emacs"; inherit nixpkgs-unstable configfiles home-manager stapelbergnix ; }) ./microvms/emacs.nix ]; }; }; microvm.vms.goprotobufvm = { autostart = false; config = { imports = [ stapelbergnix.lib.userSettings microvm.nixosModules.microvm (microvmBase { hostName = "goprotobufvm"; ipAddress = "192.168.83.7"; tapId = "microvm5"; mac = "02:00:00:00:00:06"; workspace = "/home/michael/microvm/goprotobuf"; inherit nixpkgs-unstable configfiles home-manager stapelbergnix ; extraZshInit = '' export GOPATH=$HOME/go export PATH=$GOPATH/bin:$PATH ''; }) ./microvms/goprotobuf.nix ]; }; }; } ``` ### Step 4:`microvm\-base\.nix` The`microvm\-base\.nix`module takes these parameters and declares: - Network settings: I like using[`systemd\-networkd\(8\)`](https://manpages.debian.org/systemd-networkd.8)and[`systemd\-resolved\(8\)`](https://manpages.debian.org/systemd-resolved.8)\. - Shared directories for:- the workspace directory, e\.g\.`~/microvm/emacs` - the host’s Nix store, so the VM can access software from cache \(often\) - this VM’s SSH host keys - `~/claude\-microvm`, which is a separate state directory, used only on the microvms\. - an 8 GB disk overlay \(var\.img\), stored in`/var/lib/microvms/<name\>` - `cloud\-hypervisor`\(QEMU also works well\!\) as the hypervisor, with 8 vCPUs and 4 GB RAM\. - A workaround for systemd trying to unmount`/nix/store`\(which causes a deadlock\)\. Expand full`microvm\-base\.nix`code``` { hostName, ipAddress, tapId, mac, workspace, nixpkgs-unstable, configfiles, home-manager, stapelbergnix, extraZshInit ? "", }: { config, lib, pkgs, ... }: let system = pkgs.stdenv.hostPlatform.system; pkgsUnstable = import nixpkgs-unstable { inherit system; config.allowUnfree = true; }; in { imports = [ home-manager.nixosModules.home-manager ]; # home-manager configuration home-manager.useGlobalPkgs = true; home-manager.useUserPackages = true; home-manager.extraSpecialArgs = { inherit configfiles stapelbergnix; }; home-manager.users.michael = { imports = [ ./microvm-home.nix ]; microvm.extraZshInit = extraZshInit; }; # Claude Code CLI (from nixpkgs-unstable, unfree) environment.systemPackages = [ pkgsUnstable.claude-code ]; networking.hostName = hostName; system.stateVersion = "25.11"; services.openssh.enable = true; # To match midna (host) users.groups.michael = { gid = 1000; }; users.users.michael = { group = "michael"; }; services.resolved.enable = true; networking.useDHCP = false; networking.useNetworkd = true; networking.tempAddresses = "disabled"; systemd.network.enable = true; systemd.network.networks."10-e" = { matchConfig.Name = "e*"; addresses = [ { Address = "${ipAddress}/24"; } ]; routes = [ { Gateway = "192.168.83.1"; } ]; }; networking.nameservers = [ "8.8.8.8" "1.1.1.1" ]; # Disable firewall for faster boot and less hassle; # we are behind a layer of NAT anyway. networking.firewall.enable = false; systemd.settings.Manager = { # fast shutdowns/reboots! https://mas.to/@zekjur/113109742103219075 DefaultTimeoutStopSec = "5s"; }; # Fix for microvm shutdown hang (issue #170): # Without this, systemd tries to unmount /nix/store during shutdown, # but umount lives in /nix/store, causing a deadlock. systemd.mounts = [ { what = "store"; where = "/nix/store"; overrideStrategy = "asDropin"; unitConfig.DefaultDependencies = false; } ]; # Use SSH host keys mounted from outside the VM (remain identical). services.openssh.hostKeys = [ { path = "/etc/ssh/host-keys/ssh_host_ed25519_key"; type = "ed25519"; } ]; microvm = { # Enable writable nix store overlay so nix-daemon works. # This is required for home-manager activation. # Uses tmpfs by default (ephemeral), which is fine since we # don't build anything in the VM. writableStoreOverlay = "/nix/.rw-store"; volumes = [ { mountPoint = "/var"; image = "var.img"; size = 8192; # MB } ]; shares = [ { # use proto = "virtiofs" for MicroVMs that are started by systemd proto = "virtiofs"; tag = "ro-store"; # a host's /nix/store will be picked up so that no # squashfs/erofs will be built for it. source = "/nix/store"; mountPoint = "/nix/.ro-store"; } { proto = "virtiofs"; tag = "ssh-keys"; source = "${workspace}/ssh-host-keys"; mountPoint = "/etc/ssh/host-keys"; } { proto = "virtiofs"; tag = "claude-credentials"; source = "/home/michael/claude-microvm"; mountPoint = "/home/michael/claude-microvm"; } { proto = "virtiofs"; tag = "workspace"; source = workspace; mountPoint = workspace; } ]; interfaces = [ { type = "tap"; id = tapId; mac = mac; } ]; hypervisor = "cloud-hypervisor"; vcpu = 8; mem = 4096; socket = "control.socket"; }; } ``` ### Step 5:`microvm\-home\.nix` `microvm\-base\.nix`in turn pulls in`microvm\-home\.nix`, which sets up home\-manager to: - Set up Zsh with my configuration - Set up Emacs with my configuration - Set up Claude Code in shared directory`~/claude\-microvm`\. Expand full`microvm\-home\.nix`code``` { config, pkgs, lib, configfiles, stapelbergnix, ... }: { options.microvm = { extraZshInit = lib.mkOption { type = lib.types.lines; default = ""; description = "Extra lines to add to zsh initContent"; }; }; config = { home.username = "michael"; home.homeDirectory = "/home/michael"; programs.zsh = { enable = true; history = { size = 4000; save = 10000000; ignoreDups = true; share = false; append = true; }; initContent = '' ${builtins.readFile "${configfiles}/zshrc"} export CLAUDE_CONFIG_DIR=/home/michael/claude-microvm ${config.microvm.extraZshInit} ''; }; programs.emacs = { enable = true; package = stapelbergnix.lib.emacsWithPackages { inherit pkgs; }; }; home.file.".config/emacs" = { source = "${configfiles}/config/emacs"; }; home.stateVersion = "25.11"; programs.home-manager.enable = true; }; } ``` ### Step 6:`goprotobuf\.nix` The`goprotobuf\.nix`makes available a bunch of required and convenient packages: ``` # Project-specific configuration for goprotobufvm { pkgs, ... }: { # Development environment for Go Protobuf environment.systemPackages = with pkgs; [ # Go toolchain go gopls delve protobuf gnumake gcc git ripgrep ]; } ``` ### Running the VM Let’s create the workspace directory and create an SSH host key: ``` mkdir -p ~/microvm/emacs/ssh-host-keys ssh-keygen -t ed25519 -N "" \ -f ~/microvm/emacs/ssh-host-keys/ssh_host_ed25519_key ``` Now we can start the VM: ``` sudo systemctl start microvm@emacsvm ``` It boots and responds to pings within a few seconds\. Then, SSH into the VM \(perhaps in a[`tmux\(1\)`](https://manpages.debian.org/tmux.1)session\) and run Claude \(or your Coding Agent of choice\) without permission prompts in the shared workspace directory: ``` % ssh 192.168.83.2 emacsvm% cd microvm/emacs emacsvm% claude --dangerously-skip-permissions ``` This is what running Claude in such a setup looks like: [![Claude Code in “bypass permissions” mode](https://michael.stapelberg.ch/posts/2026-02-01-coding-agent-microvm-nix/2026-01-28-neofetch-featured_hu_d06e7aa7176833b3.png)](https://michael.stapelberg.ch/posts/2026-02-01-coding-agent-microvm-nix/2026-01-28-neofetch-featured.png) ## Creating VMs with Claude After going through the process of setting up a MicroVM once, it becomes tedious\. I was curious if[Claude Skills](https://code.claude.com/docs/en/skills)could help with a task like this\. Skills are markdown files that instruct Claude to do certain steps in certain situations\. I created`\.claude/skills/create\-microvm/SKILL\.md`as follows: ``` --- name: create-microvm description: Creates a new microvm Virtual Machine on midna for running Claude in, with source code repositories and build dependencies available inside the microvm. Use when the user asks to create a new microvm. --- Inspect the existing structure at ~/machines/midna (NixOS configuration using Flakes), which includes several MicroVMs in the ~/machines/midna/microvms/ directory. Then, create a similar structure for the microvm the user asked to create. Be sure to consider: 1. Create a new subdirectory for this microvm, named NAME (the microvm name). 2. Create an entry in microvm.nix similar to an existing microvm's, but: 3. Change hostname to NAME 4. Change IP address (e.g., 192.168.83.3): find used ones and chose next free 5. Change workspace share to /home/michael/microvm/NAME 6. Include build dependencies for the new microvm based on user request 7. Create ssh-host-keys directory 8. Generate SSH host keys 9. Clone/download source code repositories into this microvm based on user request Once the microvm is defined, update ~/machines/midna to pull it in. To verify, build the config (but do not deploy). You need to git-commit changes to the microvm before ~/machines/midna can see them. ``` When using this skill with Claude Code \(tested version: v2\.0\.76 and v2\.1\.15\), with the[Opus 4\.5 model](https://www.anthropic.com/news/claude-opus-4-5), I can send a prompt like this: > please set up a microvm for Debian Code Search \(dcs\)\. see ~/dcs for the source code \(but clone from[https://github\.com/Debian/dcs](https://github.com/Debian/dcs)\) Now Claude churns for a few minutes, possibly asking a clarification question before that\. Afterwards, Claude reports back with: > The dcsvm microvm has been set up successfully\. Here’s what was created: Configuration: - /home/michael/machines/midna/microvms/dcs\.nix \- Project\-specific packages \(Go toolchain, protobuf tools, uglify\-js, yuicompressor, zopfli\) - Updated /home/michael/machines/midna/microvm\.nix to include dcsvm with:- IP: 192\.168\.83\.8 - tapId: microvm6 - mac: 02:00:00:00:00:07 Workspace: - /home/michael/microvm/dcs/ \- Workspace directory - /home/michael/microvm/dcs/ssh\-host\-keys/ \- SSH host keys \(ed25519\) - /home/michael/microvm/dcs/dcs/ \- Cloned repository from[https://github\.com/Debian/dcs](https://github.com/Debian/dcs) Build verified \- The configuration builds successfully\. To start the microvm after deploying:`systemctl start microvm@dcsvm` To SSH into it:`ssh michael@192\.168\.83\.8` Wonderful\! In my experience, Claude always got the VM creation correct\. ## Replicating this setup with Claude Code In fact, you can go one step further: Instead of just asking Claude to create new MicroVMs, you can also ask Claude to replicate this entire setup into your NixOS configuration\! Try a prompt like this: > read[https://michael\.stapelberg\.ch/posts/2026\-02\-01\-coding\-agent\-microvm\-nix/](https://michael.stapelberg.ch/posts/2026-02-01-coding-agent-microvm-nix/)— I want the exact same setup in my midna NixOS configuration please\! ## Conclusion NixOS has a reputation of being hard to adopt, but once you are using NixOS, you can do powerful things like spinning up ephemeral MicroVMs for a new project within minutes\. The maintenance effort is minimal: When I update my personal PC, my MicroVM configurations start using the new software versions, too\. Customization is easy if needed\. This actually mirrors my experience with Coding Agents: I don’t feel like they’re*automatically*making existing tasks more efficient, I feel that they make things possible that were previously out of reach \(similar to[Jevons paradox](https://en.wikipedia.org/wiki/Jevons_paradox)\)\. It was fascinating \(and scary\!\) to experience the quality increase of Coding Agents during 2025\. At the beginning of 2025 I thought that LLMs are an overhyped toy, and felt it was almost insulting when people showed me text or code produced by these models\. But almost every new frontier model release got significantly better, and by now I have been positively surprised by Claude Code’s capabilities and quality many times\. It has produced code that handles legitimate edge cases I would not have considered\. With this article, I showed one possible way to run Coding Agents safely \(or any workload that shouldn’t access your private data, really\) that you can adjust in many ways for your needs\. Did you like this post?[Subscribe to this blog’s RSS feed](https://michael.stapelberg.ch/feed.xml)to not miss any new posts\! I run a blog since 2005, spreading knowledge and experience for over 20 years\! :\)

Similar Articles

Development shells with Nix: four quick examples

Michael Stapelberg

A tutorial demonstrating four ways to set up development shells using Nix, including interactive one-offs, config files, and hermetic Nix Flakes, using GoCV and OpenCV as an example.

How I like to install NixOS (declaratively)

Michael Stapelberg

A guide on declaratively installing NixOS over the network using tools like nixos-anywhere, with an emphasis on managing configuration files under version control.

Running my agents in a VPS

Lobsters Hottest

The author outlines a method for running AI coding agents on an isolated VPS to enable autonomous, asynchronous work without compromising their local machine's security.