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

claude-code-nix-sandbox

Warning: This project is under active development and should be considered unstable. Features may be incomplete, broken, or change without notice. If you choose to run it, you do so at your own risk. There are no guarantees of correctness, security, or fitness for any particular purpose.

Launch sandboxed Claude Code sessions with Chromium using Nix.

Claude Code (from sadjow/claude-code-nix) runs inside an isolated sandbox with filesystem isolation, display forwarding, and a Chromium browser. Three backends are available with increasing isolation strength:

BackendIsolationRequires
BubblewrapUser namespaces, shared kernelUnprivileged
systemd-nspawnFull namespace isolationRoot (sudo)
QEMU VMSeparate kernel, hardware virtualizationKVM recommended

A remote sandbox manager is also provided: a Rust/Axum daemon with a web dashboard and CLI for managing sandboxes on a server over SSH.

Web Dashboard

Dashboard — sandbox list with live screenshots and system metrics

Sandbox detail — live screenshot, Claude metrics, and WebSocket log viewer

Features

  • Pure Nix — no shell/Python wrappers; all orchestration in Nix
  • Chromium from nixpkgs — always pkgs.chromium inside the sandbox
  • Git/SSH forwarding — push/pull works inside all backends
  • Nix commandsNIX_REMOTE=daemon forwarding so nix build works inside sandboxes
  • Display forwarding — X11, Wayland, GPU acceleration (bubblewrap/container) or QEMU window (VM)
  • Audio forwarding — PipeWire/PulseAudio (bubblewrap/container)
  • D-Bus session bus proxy — filtered via xdg-dbus-proxy (keyring/Secret Service only, blocks Chromium singleton collisions)
  • Remote management — web dashboard with live screenshots, real-time log streaming via WebSocket, metrics, and a CLI over SSH

Quick Start

# Install both claude-sandbox and claude-code (bundled)
nix profile install github:jhhuh/claude-code-nix-sandbox

# Bubblewrap (unprivileged)
nix run github:jhhuh/claude-code-nix-sandbox#sandbox -- /path/to/project

# systemd-nspawn container (requires sudo)
nix build github:jhhuh/claude-code-nix-sandbox#container
sudo ./result/bin/claude-sandbox-container /path/to/project

# QEMU VM (strongest isolation)
nix build github:jhhuh/claude-code-nix-sandbox#vm
./result/bin/claude-sandbox-vm /path/to/project

Requires ANTHROPIC_API_KEY in your environment, or an existing ~/.claude login (auto-mounted).

See Getting Started for full details.

Getting Started

Requirements

  • NixOS or Nix with flakes enabled on Linux
  • User namespaces for the bubblewrap backend (enabled by default on most distros)
  • X11 or Wayland display server for bubblewrap/container backends
  • KVM recommended for the VM backend (/dev/kvm)
  • ANTHROPIC_API_KEY in your environment, or an existing ~/.claude login (auto-mounted)

Quick Start

Install (both sandboxed and un-sandboxed)

The default package bundles claude-sandbox (bubblewrap) and claude (un-sandboxed) together:

# Install both binaries
nix profile install github:jhhuh/claude-code-nix-sandbox

# Update to latest
nix profile upgrade claude-code-nix-sandbox --refresh

Bubblewrap (unprivileged)

# Run Claude Code in a sandbox
nix run github:jhhuh/claude-code-nix-sandbox#sandbox -- /path/to/project

# Run inside tmux (needed for agent teams)
nix run github:jhhuh/claude-code-nix-sandbox#sandbox -- --tmux /path/to/project

# Drop into a shell inside the sandbox
nix run github:jhhuh/claude-code-nix-sandbox#sandbox -- --shell /path/to/project

systemd-nspawn container (requires sudo)

nix build github:jhhuh/claude-code-nix-sandbox#container

sudo ./result/bin/claude-sandbox-container /path/to/project

# Shell mode
sudo ./result/bin/claude-sandbox-container --shell /path/to/project

QEMU VM (strongest isolation)

