使用 microvm.nix 在 NixOS 上运行 Coding Agent 虚拟机
我逐渐认识到,coding agents(编程代理) 是处理计算机程序代码的宝贵工具,无论具体工作是什么:了解程序的架构、诊断 bug,或开发概念验证。根据使用场景的不同,逐条审查代理想要运行的命令,很快就会变得乏味且耗时。为了无需审查也能安全地运行 coding agent,我希望有一种 Virtual Machine(虚拟机) 方案:代理无法访问我的个人文件,即使代理感染了恶意软件也没什么大不了的——我只需删除这台 VM,然后重新开始。
我不想搭建一台有状态的 VM、需要时再重新安装它(唉!),而更喜欢使用 ephemeral VM(临时虚拟机) 的模式:除非明确与主机共享,否则磁盘上不会保留任何内容。
microvm.nix 项目让你可以轻松地在 NixOS 上创建这类 VM,本文将介绍我喜欢采用的 VM 设置方式。
另请参阅
如果你以前没听说过 NixOS,可以看看维基百科上的 NixOS 页面和 nixos.org。我曾在 2025 年发表演讲,介绍自己为何改用 Nix,也发表过几篇关于 Nix 的博客文章。
若要了解 AI 代理的 threat model(威胁模型),请阅读Simon Willison(西蒙·威利森)的《The lethal trifecta for AI agents: private data, untrusted content, and external communication》(2025 年 6 月)。本文应对该威胁模型的方法,是把“私人数据”这一部分从等式中移除。
如果你想了解 sandboxing(沙箱隔离) 的整个领域,可以看看 Luis Cardoso(路易斯·卡多佐)的《A field guide to sandboxes for AI》(2026 年 1 月)。本文不会比较不同方案,只会展示一条可行路径。
最后,也许你没有心情亲自构建和运行 sandboxing 基础设施。好消息是:Sandboxing 是个热门话题,市面上正不断出现许多能够满足这一需求的商业服务。例如,David Crawshaw(戴维·克劳肖)和 Josh Bleecher Snyder(乔什·布利彻·斯奈德)(我在 Go 社区认识他们两位)最近推出了 exe.dev,这是一项对代理友好的 VM 托管服务。另一个例子是推出 Sprites 的 Fly.io。
设置 microvm.nix
我们直接开始吧!接下来的部分将逐步介绍我是如何设置配置的。
步骤 1:准备网络
首先,我创建了一个新的 microbr 网桥,使用 192.168.33.1/24 作为 IP 地址范围,并通过 eno1 网络接口执行 NAT。所有 microvm* 接口都会添加到这个网桥:
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";
};步骤 2:flake.nix
然后,我在 flake.nix 中添加了 microvm 模块作为新的输入(详情请参阅 microvm.nix 文档),并在 PC(midna)的 NixOS 配置中启用了 microvm.nixosModules.host 模块。我还创建了一个新的 microvm.nix 文件,在其中声明所有 VM。下面是我的 flake.nix:
{
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
];
};
};
};
}步骤 3:microvm.nix
下面的 microvm.nix 声明了两个 microvm,一个用于 Emacs(我想进一步了解它),另一个用于 Go Protobuf——这是我熟悉的代码库,可以用来了解 Claude 的能力:
{
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
];
};
};
}步骤 4:microvm-base.nix
microvm-base.nix 模块接收这些参数,并声明:
- 网络设置:我喜欢使用 systemd-networkd(8) 和 systemd-resolved(8)。
- 以下目录的共享:
- 工作区目录,例如
~/microvm/emacs - 主机的 Nix store,这样 VM 通常可以从缓存中访问软件
- 此 VM 的 SSH 主机密钥
~/claude-microvm,这是一个独立的状态目录,仅供 microvm 使用。
- 工作区目录,例如
- 一个 8 GB 的磁盘 overlay(var.img),存储在
/var/lib/microvms/<name>中 - 使用
cloud-hypervisor(QEMU 也很好用!)作为 hypervisor(虚拟机监控程序),配置 8 个 vCPU 和 4 GB 内存。 - 解决 systemd 尝试卸载
/nix/store的问题(这会导致死锁)。
展开完整的 microvm-base.nix 代码
{
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";
};
}步骤 5:microvm-home.nix
microvm-base.nix 又会引入 microvm-home.nix,后者通过 home-manager 完成以下设置:
- 使用我的配置设置 Zsh
- 使用我的配置设置 Emacs
- 在共享目录
~/claude-microvm中设置 Claude Code。
展开完整的 microvm-home.nix 代码
{
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;
};
}步骤 6:goprotobuf.nix
goprotobuf.nix 提供了一系列必需且实用的软件包:
# 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
];
}运行 VM
我们先创建工作区目录和 SSH 主机密钥:
mkdir -p ~/microvm/emacs/ssh-host-keys
ssh-keygen -t ed25519 -N "" \
-f ~/microvm/emacs/ssh-host-keys/ssh_host_ed25519_key
现在可以启动 VM:
sudo systemctl start microvm@emacsvm
几秒钟内它就会启动并响应 ping。
接着通过 SSH 连接到 VM(可以在 tmux(1) 会话中进行),然后在共享工作区目录中运行 Claude(或你选择的 Coding Agent),且不会出现权限提示:
% ssh 192.168.83.2
emacsvm% cd microvm/emacs
emacsvm% claude --dangerously-skip-permissions
在这种设置中运行 Claude 的效果如下:

