microvm.nix를 사용해 NixOS에서 개인 파일에 접근하지 못하는 일회성(에페메럴) MicroVM을 구성하고, Claude 같은 코딩 에이전트를 안전하게 실행하는 방법을 단계별 설정 예시와 함께 소개합니다.
Michael Stapelberg ==================
Michael Stapelberg ==================
게시일 2026-02-01
목차
저는 코딩 에이전트가 어떤 형태로든 컴퓨터 프로그램 코드 작업을 할 때(예: 프로그램 아키텍처를 학습하거나, 버그를 진단하거나, 개념 증명을 개발할 때) 유용한 도구라는 점을 점점 더 실감하게 되었습니다. 사용 사례에 따라, 에이전트가 실행하려는 각 명령을 매번 검토하는 것은 금방 지루하고 많은 시간을 잡아먹게 됩니다. 검토 없이 코딩 에이전트를 안전하게 실행하기 위해, 에이전트가 제 개인 파일에 접근할 수 없고, 에이전트가 악성코드에 의해 손상되더라도 큰 문제가 되지 않는(그냥 VM을 버리고 다시 시작하면 되는) 가상 머신(VM) 솔루션을 원했습니다.
필요할 때마다 상태를 가진(stateful) 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월)을 읽어보세요. 이 글에서의 위협 모델 대응 방식은 그중 “private data(개인 데이터)” 요소를 방정식에서 제거하는 것입니다.
샌드박싱 분야 전체를 공부하고 싶다면 Luis Cardoso의 “A field guide to sandboxes for AI” (2026년 1월)를 참고하세요. 이 글에서는 다양한 솔루션을 비교하지 않고, 가능한 한 가지 경로만 보여드리겠습니다.
마지막으로, 직접 샌드박싱 인프라를 만들고/운영할 기분이 아닐 수도 있습니다. 좋은 소식은 샌드박싱이 뜨거운 주제이고 이를 해결하는 상용 서비스가 많이 나오고 있다는 점입니다. 예를 들어 David Crawshaw와 Josh Bleecher Snyder(둘 다 Go 커뮤니티에서 알고 지냅니다)는 최근 에이전트 친화적인 VM 호스팅 서비스인 exe.dev를 출시했습니다. 또 다른 예로는 Fly.io가 Sprites를 출시했습니다.
바로 시작해봅시다! 다음 섹션에서는 제가 설정을 구성한 방식을 단계별로 안내합니다.
먼저 192.168.33.1/24 IP 주소 범위를 사용하고 eno1 네트워크 인터페이스를 통해 NAT로 외부로 나가게 하는 새로운 microbr 브리지를 만들었습니다. 모든 microvm* 인터페이스는 이 브리지에 추가됩니다:
nixsystemd.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"; };
flake.nix그 다음 microvm 모듈을 flake.nix의 새 입력으로 추가했고(자세한 내용은 microvm.nix 문서 참고), 제 PC(midna)의 NixOS 설정에서 microvm.nixosModules.host 모듈을 활성화했습니다. 또한 모든 VM을 선언하는 새로운 microvm.nix 파일을 만들었습니다. 제 flake.nix는 다음과 같습니다:
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 ]; }; }; }; }
microvm.nix다음 microvm.nix는 두 개의 microvm을 선언합니다. 하나는 Emacs(더 알아보고 싶었던 대상) 용이고, 다른 하나는 제가 익숙해서 Claude의 역량을 이해하는 데 쓸 수 있는 Go Protobuf 코드베이스 용입니다:
nix{ 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 ]; }; }; }
microvm-base.nixmicrovm-base.nix 모듈은 위의 파라미터를 받아 다음을 선언합니다:
네트워크 설정: 저는 systemd-networkd(8)와 systemd-resolved(8)를 쓰는 것을 좋아합니다.
다음을 위한 공유 디렉터리:
~/microvm/emacs)~/claude-microvm — microvm에서만 사용하는 별도의 상태 디렉터리8GB 디스크 오버레이(var.img), /var/lib/microvms/<name>에 저장
하이퍼바이저로 cloud-hypervisor(QEMU도 잘 동작합니다!), vCPU 8개와 RAM 4GB
systemd가 /nix/store를 언마운트하려다(데드락 유발) 생기는 문제를 회피하는 workaround
microvm-base.nix 전체 코드 펼치기
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"; }; }
microvm-home.nixmicrovm-base.nix는 다시 microvm-home.nix를 포함하는데, 이 파일은 home-manager를 통해 다음을 설정합니다:
~/claude-microvm에 Claude Code 설정microvm-home.nix 전체 코드 펼치기
nix{ config, pkgs, lib, configfiles, stapelbergnix, ... }: { options.microvm = { extraZshInit = lib.mkOption { type = lib.types.lines; default = ""; description = "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; }; }
goprotobuf.nixgoprotobuf.nix는 필요한/편리한 패키지들을 제공하게 합니다:
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 ]; }
워크스페이스 디렉터리를 만들고 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를 실행하면 이렇게 보입니다:
MicroVM을 한 번 설정해 보고 나면, 그 과정을 반복하는 것이 번거로워집니다.
저는 Claude Skills가 이런 작업에 도움이 될지 궁금했습니다. Skills는 특정 상황에서 Claude에게 어떤 단계를 수행해야 하는지 지시하는 마크다운 파일입니다.
저는 .claude/skills/create-microvm/SKILL.md를 다음과 같이 만들었습니다:
markdown--- 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)에서 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 toolchain, protobuf tools, 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@dcsvmSSH 접속:
ssh michael@192.168.83.8
훌륭합니다! 제 경험상 Claude는 항상 VM 생성 작업을 정확히 수행했습니다.
사실, 한 단계 더 나아갈 수도 있습니다. Claude에게 새로운 MicroVM을 만들도록 요청하는 것뿐 아니라, 이 전체 설정을 여러분의 NixOS 구성에 그대로 복제해 달라고 요청할 수도 있습니다!
예를 들어 다음과 같은 프롬프트를 시도해 보세요:
https://michael.stapelberg.ch/posts/2026-02-01-coding-agent-microvm-nix/ 를 읽어주세요 — 제 midna NixOS 구성에 정확히 동일한 설정을 적용해 주세요!
NixOS는 도입하기 어렵다는 평판이 있지만, 일단 NixOS를 사용하고 나면 새로운 프로젝트를 위해 몇 분 만에 일회성 MicroVM을 띄우는 것 같은 강력한 일을 할 수 있습니다.
유지보수 부담은 최소입니다. 개인 PC를 업데이트하면 MicroVM 구성도 새 소프트웨어 버전을 같이 사용하게 됩니다. 필요하다면 커스터마이징도 쉽습니다.
이 경험은 코딩 에이전트에 대한 제 경험과도 닮아 있습니다. 코딩 에이전트가 기존 작업을 자동으로 효율적으로 만들어 준다기보다는, 이전에는 손이 닿지 않던 일을 가능하게 해 준다고 느낍니다( 제본스의 역설과 비슷하게요).
2025년 동안 코딩 에이전트의 품질이 향상되는 과정을 직접 경험하는 것은 흥미로우면서도(그리고 무섭기도!) 했습니다. 2025년 초만 해도 저는 LLM이 과장된 장난감이라고 생각했고, 사람들이 이런 모델이 생성한 텍스트나 코드를 보여주면 거의 모욕적으로 느끼기까지 했습니다. 하지만 새로운 프론티어 모델이 나올 때마다 거의 매번 눈에 띄게 좋아졌고, 지금은 Claude Code의 역량과 품질에 여러 번 긍정적으로 놀랐습니다. 제가 미처 고려하지 못했을 정당한 엣지 케이스까지 처리하는 코드를 만들어 내기도 했습니다.
이 글에서는 코딩 에이전트를 안전하게 실행하는(혹은 개인 데이터에 접근하면 안 되는 어떤 워크로드든) 한 가지 가능한 방법을 보여드렸습니다. 여러분의 필요에 맞게 여러 방식으로 조정할 수 있습니다.
이 글이 마음에 드셨나요? 새 글을 놓치지 않으려면 이 블로그의 RSS 피드를 구독해 주세요!
저는 2005년부터 블로그를 운영하며 20년 넘게 지식과 경험을 공유해 왔습니다! :)
제 작업을 후원하고 싶다면 커피 한 잔 사주기를 이용해 주세요.
후원해 주셔서 감사합니다! ❤️
목차
© 2026 Michael Stapelberg • 모든 글은 Creative Commons CC-BY 라이선스 하에 제공됩니다