Claude runs on the serial console in your terminal. Chromium renders in the QEMU display window.

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

./result/bin/claude-sandbox-vm /path/to/project

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

What gets forwarded

All three backends automatically forward these from your host:

  • ~/.claude — auth persistence (read-write)
  • ~/.gitconfig, ~/.config/git/, ~/.ssh/ — git/SSH config (read-only)
  • SSH_AUTH_SOCK — SSH agent forwarding
  • ANTHROPIC_API_KEY — API key (if set)
  • /nix/store — Nix store (read-only) + daemon socket

Available packages

PackageBinaryDescription
defaultclaude-sandbox, claudeBubblewrap sandbox + un-sandboxed claude-code (bundled)
sandboxclaude-sandboxBubblewrap sandbox only
no-networkclaude-sandboxBubblewrap without network
containerclaude-sandbox-containersystemd-nspawn with network
container-no-networkclaude-sandbox-containersystemd-nspawn without network
vmclaude-sandbox-vmQEMU VM with NAT
vm-no-networkclaude-sandbox-vmQEMU VM without network
managerclaude-sandbox-managerRemote sandbox manager daemon
cliclaude-remoteCLI for remote management

Build any package with:

nix build github:jhhuh/claude-code-nix-sandbox#<package>

Sandbox Backends

All three backends share a common pattern: they are callPackage-able Nix functions that produce writeShellApplication derivations. Each accepts network (bool) and backend-specific customization options.

Comparison

ResourceBubblewrapContainerVM
Project directoryRead-write (bind-mount)Read-write (bind-mount)Read-write (9p)
~/.claudeRead-write (bind-mount)Read-write (bind-mount)Read-write (9p)
~/.gitconfig, ~/.sshRead-only (bind-mount)Read-only (bind-mount)Read-only (9p)
/nix/storeRead-onlyRead-onlyShared from host
/homeIsolated (tmpfs)IsolatedSeparate filesystem
NetworkShared by defaultShared by defaultNAT by default
DisplayHost X11/WaylandHost X11/WaylandQEMU window (Xorg)
AudioPipeWire/PulseAudioPipeWire/PulseAudioIsolated
GPU (DRI)ForwardedForwardedVirtio VGA
D-BusForwardedForwardedIsolated
SSH agentForwardedForwardedIsolated
Nix commandsVia daemonVia daemonLocal store
GitHub CLI configForwardedForwardedForwarded (9p)
LocaleForwardedForwardedForwarded (meta)
KernelSharedSharedSeparate

Choosing a backend

  • Bubblewrap — fastest startup, least overhead, good for day-to-day use. Shares the host kernel and network by default. Requires user namespace support.
  • Container — stronger isolation with separate PID/mount/IPC namespaces. Requires root. Good when you need namespace-level isolation without the overhead of a VM.
  • VM — strongest isolation with a separate kernel. Best for untrusted workloads. Requires KVM for reasonable performance. Chromium renders in the QEMU window rather than forwarding to the host display.

Common flags

All backends accept:

[--shell] [--gh-token] <project-dir> [claude args...]
  • --shell — drop into bash instead of launching Claude Code
  • --gh-token — forward GH_TOKEN/GITHUB_TOKEN env vars into the sandbox
  • <project-dir> — the directory to mount read-write inside the sandbox
  • Additional arguments after the project directory are passed to claude

Bubblewrap Backend

The default backend. Uses bubblewrap (bwrap) to create a lightweight sandbox using Linux user namespaces. No root required.

Usage

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

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

# Run inside tmux (needed for agent teams)
./result/bin/claude-sandbox --tmux /path/to/project

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

How it works

