Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

QEMU VM Backend

The strongest isolation backend. Runs a full NixOS virtual machine with a separate kernel. Claude Code runs on the serial console (in your terminal), while Chromium renders in the QEMU display window (Xorg + Openbox).

Usage

# Build
nix build github:jhhuh/claude-code-nix-sandbox#vm

# Run
./result/bin/claude-sandbox-vm /path/to/project
./result/bin/claude-sandbox-vm --shell /path/to/project

# Without network
nix build github:jhhuh/claude-code-nix-sandbox#vm-no-network
./result/bin/claude-sandbox-vm /path/to/project

How it works

The backend imports nix/sandbox-spec.nix for the canonical package list and Chrome extension IDs, then evaluates a NixOS VM configuration using the qemu-vm.nix module. The VM is configured with:

  • 4 GB RAM, 4 cores (defaults from virtualisation module)
  • Serial console on stdio for Claude Code interaction
  • QEMU GTK window running Xorg + Openbox for Chromium display
  • 9p filesystem shares for project directory, auth, git config, SSH keys, and metadata

Console setup

The VM has two consoles: tty0 (QEMU window) and ttyS0 (serial/stdio). The serial console is listed last in virtualisation.qemu.consoles so Linux makes it /dev/console. Getty auto-logs in the sandbox user on ttyS0.

A tty guard in interactiveShellInit ensures the entrypoint (Claude Code or bash) only runs on ttyS0, not on the graphical tty0. See artifacts/skills/nixos-qemu-vm-serial-console-setup.md.

9p filesystem shares

Mount pointTagModeDescription
/projectproject_shareRead-writeProject directory
/home/sandbox/.claudeclaude_authRead-write, nofailAuth persistence
/home/sandbox/.gitconfiggit_configRead-only, nofailGit config
/home/sandbox/.config/gitgit_config_dirRead-only, nofailGit config directory
/home/sandbox/.config/ghgh_config_dirRead-only, nofailGitHub CLI config
/home/sandbox/.sshssh_dirRead-only, nofailSSH keys
/mnt/metaclaude_metaRead-onlyEntrypoint and API key

Shares use msize=104857600 (100 MB) for the project directory to improve I/O throughput. The nofail option allows the VM to boot even if the host directory doesn’t exist.

Metadata passing

The entrypoint command, API key, GitHub token, and locale settings are written to a temporary directory on the host and shared via 9p as /mnt/meta. The VM reads these files during shell init:

  • /mnt/meta/entrypoint — command to run (claude or bash)
  • /mnt/meta/apikey — Anthropic API key
  • /mnt/meta/host_home — host user’s home path (for path reconstruction)
  • /mnt/meta/host_project — host project path (for bind-mount)
  • /mnt/meta/claude.json — Claude config file
  • /mnt/meta/gh_token — GitHub token (when --gh-token is used)
  • /mnt/meta/lang — LANG locale setting
  • /mnt/meta/lc_all — LC_ALL locale setting

Nix parameters

ParameterTypeDefaultDescription
networkbooltrueEnable DHCP networking (false empties vlans)
extraModuleslist of NixOS modules[]Extra NixOS config for the VM
nixosfunction(required)NixOS evaluator

Customization example

pkgs.callPackage ./nix/backends/vm.nix {
  nixos = args: nixpkgs.lib.nixosSystem {
    system = "x86_64-linux";
    modules = args.imports;
  };
  extraModules = [{
    virtualisation.memorySize = 8192;
    virtualisation.cores = 8;
    environment.systemPackages = with pkgs; [ python3 ];
  }];
}

Requirements

  • KVM recommended (/dev/kvm) for reasonable performance
  • Works without KVM but is significantly slower (software emulation)