在 NixOS 上用 microvm.nix 建立 Coding Agent 專用虛擬機
原文由 Michael Stapelberg 于 發布,訂閱此部落格
我漸漸體會到,不論是要理解程式架構、診斷錯誤,還是開發概念驗證,coding agent 在處理程式碼的各種情境下都是相當有價值的工具。不過,依使用情境而定,要逐一審核 agent 想執行的每個指令,很快就會變得繁瑣又耗時。為了能不經審核就安心地執行 coding agent,我想要一套虛擬機(VM)方案:讓 agent 完全接觸不到我的私人檔案,即使被惡意軟體入侵也無關緊要——大不了把整台 VM 丟掉重來就好。
比起建立一台有狀態的 VM,需要時再重灌(光想就累!),我更偏好拋棄式的暫時性 VM:除了明確與主機共享的內容之外,任何東西都不會留在磁碟上。
microvm.nix 專案讓在 NixOS 上建立這類 VM 變得非常簡單,本文會分享我個人設定 VM 的方式。
延伸閱讀
如果你還沒聽過 NixOS,可以參考 NixOS 維基百科頁面和 nixos.org。我在 2025 年分享了為何轉向 Nix,也發表過幾篇關於 Nix 的部落格文章。
想了解 AI agent 的威脅模型,可閱讀 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 月)。本文不會比較各種不同的解決方案,只會展示其中一種可行的做法。
最後,如果你沒興趣自己搭建或維運沙盒基礎設施,好消息是:沙盒現在是熱門話題,已經有許多商業服務應運而生來滿足這項需求。例如,我在 Go 社群認識的 David Crawshaw 和 Josh Bleecher Snyder 最近推出了 exe.dev,這是一個對 agent 友善的 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 模組新增為一個新的 input(詳情請參考microvm.nix 文件),並在我的電腦(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 host keys
~/claude-microvm,這是一個獨立的狀態目錄,僅供 microvm 使用。
- 工作區目錄,例如
- 一個 8 GB 的磁碟覆蓋層(var.img),儲存在
/var/lib/microvms/<name> - 以
cloud-hypervisor(QEMU 也很好用!)作為 hypervisor,配備 8 個 vCPU 和 4 GB 記憶體。 - 針對 systemd 嘗試卸載
/nix/store(會造成死結)的 workaround。
展開完整的 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 host key:
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.在使用 Claude Code(測試版本:v2.0.76 與 v2.1.15)搭配 Opus 4.5 模型使用這個 skill 時,我可以送出像這樣的提示:
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 toolchain、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 host keys(ed25519)
- /home/michael/microvm/dcs/dcs/ - 從 https://github.com/Debian/dcs 複製的儲存庫
已驗證建置——組態建置成功。
部署後啟動 microvm:
systemctl start microvm@dcsvmSSH 連入方式:
ssh [email protected]
太棒了!就我的經驗,Claude 每次都能正確地建立 VM。
用 Claude Code 複製整套設定
其實,你還可以更進一步:不只是請 Claude 建立新的 MicroVM,還可以請它把整套設定完整複製到你的 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 給人的印象是難以入門,但一旦開始使用,你就能做到像這樣強大的事:幾分鐘內為新專案啟動拋棄式的 MicroVM。
維護成本極低:當我更新個人電腦時,我的 MicroVM 設定也會跟著使用新版本的軟體。如果需要客製化,也很容易。
這其實也呼應了我對 Coding Agent 的體驗:我不覺得它們是自動讓既有工作變得更有效率,而是讓原本做不到的事情變得可能(有點類似 Jevons paradox)。
在 2025 年間親身經歷 Coding Agent 品質的提升,既令人著迷又有點嚇人。2025 年初,我還覺得 LLM 是被過度吹捧的玩具,甚至覺得別人拿這些模型產生的文字或程式碼給我看有點冒犯。但幾乎每一次前沿模型的更新都有顯著進步,到現在,Claude Code 的能力和品質已經多次讓我驚艷。它產出的程式碼甚至能處理到我自己都不會想到的合理邊界情況。
透過本文,我展示了一種安全執行 Coding Agent 的可行方式(其實任何不該存取你私人資料的工作負載都適用),你可以依需求做各種調整。
隨機一篇部落格
留言
登入後參與討論