The sandbox script imports nix/sandbox-spec.nix for the canonical package list and builds a symlinkJoin of spec.packages plus chromiumSandbox and any extraPackages into a single PATH. Host /etc paths are also driven by the spec. It then calls bwrap with:

  • Filesystem: /nix/store read-only, project directory read-write, ~/.claude read-write, /home as tmpfs
  • Display: X11 socket + Xauthority, Wayland socket forwarded
  • D-Bus: system bus and session bus forwarded (Chromium isolated from session bus via env -u DBUS_SESSION_BUS_ADDRESS in wrapper to prevent singleton collisions)
  • GPU: /dev/dri and /run/opengl-driver forwarded for hardware acceleration
  • Audio: PipeWire and PulseAudio sockets forwarded
  • Network: shared with host by default, --unshare-net when network = false
  • Nix: daemon socket forwarded with NIX_REMOTE=daemon

The sandbox home is /home/sandbox. The process runs as your user (no UID mapping).

tmux mode (--tmux)

The --tmux flag starts claude-code inside a tmux session, required for Claude Code’s experimental agent teams feature. The tmux state is stored per-project in <project-dir>/.tmux/:

  • tmux.conf — minimal config, created on first run, editable and persistent across restarts
  • socket — tmux server socket (runtime, per-project to avoid collisions)

The session is named sandbox:<project-name> with an orange status bar to visually distinguish it from host tmux sessions.

Nix parameters

ParameterTypeDefaultDescription
networkbooltrueAllow network access (false adds --unshare-net)
extraPackageslist of packages[]Additional packages on PATH inside the sandbox

Customization example

pkgs.callPackage ./nix/backends/bubblewrap.nix {
  extraPackages = [ pkgs.python3 pkgs.nodejs ];
  network = false;
}

Requirements

  • Linux with user namespace support (security.unprivilegedUsernsClone = true on NixOS)
  • X11 or Wayland display server

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:

  1. Creates an ephemeral container root in /tmp/claude-nspawn.XXXXXX
  2. Creates stub files (os-release, machine-id) and passwd/group entries
  3. Detects the real user’s UID/GID via SUDO_USER for file ownership
  4. Launches systemd-nspawn --ephemeral with bind-mounts for the project, display, audio, GPU, etc.
  5. Runs as PID2 (--as-pid2), then uses setpriv to 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

ParameterTypeDefaultDescription
networkbooltrueAllow network access (false adds --private-network)
extraModuleslist of NixOS modules[]Extra NixOS config for the container
nixosfunction(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)
  • ~/.claude auth directory (read-write)
  • Nix store, database, and daemon socket
  • Host DNS, TLS certificates, fonts, timezone
  • Locale (LANG, LC_ALL)

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)

Remote Manager

Run sandboxes on a remote server and manage them from your laptop via a web dashboard or CLI. The manager is a Rust/Axum daemon that orchestrates sandbox lifecycles, captures live screenshots, and collects metrics.

Architecture

laptop                              remote server
  │                                   │
  │  claude-remote create ...         │ manager daemon (127.0.0.1:3000)
  │ ─────────────────────────────────>│   ├── starts Xvfb display
  │                                   │   ├── starts tmux session
  │  claude-remote attach <id>        │   ├── runs sandbox backend
  │ ─────────────────────────────────>│   ├── captures screenshots
  │                                   │   └── collects metrics
  │  claude-remote ui                 │
  │  open http://localhost:3000       │ web dashboard (htmx, live refresh)
  │ ─────────────────────────────────>│

All communication happens over SSH — the CLI runs ssh $HOST curl ... to talk to the manager’s localhost-only HTTP API.

Running the manager

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

# Run
MANAGER_LISTEN=127.0.0.1:3000 ./result/bin/claude-sandbox-manager

Or deploy as a NixOS systemd service — see Manager Module.

Environment variables

VariableDefaultDescription
MANAGER_LISTEN127.0.0.1:3000Listen address and port
MANAGER_STATE_DIR.Directory for state.json persistence
MANAGER_STATIC_DIR(set by Nix wrapper)Path to static web assets

Components

The manager daemon runs three concurrent tasks:

  1. HTTP server — Axum router serving pages, JSON API, htmx fragments, and static files
  2. Liveness monitor — checks tmux sessions every 5 seconds, marks dead sandboxes
  3. Screenshot loop — captures Xvfb displays (ImageMagick import) or QEMU QMP screendumps every 2 seconds

