在 NixOS 上使用 microvm.nix 搭建编程智能体虚拟机
原文由 Michael Stapelberg 于 发布,订阅该博客
我逐渐认识到,编程智能体在处理代码的各类任务中都是很有价值的工具,无论是了解某个程序的架构、排查缺陷,还是开发概念验证。但具体到实际使用场景,逐条审核智能体想要执行的每一条命令,很快就会变得繁琐又耗时。为了无需审核也能安全地运行编程智能体,我想要一种虚拟机(VM)方案:智能体无法访问我的个人文件,即便被恶意软件感染也无伤大雅——大不了直接丢弃虚拟机,重新开始。
与其搭建一个有状态的虚拟机、需要时再重装(太麻烦了!),我更偏好临时虚拟机的模式:除了与宿主机显式共享的内容外,磁盘上不会留下任何持久化数据。
microvm.nix 项目让在 NixOS 上创建这类虚拟机变得十分简单,本文将介绍我搭建虚拟机的方式。
另请参阅
如果你之前从未听说过 NixOS,可以查看NixOS 维基百科页面和nixos.org。我谈过我为何在 2025 年转向 Nix,也发布过几篇关于 Nix 的博文。
要了解 AI 智能体的威胁模型,请阅读Simon Willison 的《The lethal trifecta for AI agents: private data, untrusted content, and external communication》(2025 年 6 月)。本文应对该威胁模型的思路,是将其中“私有数据”这一项从等式中移除。
如果想全面了解沙箱领域,可以看看Luis Cardoso 的《A field guide to sandboxes for AI》(2026 年 1 月)。本文不会对比不同的方案,只会展示其中一条可行的路径。
最后,如果你暂时不想自己搭建或运行沙箱基础设施,好消息是:沙箱如今是个热门话题,市面上已经涌现出许多满足这一需求的商业化产品。例如,我在 Go 社区就认识的 David Crawshaw 和 Josh Bleecher Snyder 最近推出了exe.dev,这是一项面向智能体的虚拟机托管服务。另一个例子是推出了 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 文档),并在我的电脑(midna)的 NixOS 配置中启用了 microvm.nixosModules.host 模块。我还新建了一个 microvm.nix 文件,用于声明所有的虚拟机。我的 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(我想借此多了解一些 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,让虚拟机可以(经常)从缓存访问软件
- 该虚拟机的 SSH 主机密钥
~/claude-microvm,这是一个独立的状态目录,仅在 microvm 中使用。
- 工作区目录,例如
- 一个 8 GB 的磁盘叠加层(var.img),存放在
/var/lib/microvms/<name>中 - 以
cloud-hypervisor(QEMU 也很好用!)作为虚拟化层,配置 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
];
}运行虚拟机
接下来创建工作区目录并生成 SSH 主机密钥:
mkdir -p ~/microvm/emacs/ssh-host-keys
ssh-keygen -t ed25519 -N "" \
-f ~/microvm/emacs/ssh-host-keys/ssh_host_ed25519_key
现在就可以启动虚拟机了:
sudo systemctl start microvm@emacsvm
几秒钟内它就能完成启动并响应 ping。
然后通过 SSH 连接到虚拟机(可以放在 tmux(1) 会话中),并在共享的工作区目录下以免确认模式运行 Claude(或你选用的任意编程智能体):
% ssh 192.168.83.2
emacsvm% cd microvm/emacs
emacsvm% claude --dangerously-skip-permissions
这就是在这样的环境中运行 Claude 的效果:

用 Claude 创建虚拟机
完整走过一次搭建 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.配合 Claude Code 使用这个 skill 时(测试版本:v2.0.76 和 v2.1.15),搭配 Opus 4.5 模型,我可以发送这样的提示:
请为 Debian Code Search(dcs)搭建一个 microvm。源代码见 ~/dcs(但请从 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@dcsvmSSH 连接:
ssh [email protected]
太棒了!就我的经验而言,Claude 总能正确完成虚拟机的创建。
用 Claude Code 复现这套配置
事实上,你还可以更进一步:不仅仅是让 Claude 创建新的 MicroVM,还可以让它把整套配置复现到你自己的 NixOS 配置中!
可以试试这样的提示:
请阅读 https://michael.stapelberg.ch/posts/2026-02-01-coding-agent-microvm-nix/ —— 我想在我的 midna NixOS 配置中复现完全相同的配置!
结语
NixOS 一向被认为上手门槛较高,但一旦用上它,你就能轻松做到一些强大的事情,比如在几分钟内为新项目启动一个临时 MicroVM。
维护成本也很低:当我更新个人电脑时,我的 MicroVM 配置也会自动用上新版本的软件。如有需要,定制也很容易。
这其实也映照出我对编程智能体的感受:我并不觉得它们会自动让现有任务变得更高效,而是觉得它们让一些原本做不到的事情变得可行(有点类似于杰文斯悖论)。
在 2025 年亲历编程智能体质量的飞跃,既令人着迷,又有些令人不安。2025 年初,我还觉得大语言模型是个被过度吹捧的玩具,甚至当别人向我展示这些模型生成的文本或代码时,会感到近乎被冒犯。但此后几乎每一次前沿模型的发布都有显著提升,到现在,Claude Code 的能力和质量已经多次让我感到惊喜。它生成的代码甚至能处理一些我自己都未曾考虑到的合理边界情况。
通过本文,我展示了一种安全运行编程智能体(或者说,任何不应访问你私有数据的工作负载)的方法,你可以根据自身需求以多种方式对其进行调整。
随机一篇博客
评论
登录后参与讨论