This demo shows that the wrapper script terrax can dynamically mount different directories for the microVM at runtime.

TL;DR

Use microVM.nix for extreme isolation for running AI agents.

For the impatient:

  1. Get the repo
git clone https://github.com/kohane27/agents-in-microvm-nix
cd agents-in-microvm-nix
  1. Prepare the ssh key
mkdir -p ~/.ssh/microvm
ssh-keygen -t ed25519 -f ~/.ssh/microvm/ssh_host_ed25519_key -N "" -C "microvm-host-key"
  1. Prepare the user key
ssh-keygen -t ed25519 -f ~/.ssh/microvm-user -N "" -C "microvm-user"
  1. Replace username with your_username
cd agents-in-microvm-nix
find . -type f -name "*.nix" -not -path "./terrax/home-manager.nix" -not -path "./terrax/user.nix" -exec sed -i 's/username/your_username/g' {} +
  1. Add the public key to user.nix
PUB_KEY=$(cat $HOME/.ssh/microvm-user.pub)
sed -i "s|.*ssh-ed25519.*| \"$PUB_KEY\"|" ./terrax/user.nix
  1. Add the following inputs to your host flake.nix:
{
    microvm = {
      url = "github:microvm-nix/microvm.nix";
      inputs.nixpkgs.follows = "nixpkgs";
    };

    home-manager = {
      url = "github:nix-community/home-manager";
      inputs.nixpkgs.follows = "nixpkgs";
    };

    sops = {
      url = "github:Mic92/sops-nix";
      inputs.nixpkgs.follows = "nixpkgs";
    };

    # more up-to-date LLM packages
    llm-agents = {
      url = "github:numtide/llm-agents.nix";
      inputs.nixpkgs.follows = "nixpkgs";
    };
}
  1. Copy agents-in-microvm-nix content to your NixOS dotfiles

  2. sudo nixos-rebuild switch

  3. sudo systemctl start microvm@terrax

10 . ssh -i ~/.ssh/microvm-user [email protected]

11 . opencode and test with models that don’t require an API key

Introduction

In my previous article Sandboxing AI agents with jail.nix, I shared using jail.nix to sandbox your AI agent environment. Now we take it one step further by creating an even bigger jail for our AI agents with microVM.nix.

Features/Changes

  1. Break down Michael Stapelberg’s config into more modular
  2. Remove his personal stapelbergnix and configfiles so that it’s more accessible and applicable
  3. Add launcher.nix that improves the ergonomic of using the microVM, i.e., run terrax on your host machine and “it just works™”
  4. Passing secrets into the microVM with sops-nix
  5. Dynamically mount and unmount directories rather than hardcoding the directories in the config and then rebuild your NixOS for it to take effect

As you can see below, the home directory is extremely minimal:

home-dir

There is no ~/.mozilla or ~/.config/google-chrome with browser cookies that could be stolen. .claude.json and .claude exist because we explicitly added them to the microVM. Running env will not show any sensitive information or other API keys except the one we shared.

Assumptions/Prerequisites

  1. You’re using flakes for your NixOS system
  2. The setup assumes the user is using Claude Code with ANTHROPIC_API_KEY or OpenCode with OPENROUTER_API_KEY (but the config is easy to be adopted to other agents like Gemini)
  3. This setup requires you to adopt it to your config to make it work. There are changes required, which I list below.

Getting Started

The repo accompanying this article is at agents-in-microvm-nix. Please clone it if you’d like to follow along.

Note
microVM’s username is microvm and hostname terrax.

1. Host Setup

Add the following inputs to flake.nix:

{
    microvm = {
      url = "github:microvm-nix/microvm.nix";
      inputs.nixpkgs.follows = "nixpkgs";
    };

    home-manager = {
      url = "github:nix-community/home-manager";
      inputs.nixpkgs.follows = "nixpkgs";
    };

    sops = {
      url = "github:Mic92/sops-nix";
      inputs.nixpkgs.follows = "nixpkgs";
    };

    # more up-to-date LLM packages
    llm-agents = {
      url = "github:numtide/llm-agents.nix";
      inputs.nixpkgs.follows = "nixpkgs";
    };
}

default.nix

{ ... }:
{
  imports = [
    ./network.nix
    # ./secrets.nix
    ./launcher.nix
    # ./doas.nix
    ./microvm.nix
    ./claude.nix
  ];
}

The top-level default.nix is our entry point: network.nix, secrets.nix, doas.nix and launcher.nix are for your host. For example, network.nix sets up a network bridge on your host for the microVM; secrets.nix uses sops-nix to pass secrets from your host to the microVM.

Note
secrets.nix is commented out to help you get started easier without having to set up sops-nix if you haven’t. After successful ssh microvm, you can come back to it.

The bounderies are clear: the above files are prepping your host for the microVM. Files under terrax/, on the other hand, set up the microVM.

microvm.nix

It’s the entry point for setting up our microVM.

network.nix