State persistence

Sandbox state is persisted as JSON in $MANAGER_STATE_DIR/state.json. On startup, the manager loads existing state and reconciles PIDs — any sandbox whose tmux session has disappeared is marked as dead.

Runtime dependencies

The Nix package wraps the manager binary with these tools on PATH:

  • ImageMagick — Xvfb screenshot capture (import)
  • socat — QEMU QMP communication
  • tmux — sandbox session management
  • Xvfb (xorgserver) — virtual framebuffer for bubblewrap/container backends
  • Sandbox backends — configured via sandboxPackages parameter

CLI (claude-remote)

claude-remote is a local CLI for managing sandboxes on a remote server. All commands run over SSH — no direct HTTP from your laptop.

Installation

Available in the devShell or as a standalone package:

# Via devShell
nix develop github:jhhuh/claude-code-nix-sandbox
claude-remote help

# Standalone
nix build github:jhhuh/claude-code-nix-sandbox#cli
./result/bin/claude-remote help

Configuration

Settings are resolved in order: environment variable > config file > default.

Config file

Location: ${XDG_CONFIG_HOME:-~/.config}/claude-remote/config

# ~/.config/claude-remote/config
host = myserver
port = 3000
ssh_opts = -i ~/.ssh/mykey

Lines starting with # are comments. Blank lines are ignored.

Environment variables

VariableConfig keyDefaultDescription
CLAUDE_REMOTE_HOSThostRemote server hostname (required)
CLAUDE_REMOTE_PORTport3000Manager port on the remote
CLAUDE_REMOTE_SSH_OPTSssh_optsExtra SSH options (e.g. -i ~/.ssh/key)

Environment variables always override config file values.

Commands

create

Create a new sandbox on the remote server.

claude-remote create <name> <backend> <project-dir> [--no-network] [--sync]
  • <backend>bubblewrap, container, or vm
  • --no-network — disable network access
  • --sync — rsync the local project directory to the remote before creating

list

List all sandboxes (alias: ls).

claude-remote list

Output shows id (first 8 chars), name, backend, status, and project directory.

attach

Attach to a sandbox’s tmux session over SSH.

claude-remote attach <id-prefix>

The id-prefix can be any unique prefix of the sandbox UUID.

stop

Stop a running sandbox.

claude-remote stop <id-prefix>

delete

Delete a sandbox (alias: rm).

claude-remote delete <id-prefix>

metrics

Show system metrics, and optionally sandbox-specific Claude session metrics.

claude-remote metrics              # system only
claude-remote metrics <id-prefix>  # system + sandbox Claude metrics

sync

One-shot rsync from local to remote.

claude-remote sync <local-dir> [remote-dir]

If remote-dir is omitted, it defaults to the same path as local-dir. Excludes .git/ and respects .gitignore.

watch

Continuous bidirectional sync using fswatch + rsync.

claude-remote watch <local-dir> [remote-dir]
  • Performs an initial local-to-remote sync
  • Watches for local file changes and syncs to remote (debounced with 100ms window)
  • Polls remote-to-local every 2 seconds in the background
  • Excludes .git/ and respects .gitignore
  • Ctrl+C to stop

ui

Forward the web dashboard via SSH tunnel.

claude-remote ui
# Then open http://localhost:3000

REST API

The manager exposes a JSON API alongside the web dashboard. All endpoints listen on the configured MANAGER_LISTEN address (default 127.0.0.1:3000).

Endpoints

List sandboxes

GET /api/sandboxes

Returns a JSON array of all sandboxes, sorted by creation time (newest first).

curl localhost:3000/api/sandboxes

Get sandbox

GET /api/sandboxes/<id>

Returns a single sandbox by full UUID.

curl localhost:3000/api/sandboxes/<id>

Create sandbox

POST /api/sandboxes
Content-Type: application/json

Request body:

{
  "name": "my-project",
  "backend": "bubblewrap",
  "project_dir": "/home/user/project",
  "network": true
}
  • backend"bubblewrap", "container", or "vm"
  • network — optional, defaults to true

Returns 201 Created with the sandbox JSON on success.

curl -X POST localhost:3000/api/sandboxes \
  -H 'Content-Type: application/json' \
  -d '{"name":"test","backend":"bubblewrap","project_dir":"/tmp/test","network":true}'

Stop sandbox

POST /api/sandboxes/<id>/stop

Returns 204 No Content on success.

curl -X POST localhost:3000/api/sandboxes/<id>/stop

Delete sandbox

DELETE /api/sandboxes/<id>

Returns 204 No Content on success.

curl -X DELETE localhost:3000/api/sandboxes/<id>

Get screenshot

GET /api/sandboxes/<id>/screenshot

Returns the latest screenshot as image/png. Returns 404 if no screenshot is available.

curl localhost:3000/api/sandboxes/<id>/screenshot -o screenshot.png

Get sandbox metrics

GET /api/sandboxes/<id>/metrics

Returns Claude session metrics parsed from the sandbox’s project directory (tokens used, tool calls, message count, etc.).

curl localhost:3000/api/sandboxes/<id>/metrics

Get system metrics

GET /api/metrics/system

Returns system-wide metrics: CPU usage, memory, disk, and load averages.

curl localhost:3000/api/metrics/system

Get logs

GET /api/sandboxes/<id>/logs

Returns the full log file (tmux pipe-pane output) as text/plain.

curl localhost:3000/api/sandboxes/<id>/logs

Stream logs (WebSocket)

GET /ws/sandboxes/<id>/logs

Upgrades to a WebSocket connection. Sends the last 1000 lines as initial backlog, then pushes new lines in real time as the sandbox produces output.

Sandbox object

{
  "id": "a1b2c3d4-...",
  "name": "my-project",
  "backend": "bubblewrap",
  "project_dir": "/home/user/project",
  "status": "running",
  "display_num": 50,
  "tmux_session": "claude-a1b2c3d4",
  "pid_xvfb": 12345,
  "qemu_qmp_socket": null,
  "network": true,
  "created_at": "2025-01-15T10:30:00Z"
}
  • status"running", "stopped", or "dead"
  • display_num — Xvfb display number (bubblewrap/container backends)
  • qemu_qmp_socket — QMP socket path (VM backend)
  • tmux_session — tmux session name for attaching

Web Dashboard

The manager includes a web dashboard for visual sandbox management. Access it via claude-remote ui (SSH tunnel) or directly if you can reach the manager’s listen address.

Dashboard — sandbox list with live screenshots and system metrics

Features

  • Sandbox list — all sandboxes with status badges, backend type, and creation time
  • Live screenshots — captured every 2 seconds from Xvfb or QEMU QMP
  • Sandbox detail — individual page with live screenshot feed, Claude session metrics, and real-time log viewer
  • Real-time log streaming — WebSocket-powered terminal view of sandbox tmux output
  • Create form — HTML form for creating new sandboxes
  • System metrics — CPU, memory, disk usage

Sandbox Detail

Sandbox detail — live screenshot, Claude metrics, and WebSocket log viewer

Each sandbox detail page shows:

  • Sandbox info — ID, backend, project directory, network status, display number, tmux session
  • Live screenshot — auto-refreshing Xvfb or QEMU screendump
  • Claude metrics — messages, input/output tokens, cache stats, tool uses (parsed from Claude’s JSONL session files)
  • Log viewer — real-time streaming of the sandbox’s tmux output via WebSocket, with connection status indicator and auto-scroll

Technology

The dashboard is server-rendered HTML with htmx for auto-refreshing fragments. There is no JavaScript build step — htmx and CSS are vendored as static files, and HTML templates are compiled into the binary via askama.

Log streaming uses a WebSocket endpoint (/ws/sandboxes/<id>/logs) that tails the sandbox’s tmux pipe-pane log file and pushes new lines to connected clients in real time.