使用 Claude 创建 VM
完成一次 MicroVM 设置后,再做一遍就会变得很烦琐。
我很好奇 Claude Skills 是否能帮助完成这类任务。Skills 是一些 Markdown 文件,用于指导 Claude 在特定情况下执行特定步骤。
我创建了 .claude/skills/create-microvm/SKILL.md,内容如下:
---
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.使用此 skill 和 Claude Code 时(测试版本:v2.0.76 和 v2.1.15),配合 Opus 4.5 模型,我可以发送这样的提示:
please set up a microvm for Debian Code Search (dcs). see ~/dcs for the source code (but clone from https://github.com/Debian/dcs)
随后 Claude 会忙活几分钟,期间可能会先询问一个澄清问题。之后,Claude 会这样回复:
dcsvm microvm 已成功设置。以下是创建的内容:
配置:
- /home/michael/machines/midna/microvms/dcs.nix——项目专用软件包(Go 工具链、protobuf 工具、uglify-js、yuicompressor、zopfli)
- 已更新 /home/michael/machines/midna/microvm.nix,加入 dcsvm,其中包括:
- IP:192.168.83.8
- tapId:microvm6
- mac:02:00:00:00:00:07
工作区:
- /home/michael/microvm/dcs/——工作区目录
- /home/michael/microvm/dcs/ssh-host-keys/——SSH 主机密钥(ed25519)
- /home/michael/microvm/dcs/dcs/——从 https://github.com/Debian/dcs 克隆的仓库
构建已验证——配置构建成功。
部署后启动 microvm:
systemctl start microvm@dcsvm通过 SSH 连接:
ssh [email protected]
太棒了!以我的经验,Claude 总能正确创建 VM。
使用 Claude Code 复现这一设置
实际上,你还可以更进一步:除了让 Claude 创建新的 MicroVM,也可以让 Claude 将整套设置复现到你的 NixOS 配置中!
可以尝试这样的提示:
read https://michael.stapelberg.ch/posts/2026-02-01-coding-agent-microvm-nix/ — I want the exact same setup in my midna NixOS configuration please!
结语
NixOS 一直以难以上手著称,但一旦开始使用 NixOS,你就能在几分钟内为新项目启动临时 MicroVM,从而完成许多强大的工作。
维护工作也很少:当我更新个人 PC 时,我的 MicroVM 配置也会开始使用新的软件版本。如有需要,自定义也很容易。
这其实与我使用 Coding Agents 的体验相似:我并不觉得它们会自动地让现有任务变得更高效;我觉得它们让原本无法企及的事情成为可能(类似于 Jevons paradox(杰文斯悖论))。
在 2025 年亲眼见证 Coding Agents 质量的提升,既令人着迷(也很可怕!)。2025 年初,我认为 LLM 只是被过度炒作的玩具;当人们把这些模型生成的文本或代码展示给我时,我甚至觉得那近乎侮辱。但几乎每次新的前沿模型发布,性能都有显著提升。到现在为止,Claude Code 的能力和质量已经多次让我惊喜。它生成的代码能够处理一些合理的边界情况,而我自己根本不会想到这些情况。
通过本文,我展示了一种安全运行 Coding Agents 的可行方式(实际上,任何不应访问私人数据的工作负载都可以采用这种方式),你可以根据自己的需求从多个方面进行调整。
随机一篇博客