Coding Agent VMs on NixOS with microvm.nix

Michael Stapelberg

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

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

状態を持ったVMを用意して必要になるたびに入れ直す(うんざりしますよね)のではなく、ホストと明示的に共有したもの以外は何もディスクに残らない、エフェメラルなVMというモデルを好んでいます。

NixOSでそうしたVMを簡単に作れるのがmicrovm.nixプロジェクトです。この記事では、私なりのVMのセットアップ方法をご紹介します。

関連情報

NixOSをまだご存じない方は、NixOSのWikipediaページ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氏が最近立ち上げたエージェントフレンドリーなVMホスティングサービスのexe.devや、Spritesを立ち上げたFly.ioなどがあります。

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

次に、flake.nixmicrovmモジュールを新しい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専用の独立した状態保存用ディレクトリです。
  • 8GBのディスクオーバーレイ(var.img)。/var/lib/microvms/<name>に保存されます。
  • ハイパーバイザーとしてcloud-hypervisor(QEMUでも問題なく動きます)。vCPUは8コア、メモリは4GBです。
  • /nix/storeをアンマウントしようとするsystemdの挙動に対するワークアラウンド(デッドロックを引き起こします)。
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にも応答するようになります。

次に、VMにSSHで接続し(tmux(1)セッション内がおすすめです)、共有ワークスペースディレクトリで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モデルと組み合わせて使うと、次のようなプロンプトを送ることができます。

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が数分間作業し、場合によっては確認の質問をしてきます。その後、次のような報告が返ってきます。

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で再現する

実は、もう一歩進むこともできます。新しい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の能力と品質に何度も良い意味で驚かされています。自分では思いつかなかったような正当なエッジケースまで処理するコードを生成してくれたこともあります。

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

原文は Michael Stapelberg により に公開されました。

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