htmx fragments

The dashboard uses htmx polling to keep content fresh without full page reloads:

Fragment endpointDescription
/fragments/sandbox-listSandbox list on the index page
/fragments/system-metricsSystem metrics display
/fragments/sandboxes/<id>/claude-metricsClaude session metrics for a sandbox
/fragments/sandboxes/<id>/screenshotLive screenshot <img> tag

WebSocket endpoint

EndpointDescription
/ws/sandboxes/<id>/logsReal-time log stream (sends last 1000 lines as backlog, then new lines as they appear)

REST endpoint

EndpointDescription
/api/sandboxes/<id>/logsFull log file as text/plain

Pages

URLDescription
/Index — sandbox list + system metrics
/newCreate sandbox form
/sandboxes/<id>Sandbox detail — screenshot + metrics + log viewer

Sandbox NixOS Module

For NixOS users, a declarative module installs sandbox backends as system packages.

Usage

# flake.nix
{
  inputs.claude-sandbox.url = "github:jhhuh/claude-code-nix-sandbox";

  outputs = { nixpkgs, claude-sandbox, ... }: {
    nixosConfigurations.myhost = nixpkgs.lib.nixosSystem {
      modules = [
        claude-sandbox.nixosModules.default
        {
          services.claude-sandbox = {
            enable = true;
            container.enable = true;
            vm.enable = true;
          };
        }
      ];
    };
  };
}

Options

services.claude-sandbox.enable

Whether to install Claude Code sandbox wrappers.

  • Type: bool
  • Default: false

services.claude-sandbox.network

Allow network access from sandboxes. Applies to all enabled backends.

  • Type: bool
  • Default: true

services.claude-sandbox.bubblewrap.enable

Install the bubblewrap sandbox. Enabled by default when the module is active.

  • Type: bool
  • Default: true

services.claude-sandbox.bubblewrap.extraPackages

Extra packages available inside the bubblewrap sandbox.

  • Type: list of package
  • Default: []

services.claude-sandbox.container.enable

Install the systemd-nspawn container sandbox.

  • Type: bool
  • Default: false

services.claude-sandbox.container.extraModules

Extra NixOS modules for the container.

  • Type: list of anything
  • Default: []

services.claude-sandbox.vm.enable

Install the QEMU VM sandbox.

  • Type: bool
  • Default: false

services.claude-sandbox.vm.extraModules

Extra NixOS modules for the VM.

  • Type: list of anything
  • Default: []

Implied configuration

When enabled, the module also sets:

  • nixpkgs.config.allowUnfree = true — may be required depending on claude-code source
  • security.unprivilegedUsernsClone = true — when bubblewrap is enabled (required for user namespaces)

Example with customization

services.claude-sandbox = {
  enable = true;
  network = false;  # isolate all backends

  bubblewrap.extraPackages = with pkgs; [ python3 nodejs ];

  container.enable = true;
  container.extraModules = [{
    environment.systemPackages = with pkgs; [ python3 ];
  }];

  vm.enable = true;
  vm.extraModules = [{
    virtualisation.memorySize = 8192;
  }];
};

Manager NixOS Module

Deploy the remote sandbox manager as a systemd service.

Usage

# flake.nix
{
  inputs.claude-sandbox.url = "github:jhhuh/claude-code-nix-sandbox";

  outputs = { nixpkgs, claude-sandbox, ... }: {
    nixosConfigurations.myhost = nixpkgs.lib.nixosSystem {
      modules = [
        claude-sandbox.nixosModules.manager
        {
          services.claude-sandbox-manager = {
            enable = true;
            sandboxPackages = [
              claude-sandbox.packages.x86_64-linux.default
            ];
          };
        }
      ];
    };
  };
}

Options

services.claude-sandbox-manager.enable

Enable the Claude Sandbox Manager web dashboard.

  • Type: bool
  • Default: false

services.claude-sandbox-manager.listenAddress

Address and port for the manager to listen on.

  • Type: str
  • Default: "127.0.0.1:3000"

