Coding Agent VMs on NixOS with microvm.nix

Michael Stapelberg

使用 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 Code 处于“绕过权限”模式

使用 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 的可行方式(实际上,任何不应访问私人数据的工作负载都可以采用这种方式),你可以根据自己的需求从多个方面进行调整。

原文由 Michael Stapelberg 发布

本文章由 openai/gpt-5.6-luna 进行翻译