One noteworthy config is systemd.network.wait-online.enable = false;. I’m using NetworkManager to manage the Wi-Fi on my laptop, so the systemd-networkd only handles the microVM bridge, not the external connection. Otherwise, host’s nixos-rebuild switch would time out waiting for the connection.

secrets.nix

It mounts the API secrets to microVM:

  1. systemd.services.terrax-prepare-secrets is using systemd service to copy the two API keys to /var/lib/microvms/terrax/secrets/.

  2. In terrax/microvm.nix, we have the following:

{
  proto = "virtiofs";
  tag = "terrax-secrets";
  source = "/var/lib/microvms/terrax/secrets";
  mountPoint = "/run/secrets";
}

Such that inside the microVM, the secret is readable at /run/secrets/openrouter_api_key etc.

  1. In terrax/zsh.nix, we have the following such that echo $OPENROUTER_API_KEY and echo $ANTHROPIC_API_KEY work inside the VM and can be used by the respective agents:
{
  programs.zsh = {
    initContent = ''
      if [ -f /run/secrets/anthropic_api_key ]; then
        export ANTHROPIC_API_KEY=$(cat /run/secrets/anthropic_api_key)
      fi
      if [ -f /run/secrets/openrouter_api_key ]; then
        export OPENROUTER_API_KEY=$(cat /run/secrets/openrouter_api_key)
      fi
    '';
  };
}

For further info on sops-nix, I recommend reading Managing Secrets in NixOS Home Manager with SOPS, which helped me a lot when I was setting it up. Also, special thanks to Using microvm.nix to sandbox Openclaw for the systemd service idea.

terrax/microvm.nix and launcher.nix

Note
  1. Don’t forget to replace the HOST_WORKSPACE_ROOT’s username with your host machine’s username.
  2. Change exec claude --dangerously-skip-permissions to exec opencode if you’re using OpenCode.

They are responsible for dynamically mounting different project directories at runtime.

  1. launcher.nix mounts the project dir to /home/username/.local/share/microvms/terrax/workspace
  2. terrax/microvm.nix mount the same /home/username/.local/share/microvms/terrax/workspace to /tmp/workspace

The result is that inside the microVM, /tmp/workspace always refers to the project directory, giving it the illusion of “dynamically” changing directory at runtime.

They improve the ergonomic of using the microVM in two ways:

  1. Without it, we need to run the following every time:
sudo systemctl restart microvm@terrax
# wait until the microvm is ready
ssh microvm
cd /tmp/workspace/
claude --dangerously-skip-permissions

The launcher automates the above steps and added some niceties for robustness and cleanup.

  1. It swaps which directory is mounted at the same fixed workspace path before starting the microVM, such that it can access different directories without having to configuration.

The alternative would be the following if you want to use the microVM on, say, $HOME/repo/rustlings:

I. Add the following to terrax/microvm.nix

{ ... }:
{
  microvm = {
    shares = [
      {
        proto = "virtiofs";
        tag = "ro-store";
        source = "/home/username/repo/rustlings";
        mountPoint = "/home/microvm/repo/rustlings";
      }
    ];
  };
}

II. Run sudo nixos-rebuild switch and wait for your system to rebuild

III. ssh microvm

IV. cd /home/microvm/repo/rustlings

V. opencode or claude --dangerously-skip-permissions

That would get tedious real fast.

doas.nix

It’s also just for convenience. When we run terrax, it won’t ask for your sudo password. I have commented it out by default if you don’t want this behavior.

Note
  1. Make sure you have doas on your host machine environment.systemPackages.
  2. Don’t forget to replace the above username with your host machine’s username.

2. microVM setup

terrax/default.nix

It is our entry point for the microVM. In the imports you can see the following imports:

  imports = [
    ./locale.nix
  ];

On my personal config the import is ../../../../modules/nixos/hardware/locale.nix. This shows you the power of NixOS: it references my host’s locale.nix. There is no duplicate code for setting up my host and microVM’s locale.

I’m also using numtide/llm-agents.nix for more up-to-date packages.

terrax/locale.nix

Add your time.timeZone = lib.mkDefault ""; and the correct locale.

terrax/home-manager.nix

In my personal dotfile I have the following:

{
  home-manager = {
    users.microvm = {
      imports = [
        ../../../../modules/home-manager/cli/starship
        ../../../../modules/home-manager/cli/yazi
      ];
    };
  };
}

Again, it references my host’s starship and yazi such that it’s nicer to work with when I need to ssh microvm.

terrax/microvm.nix, terrax/sshd.nix, terrax/user.nix

They are responsible for authentication such that ssh microvm just works™.

  1. On your host machine, generate a new SSH key pair:
mkdir -p ~/.ssh/microvm
ssh-keygen -t ed25519 -f ~/.ssh/microvm/ssh_host_ed25519_key -N "" -C "microvm-host-key"
  1. In terrax/microvm.nix, the following mounts your host’s /home/username/.ssh/microvm to the microVM’s /etc/ssh/host-keys:
{
  proto = "virtiofs";
  tag = "ssh-keys";
  source = "/home/username/.ssh/microvm";
  mountPoint = "/etc/ssh/host-keys";
}
Note
Don’t forget to replace the above username with your host machine’s username.
  1. In terrax/sshd.nix it uses the hostKey.path /etc/ssh/host-keys/ssh_host_ed25519_key for authentication.

  2. In terrax/user.nix, create a new user that is authenticated to SSH into the microVM:

ssh-keygen -t ed25519 -f ~/.ssh/microvm-user -N "" -C "microvm-user"

Take the generated ~/.ssh/microvm-user.pub key and add it to users.users.microvm.openssh.authorizedKeys.keys.

On my host machine ~/.ssh/config I have the following:

Host microvm
  HostName 192.168.83.6
  User       microvm
  IdentityFile ~/.ssh/microvm-user

The end goal is that we can just use ssh microvm instead of ssh -i ~/.ssh/microvm-user [email protected] for passwordless authentication and changing the SSH keypair.

The moment of truth

  1. sudo nixos-rebuild switch

  2. Start the microVM with sudo systemctl start microvm@terrax.

  3. Check its status with sudo systemctl status microvm@terrax:

➜ sudo systemctl status microvm@terrax
● [email protected] - MicroVM 'terrax'
     Loaded: loaded (/etc/systemd/system/[email protected]; static)
    Drop-In: /nix/store/cnh9a7zw3yrnw6k1wizlna5i373q0ixq-system-units/[email protected]
             └─overrides.conf
     Active: active (running) since Wed 2026-02-18 20:04:17 GMT; 46s ago
 Invocation: 9fbf076cec184498b1968639b8a55be0
    Process: 996229 ExecStartPre=/nix/store/riknx3la3y8gs633j6hrj6ih7bryng72-unit-script-microvm_-pre-start/bin/microvm_-pre-start (code=exited, status=0/SUCC>
   Main PID: 996234 (cloud-hyperviso)
         IP: 0B in, 0B out
         IO: 8K read, 6.6M written
      Tasks: 44 (limit: 37613)
     Memory: 721.7M (peak: 723M)
        CPU: 12.887s
     CGroup: /system.slice/system-microvm.slice/[email protected]
             └─996234 microvm@terrax --cpus boot=8 --watchdog --kernel /nix/store/dslmcyd0p4aq5hnvf7dnrr89jb8h90y1-linux-6.18.10-dev/vmlinux --initramfs /nix/>

Feb 18 20:04:25 microvm-user microvm@terrax[996234]: [  OK  ] Started Serial Getty on hvc0.
Feb 18 20:04:25 microvm-user microvm@terrax[996234]: [  OK  ] Started Serial Getty on ttyS0.
Feb 18 20:04:25 microvm-user microvm@terrax[996234]: [  OK  ] Reached target Login Prompts.
Feb 18 20:04:25 microvm-user microvm@terrax[996234]: [  OK  ] Reached target Multi-User System.
Feb 18 20:04:27 microvm-user microvm@terrax[996234]: [92B blob data]
Feb 18 20:04:27 microvm-user microvm@terrax[996234]:
Feb 18 20:04:27 microvm-user microvm@terrax[996234]: <<< Welcome to NixOS 26.05pre-git (x86_64) - ttyS0 >>>
Feb 18 20:04:27 microvm-user microvm@terrax[996234]:
Feb 18 20:04:27 microvm-user microvm@terrax[996234]: Run 'nixos-help' for the NixOS manual.

<<< Welcome to NixOS 26.05pre-git (x86_64) - ttyS0 >>> mean it’s booted up and running!

  1. ssh -i ~/.ssh/microvm-user [email protected] (or ssh microvm if you have set up ~/.ssh/config)

  2. Run the following commands to see if the secrets are set up:

echo $ANTHROPIC_API_KEY
echo $OPENROUTER_API_KEY
  1. On your host machine, change to some project directory and run terrax. You should be dropped to the microVM with claude already running!

If there are any errors, remember to first sudo nixos-rebuild switch and then sudo systemctl restart microvm@terrax before any changes are taken effect.

Should you use jail.nix or microVM.nix?

It comes down to your threat level tolerance and convenience needs. microVM.nix offers high isolation (Guest Kernel) but it’s also much more complicated to set up (relative to jail.nix anyway). Also, as you can see from the demo, every time we run terrax, it restarts the microVM and it takes around 18 seconds before we can use it.

Personally, I use microVM.nix when I run any new and unfamilar projects and jail.nix for familiar ones. If you think using a microVM is too resource intensive, or too complex to setup, or startup time too long, I recommend using Sandboxing AI agents with jail.nix.

Conclusion

If you encounter any problems feel free to open an issue on the repo https://github.com/kohane27/agents-in-microvm-nix.

Special thanks to Coding Agent VMs on NixOS with microvm.nix for the initial configuration setup and inspiration!

Thank you for reading!