services.claude-sandbox-manager.stateDir

Directory for persistent state (state.json).

  • Type: str
  • Default: "/var/lib/claude-manager"

services.claude-sandbox-manager.user

System user to run the manager as.

  • Type: str
  • Default: "claude-manager"

services.claude-sandbox-manager.group

System group to run the manager as.

  • Type: str
  • Default: "claude-manager"

services.claude-sandbox-manager.sandboxPackages

Sandbox backend packages to put on the manager’s PATH.

  • Type: list of package
  • Default: []

services.claude-sandbox-manager.containerSudoers

Add a sudoers rule allowing the manager user to run claude-sandbox-container without a password. Required if you want the manager to launch container-backend sandboxes.

  • Type: bool
  • Default: false

What the module creates

  • A system user and group (claude-manager by default)
  • A systemd service (claude-sandbox-manager.service) that:
    • Sets MANAGER_LISTEN and MANAGER_STATE_DIR environment variables
    • Puts sandboxPackages on PATH
    • Manages StateDirectory for persistent data
    • Restarts on failure (5 second delay)
  • Optionally, a sudoers rule for the container backend

Example with container support

services.claude-sandbox-manager = {
  enable = true;
  listenAddress = "127.0.0.1:3001";
  sandboxPackages = [
    claude-sandbox.packages.x86_64-linux.default
    claude-sandbox.packages.x86_64-linux.container
  ];
  containerSudoers = true;
};

Customization

All backends are callPackage-able Nix functions, so you can override their parameters directly in your flake.

Extra packages (bubblewrap)

Add packages to the sandbox PATH via extraPackages:

packages.default = pkgs.callPackage ./nix/backends/bubblewrap.nix {
  extraPackages = with pkgs; [ python3 nodejs ripgrep ];
};

Extra NixOS modules (container / VM)

Add NixOS configuration to the container or VM via extraModules:

packages.container = 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 ];
    # Any NixOS option works here
  }];
};

For the VM backend, you can also configure VM-specific options:

packages.vm = 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 ];
  }];
};

Network isolation

All backends accept network = false to disable network access:

# Bubblewrap: adds --unshare-net
packages.isolated = pkgs.callPackage ./nix/backends/bubblewrap.nix {
  network = false;
};

# Container: adds --private-network
packages.container-isolated = pkgs.callPackage ./nix/backends/container.nix {
  nixos = args: nixpkgs.lib.nixosSystem { ... };
  network = false;
};

# VM: disables DHCP, empties vlans
packages.vm-isolated = pkgs.callPackage ./nix/backends/vm.nix {
  nixos = args: nixpkgs.lib.nixosSystem { ... };
  network = false;
};

Pre-built network-isolated variants are available as no-network, container-no-network, and vm-no-network packages.

Manager sandbox backends

Configure which backends the manager can use via sandboxPackages:

packages.manager = pkgs.callPackage ./nix/manager/package.nix {
  sandboxPackages = [
    (pkgs.callPackage ./nix/backends/bubblewrap.nix { })
    (pkgs.callPackage ./nix/backends/bubblewrap.nix { network = false; })
  ];
};

Using as a flake input

{
  inputs.claude-sandbox.url = "github:jhhuh/claude-code-nix-sandbox";
  inputs.claude-code-nix.url = "github:sadjow/claude-code-nix";

  outputs = { nixpkgs, claude-sandbox, claude-code-nix, ... }:
    let
      pkgs = import nixpkgs {
        system = "x86_64-linux";
        overlays = [ claude-code-nix.overlays.default ];
      };
    in {
      # Use a backend directly (needs claude-code-nix overlay for pkgs.claude-code)
      packages.x86_64-linux.my-sandbox = pkgs.callPackage
        "${claude-sandbox}/nix/backends/bubblewrap.nix"
        { extraPackages = [ pkgs.python3 ]; };

      # Or use the pre-built packages (overlay already applied)
      packages.x86_64-linux.sandbox = claude-sandbox.packages.x86_64-linux.default;
    };
}

