在 NixOS 上使用 microvm.nix 建立 Coding Agent 虛擬機
我逐漸體會到 coding agents(程式碼代理)是在各種情境下處理程式碼的寶貴工具,例如了解任何程式的架構、診斷錯誤或開發概念驗證。取決於使用情境,逐一審查代理想要執行的每個指令,很快就會變得繁瑣且耗時。為了在無需審查的情況下安全地執行 coding agent,我想要一種虛擬機器(VM)解決方案,讓代理無法存取我的個人檔案,即使代理遭到惡意軟體入侵也無關緊要:我只要丟棄該 VM 並重新開始即可。
比起建立一個有狀態的 VM 並在需要時重新安裝(唉!),我更偏好 ephemeral VMs(暫時性虛擬機)的模式,除了明確與主機共享的內容外,不會有任何資料留存在磁碟上。
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 月)。本文處理 threat model 的做法,是將其中的「private data」部分從方程式中移除。
若想了解 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 模組新增為新的 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 主機金鑰
~/claude-microvm,這是一個獨立的狀態目錄,僅用於 microvm。
- 工作區目錄,例如
- 一個 8 GB 的磁碟覆蓋層(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 是指示 Claude 在特定情況下執行特定步驟的 Markdown 檔案。
我建立了 .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)中使用此 skill,並搭配 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@dcsvm透過 SSH 連入:
ssh [email protected]
太棒了!以我的經驗,Claude 每次都能正確建立 VM。
使用 Claude Code 複製此設定
事實上,你還可以更進一步:不只是請 Claude 建立新的 MicroVM,還可以請 Claude 將整個設定複製到你的 NixOS 設定中!
試試像這樣的提示:
請閱讀 https://michael.stapelberg.ch/posts/2026-02-01-coding-agent-microvm-nix/ — 我想在我的 midna NixOS 設定中建立完全相同的設定!
結論
NixOS 給人的印象是難以入門,但一旦開始使用 NixOS,就能在幾分鐘內為新專案快速啟動 ephemeral MicroVM 等強大的操作。
維護成本極低:當我更新個人電腦時,我的 MicroVM 設定也會開始使用新的軟體版本。如有需要,自訂也很容易。
這其實也反映了我對 Coding Agents 的體驗:我不覺得它們是自動讓既有任務更有效率,而是讓原本遙不可及的事情變得可能(類似 Jevons paradox(傑文斯悖論))。
在 2025 年間體驗 Coding Agents 品質的提升,既令人著迷(又有點可怕!)。2025 年初,我認為 LLM 是被過度吹捧的玩具,甚至覺得別人向我展示這些模型產生的文字或程式碼時,幾乎是一種冒犯。但幾乎每一次新的前沿模型發布,表現都顯著進步,到現在 Claude Code 的能力與品質已多次讓我驚艷。它產生的程式碼能處理許多我未曾考慮到的正當邊界情況。
透過本文,我展示了一種安全執行 Coding Agents(或實際上任何不應存取你私人資料的工作負載)的方式,你可以依需求以多種方式進行調整。
隨機一篇部落格