Coding Agent VMs on NixOS with microvm.nix

Michael Stapelberg

microvm.nixでNixOS上にコーディングエージェント用VMを構築する

原文は Michael Stapelberg により に公開されました。 このブログを購読する

コーディングエージェントは、プログラムのアーキテクチャを学ぶときも、バグを診断するときも、概念実証を作るときも、あらゆる場面でコードを扱うための貴重なツールだと感じるようになりました。使い方によっては、エージェントが実行しようとするコマンドを一つひとつ確認するのは、すぐに面倒で時間のかかる作業になります。確認なしで安全にコーディングエージェントを動かすために、エージェントが個人ファイルにアクセスできず、万が一マルウェアに感染してもVMを捨ててやり直せば済むような仮想マシン(VM)ソリューションが欲しいと考えました。

状態を持つVMをセットアップして必要になるたびに再インストールする(うんざりしますよね)代わりに、ホストと明示的に共有したもの以外は何もディスクに残らない、エフェメラルなVMというモデルを好んでいます。

microvm.nixプロジェクトを使えばNixOS上でそのようなVMを簡単に作成でき、この記事では私なりのVMのセットアップ方法を紹介します。

関連情報

NixOSをまだご存じない方は、Wikipediaの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月)をご覧ください。本記事ではさまざまなソリューションを比較するのではなく、考えられる一つの方法を紹介するだけにとどめます。

そして最後に、サンドボックスのインフラを自分で構築・運用する気分ではないという方もいるでしょう。朗報です。サンドボックスは今ホットなトピックで、このニーズに応える商用サービスが次々と登場しています。例えば、David Crawshaw氏とJosh Bleecher Snyder氏(どちらもGoコミュニティで知り合いました)が最近立ち上げたエージェントフレンドリーなVMホスティングサービスexe.devがあります。また、Fly.ioが立ち上げたSpritesもその一例です。

microvm.nixのセットアップ

早速始めましょう。次のセクションでは、私の設定方法を順を追って説明します。

ステップ1: ネットワークの準備

まず、IPアドレス範囲に192.168.33.1/24を使い、eno1ネットワークインターフェースからNATで外部に出る新しいブリッジmicrobrを作成しました。すべての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

次に、microvmモジュールをflake.nixの新しいinputとして追加し(詳細はmicrovm.nixのドキュメントをご覧ください)、PC(midna)用のNixOS設定でmicrovm.nixosModules.hostモジュールを有効にしました。また、すべてのVMを宣言するための新しい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では2つのmicroVMを宣言しています。1つは(もっと詳しく知りたいと思っていた)Emacs用、もう1つはよく知っていてClaudeの能力を測るのに使えるGo Protobufのコードベース用です。

{
  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ストア(VMがキャッシュからソフトウェアにアクセスできるようにするため)
    • このVMのSSHホストキー
    • ~/claude-microvm(microVM専用の独立した状態ディレクトリ)
  • /var/lib/microvms/<name>に保存される8GBのディスクオーバーレイ(var.img)
  • ハイパーバイザーとしてcloud-hypervisor(QEMUでも問題なく動きます!)、8 vCPUと4GB RAM
  • 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に応答するようになります。

次に、(できればtmux(1)セッション内で)VMにSSHで接続し、共有ワークスペースディレクトリでClaude(あるいはお好みのコーディングエージェント)を権限確認なしで実行します。

% ssh 192.168.83.2
emacsvm% cd microvm/emacs
emacsvm% claude --dangerously-skip-permissions

このセットアップでClaudeを実行すると、次のようになります。

権限バイパスモードのClaude Code

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.

このSkillをClaude Code(テストしたバージョン: v2.0.76およびv2.1.15)でOpus 4.5モデルと組み合わせて使うと、次のようなプロンプトを送ることができます。

Debian Code Search (dcs) 用のmicroVMをセットアップしてください。ソースコードについては ~/dcs を参照してください(ただし https://github.com/Debian/dcs からクローンしてください)

しばらくすると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の作成を頼むだけでなく、このセットアップ全体をあなたのNixOS設定に再現させることもできるのです。

次のようなプロンプトを試してみてください。

https://michael.stapelberg.ch/posts/2026-02-01-coding-agent-microvm-nix/ を読んでください — 私のmidnaのNixOS設定でまったく同じセットアップを再現してください!

まとめ

NixOSは導入が難しいという評判がありますが、一度使い始めれば、新しいプロジェクトのためにエフェメラルなMicroVMを数分で立ち上げるといった強力なことができるようになります。

メンテナンスの手間は最小限です。個人用PCをアップデートすれば、MicroVMの設定も新しいソフトウェアバージョンを使うようになります。必要に応じてカスタマイズも簡単です。

これは私のコーディングエージェントに関する経験とも重なります。既存のタスクが自動的に効率化されるとは感じていません。むしろ、これまで手が届かなかったことが可能になると感じています(ジェボンズのパラドックスに似ています)。

2025年を通じてコーディングエージェントの品質が向上していくのを目の当たりにするのは、魅力的で(そして恐ろしくも)ありました。2025年の初めには、LLMは過大評価されたおもちゃだと思っていましたし、人々がこれらのモデルが生成したテキストやコードを見せてくるのはほとんど侮辱のようにさえ感じていました。しかし、新しいフロンティアモデルがリリースされるたびにほぼ毎回大きく改善され、今ではClaude Codeの能力と品質に何度も良い意味で驚かされています。自分では思いつかなかった正当なエッジケースを処理するコードを生成してくれたこともあります。

本記事では、コーディングエージェントを安全に実行するための(というか、本当はプライベートデータにアクセスさせたくないあらゆるワークロードのための)一つの方法を紹介しました。ニーズに合わせてさまざまな形で調整してみてください。

この記事は「muse-spark-1.2-contributor」を使用して翻訳されました。

コメント