Architecture

Directory structure

flake.nix              # Entry point: packages, checks, nixosModules, devShells
nix/sandbox-spec.nix   # Single source of truth for sandbox requirements
nix/chromium.nix       # Chromium wrapper with extension policy
nix/backends/
  bubblewrap.nix       # bwrap sandbox — unprivileged, user namespaces
  container.nix        # systemd-nspawn container — requires root
  vm.nix               # QEMU VM — separate kernel, hardware virtualization
nix/modules/
  sandbox.nix          # NixOS module for declarative sandbox configuration
  manager.nix          # NixOS module for the manager systemd service
nix/manager/
  package.nix          # rustPlatform.buildRustPackage for the manager daemon
scripts/
  claude-remote.nix    # writeShellApplication CLI for remote management
manager/               # Rust/Axum web dashboard + REST API
  src/
    main.rs            # Axum router, background tasks (monitor + screenshot)
    state.rs           # Sandbox/ManagerState types, JSON persistence
    api.rs             # Page handlers + JSON REST API
    fragments.rs       # htmx partial handlers for auto-refreshing
    sandbox.rs         # Lifecycle: Xvfb → tmux → backend → monitor
    display.rs         # Xvfb spawn/kill, display number allocation
    session.rs         # tmux create/check/kill
    screenshot.rs      # Xvfb capture (ImageMagick) + VM QMP screendump
    metrics.rs         # sysinfo metrics + Claude JSONL session parser
  templates/           # askama HTML templates
  static/              # Vendored htmx.min.js + style.css
tests/
  manager.nix          # NixOS VM integration test

Design principles

  • Pure Nix — all orchestration is Nix expressions, no shell/Python wrappers for coordination
  • One backend per file — each backend is a self-contained callPackage-able function in nix/backends/
  • Spec-drivennix/sandbox-spec.nix is the single source of truth for packages, extension IDs, and /etc paths. Backends import the spec and implement HOW to deliver each requirement
  • Chromium from nixpkgs — always pkgs.chromium, never a manual download. Chromium is excluded from the spec because bwrap uses a chromiumSandbox wrapper while container/VM use stock chromium
  • Dynamic bash arrays — backends build bwrap/nspawn/QEMU argument lists conditionally using bash arrays for optional features (display, D-Bus, GPU, auth, network)

Backend pattern

Each backend follows the same structure:

  1. Nix function with { lib, pkgs, writeShellApplication, ..., network ? true, extraPackages/extraModules ? [] }
  2. Import specspec = import ../sandbox-spec.nix { inherit pkgs; } for packages and /etc paths
  3. Build a PATH or system closuresymlinkJoin with spec.packages (bubblewrap) or nixosSystem with spec.packages in environment.systemPackages (container/VM)
  4. Generate a shell script via writeShellApplication that:
    • Parses --shell, --gh-token flags and project directory argument
    • Conditionally builds arrays of flags for display, D-Bus, GPU, audio, auth, git, SSH, network
    • Execs the sandbox runtime (bwrap, systemd-nspawn, or QEMU VM script)

Manager architecture

The manager daemon (manager/src/main.rs) runs three concurrent tokio tasks:

  1. HTTP server — Axum router with:
    • HTML pages (askama templates): index, new sandbox form, sandbox detail
    • JSON API: CRUD for sandboxes, screenshots, metrics
    • htmx fragments: auto-refreshing partial HTML responses
    • Static file serving: vendored htmx.min.js and CSS
  2. Liveness monitor (5s interval) — reconciles tmux sessions, marks dead sandboxes
  3. Screenshot loop (2s interval) — captures Xvfb displays via ImageMagick import or QEMU QMP screendump

State is shared via Arc<AppState> with tokio::sync::RwLock for the manager state and screenshot cache.

CLI architecture

claude-remote is a writeShellApplication that wraps ssh, curl, jq, tmux, rsync, and fswatch. Every API call is executed as ssh $HOST curl -s ... — the CLI never makes direct HTTP requests.