systemd-nspawn Container Backend
Uses systemd-nspawn for container-level isolation with separate PID, mount, and IPC namespaces. Requires root.
Usage
# Build
nix build github:jhhuh/claude-code-nix-sandbox#container
# Run (requires sudo)
sudo ./result/bin/claude-sandbox-container /path/to/project
sudo ./result/bin/claude-sandbox-container --shell /path/to/project
# Without network
nix build github:jhhuh/claude-code-nix-sandbox#container-no-network
sudo ./result/bin/claude-sandbox-container /path/to/project
How it works
The backend imports nix/sandbox-spec.nix for the canonical package list and evaluates a NixOS configuration (nixosSystem) with spec.packages in environment.systemPackages to produce a system closure (toplevel). Host /etc paths are also driven by the spec. At runtime it:
- Creates an ephemeral container root in
/tmp/claude-nspawn.XXXXXX - Creates stub files (
os-release,machine-id) and passwd/group entries - Detects the real user’s UID/GID via
SUDO_USERfor file ownership - Launches
systemd-nspawn --ephemeralwith bind-mounts for the project, display, audio, GPU, etc. - Runs as PID2 (
--as-pid2), then usessetprivto drop from root to the real user’s UID/GID
The project directory is mounted at /project inside the container.
Why setpriv instead of su/runuser
The container uses setpriv --reuid --regid --init-groups to drop privileges because su and runuser require PAM, which isn’t available in the minimal container environment. See artifacts/skills/nspawn-privilege-drop-without-pam.md for details.
UID/GID mapping
The real user’s UID and GID are detected from SUDO_USER/SUDO_HOME environment variables (set by sudo). A sandbox user is created inside the container with matching UID/GID so that files created in the project directory have correct ownership on the host. See artifacts/skills/sudo-aware-uid-detection-for-containers.md.
Nix parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
network | bool | true | Allow network access (false adds --private-network) |
extraModules | list of NixOS modules | [] | Extra NixOS config for the container |
nixos | function | (required) | NixOS evaluator, typically args: nixpkgs.lib.nixosSystem { ... } |
Customization example
pkgs.callPackage ./nix/backends/container.nix {
nixos = args: nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = args.imports;
};
extraModules = [{
environment.systemPackages = with pkgs; [ python3 nodejs ];
}];
}
Forwarded resources
- X11 display socket and Xauthority (copied into container root)
- Wayland socket
- D-Bus system bus and session bus (Chromium isolated from session bus via wrapper)
- GPU (
/dev/dri,/dev/shm,/run/opengl-driver) - PipeWire and PulseAudio sockets
- SSH agent (remapped to
/run/user/<uid>/ssh-agent.sock) - Git config and SSH keys (read-only)
~/.claudeauth directory (read-write)- Nix store, database, and daemon socket
- Host DNS, TLS certificates, fonts, timezone
- Locale (
LANG,LC_ALL)