25 KiB
Phase 1: Minimal Viable Sandbox - Research
Researched: 2026-04-09 Domain: Nix derivation + bubblewrap sandboxing for Claude Code Confidence: HIGH
Summary
This phase produces a single Nix flake that outputs a claudebox command wrapping Claude Code inside a bubblewrap (bwrap) sandbox. The sandbox uses --clearenv to start with an empty environment, allowlists specific variables, bind-mounts only the necessary filesystem paths, and explicitly excludes all secret material.
The host system (NixOS with Lix 2.93.3) has bubblewrap 0.11.0, which supports all required flags. Claude Code is a Node.js application (v2.1.70 on host, 2.0.51 in nixpkgs) installed as a wrapped bash script that execs node. The comma-with-db package from nix-community/nix-index-database is confirmed available and bundles its own database. NixOS has several /etc symlink chains that need careful handling for DNS and SSL to work inside the sandbox.
Primary recommendation: Use writeShellApplication with builtins.readFile for the script body, --clearenv + --setenv for environment, tmpfs root with selective bind-mounts, and exec into the final claude command for clean signal handling.
<user_constraints>
User Constraints (from CONTEXT.md)
Locked Decisions
- D-01: Forward all unknown flags to
claude. claudebox claims only its own flags (--yes,--dry-run,--check) and passes everything else through. No--separator required.--dangerously-skip-permissionsis always injected. - D-02: Use
comma-with-dbfrom thenix-community/nix-index-databaseflake. Self-contained -- bundles the package index, no host dependency, no extra bind mount needed. DB updates when the flake input is bumped. - D-03: Strict allowlist per SAND-03, plus a
CLAUDEBOX_EXTRA_ENVescape hatch. Core allowlist always passes (HOME, PATH, TERM, EDITOR, LANG, LC_ALL, NIX_SSL_CERT_FILE, SSL_CERT_FILE, ANTHROPIC_API_KEY, USER, SHELL, XDG_RUNTIME_DIR). User can add extras at launch viaCLAUDEBOX_EXTRA_ENV="COLORTERM,NODE_OPTIONS"-- their responsibility to not leak secrets. - D-04: Sandbox-generated vars (TMPDIR=/tmp, etc.) are set via
--setenv, never read from host. - D-05: Generate a minimal
.gitconfiginside the sandbox at launch time. Readsuser.nameanduser.emailfrom the host's git config, writes them plussafe.directory = *into the sandbox's~/.gitconfig. No host.gitconfigmounted.
Claude's Discretion
- Mount ordering strategy for CWD-under-HOME (bwrap specifics)
- Exact tmpfs layout and /dev, /proc, /tmp setup
- How
--clearenv+--setenvare sequenced in the bwrap invocation - DNS resolution mount strategy (resolv.conf and its symlink targets)
- SSL cert bundle path detection
Deferred Ideas (OUT OF SCOPE)
None -- discussion stayed within phase scope. </user_constraints>
<phase_requirements>
Phase Requirements
| ID | Description | Research Support |
|---|---|---|
| SAND-01 | Wrapper via Nix writeShellApplication |
Standard Stack: writeShellApplication with builtins.readFile pattern |
| SAND-02 | --clearenv empty environment |
Verified: bwrap 0.11.0 supports --clearenv + --setenv |
| SAND-03 | Environment allowlist | Architecture: env passthrough loop pattern |
| SAND-04 | tmpfs root filesystem | Verified: --tmpfs / works in bwrap 0.11.0 |
| SAND-05 | CWD bind-mounted rw | Architecture: mount ordering (CWD after HOME dir creation) |
| SAND-06 | /nix/store read-only |
Verified: --ro-bind /nix/store /nix/store works |
| SAND-07 | Nix daemon socket mounted | Verified: /nix/var/nix/daemon-socket bind works, nix can talk to daemon |
| SAND-08 | ~/.claudebox -> ~/.claude |
Architecture: bind ~/.claudebox as $HOME/.claude |
| SAND-09 | Secret paths never mounted | Architecture: negative list, verified by env check |
| SAND-10 | PATH only Nix store paths | Standard Stack: runtimeInputs wires PATH automatically |
| SAND-11 | Working /tmp, /dev, /proc | Verified: --tmpfs /tmp --dev /dev --proc /proc |
| SAND-12 | DNS resolution works | Pitfalls: NixOS resolv.conf is a real file (not symlink), bind-mount directly |
| SAND-13 | SSL/TLS works | Pitfalls: NixOS cert chain requires /etc/ssl AND /etc/static mounts |
| SAND-14 | Exit code passthrough | Architecture: exec bwrap ... pattern |
| SAND-15 | Signals via exec | Architecture: exec ensures no intermediate shell |
| TOOL-01 | comma available | Standard Stack: comma-with-db from nix-index-database flake |
| TOOL-02 | nix shell works |
Verified: daemon socket + nix.conf mount enables nix commands |
| TOOL-03 | New store paths visible | Architecture: /nix/store must be a live bind, not snapshot |
| GIT-01 | Git works with minimal config | Architecture: generate .gitconfig at launch from host identity |
| GIT-02 | safe.directory configured | Architecture: safe.directory = * in generated .gitconfig |
| NIX-01 | Nix flake with default package | Standard Stack: flake.nix structure |
| NIX-02 | Runtime deps pinned via flake | Standard Stack: flake inputs pin nixpkgs + nix-index-database |
| NIX-03 | nix run / nix profile install works |
Standard Stack: flake outputs packages.default |
| UX-06 | --dangerously-skip-permissions always passed |
Architecture: injected before user args in exec |
| </phase_requirements> |
Standard Stack
Core
| Library | Version | Purpose | Why Standard |
|---|---|---|---|
writeShellApplication |
nixpkgs stable | Produce claudebox script | Shellcheck at build, set -euo pipefail, runtimeInputs wiring [VERIFIED: nix eval on host] |
bubblewrap |
0.11.0 | Sandbox runtime | Unprivileged user-ns sandbox, all required flags confirmed [VERIFIED: bwrap --version on host] |
comma-with-db |
2.3.3 | On-demand package runner | Bundles nix-index database, no extra mount needed [VERIFIED: nix eval github:nix-community/nix-index-database#packages.x86_64-linux.comma-with-db.name] |
Runtime Dependencies (runtimeInputs for writeShellApplication)
| Package | Purpose | Notes |
|---|---|---|
bubblewrap |
Sandbox | 0.11.0 in nixpkgs [VERIFIED: nix eval nixpkgs#bubblewrap.version] |
coreutils |
Basic utils | env, cat, mkdir, etc. [VERIFIED: available] |
git |
VCS | Claude Code requires git [VERIFIED: available] |
curl |
HTTP | MCP + tool use [VERIFIED: works inside sandbox] |
jq |
JSON | Config manipulation [ASSUMED: standard nixpkgs] |
ripgrep |
Search | Claude Code's grep [ASSUMED: standard nixpkgs] |
fd |
File find | Claude Code's find [ASSUMED: standard nixpkgs] |
nix |
Package mgr | For nix shell inside sandbox [VERIFIED: daemon comms work] |
comma-with-db |
On-demand pkgs | From nix-index-database flake input [VERIFIED: 2.3.3] |
bash |
Shell | bwrap exec target [VERIFIED: available] |
nodejs |
Runtime | Claude Code is a Node.js app [VERIFIED: nodejs-24.13.0 in closure] |
Excluded (secrets)
| Package | Why Excluded |
|---|---|
gnupg |
Secret material |
openssh |
Secret material |
age/agenix |
Secret material |
tailscale |
Infrastructure access |
Flake Inputs
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
nix-index-database = {
url = "github:nix-community/nix-index-database";
inputs.nixpkgs.follows = "nixpkgs";
};
};
}
[VERIFIED: nix-index-database uses packages.x86_64-linux.comma-with-db output -- the legacyPackages path is deprecated]
Architecture Patterns
Recommended Project Structure
claudebox/
├── flake.nix # Flake with nixpkgs + nix-index-database inputs
├── flake.lock # Pinned dependencies
├── claudebox.sh # Shell script body (read via builtins.readFile)
├── CLAUDE.md # Project docs
└── .planning/ # GSD planning artifacts
Pattern 1: writeShellApplication with builtins.readFile
What: Keep the shell script in a separate .sh file, read it into the Nix expression.
When to use: Always -- gives shell syntax highlighting, independent shellcheck, easier iteration.
Example:
# flake.nix (simplified)
{
outputs = { self, nixpkgs, nix-index-database, ... }:
let
system = "x86_64-linux";
pkgs = nixpkgs.legacyPackages.${system};
comma-with-db = nix-index-database.packages.${system}.comma-with-db;
in {
packages.${system}.default = pkgs.writeShellApplication {
name = "claudebox";
runtimeInputs = [
pkgs.bubblewrap pkgs.coreutils pkgs.git pkgs.curl
pkgs.jq pkgs.ripgrep pkgs.fd pkgs.nix
comma-with-db pkgs.bash pkgs.nodejs
];
text = builtins.readFile ./claudebox.sh;
};
};
}
[VERIFIED: writeShellApplication API is stable in nixpkgs, runtimeInputs prepends to PATH]
Pattern 2: bwrap Invocation Structure
What: The core sandbox call with proper ordering. Mount ordering rule: tmpfs root first, then system mounts, then HOME-level mounts, then CWD (most specific last wins).
exec bwrap \
--clearenv \
# --- Sandbox-generated vars ---
--setenv HOME "$HOME" \
--setenv USER "$USER" \
--setenv PATH "$SANDBOX_PATH" \
--setenv TERM "${TERM:-xterm}" \
--setenv SHELL "/bin/bash" \
--setenv TMPDIR /tmp \
--setenv NIX_SSL_CERT_FILE /etc/ssl/certs/ca-certificates.crt \
# --- Allowlisted host vars (only if set) ---
${EDITOR:+--setenv EDITOR "$EDITOR"} \
${LANG:+--setenv LANG "$LANG"} \
${ANTHROPIC_API_KEY:+--setenv ANTHROPIC_API_KEY "$ANTHROPIC_API_KEY"} \
# ... etc for each allowlisted var ...
# --- Filesystem: base layer ---
--tmpfs / \
--proc /proc \
--dev /dev \
--tmpfs /tmp \
# --- Filesystem: system ---
--ro-bind /nix/store /nix/store \
--bind /nix/var/nix /nix/var/nix \
--ro-bind /etc/resolv.conf /etc/resolv.conf \
--ro-bind /etc/ssl /etc/ssl \
--ro-bind /etc/static /etc/static \
--ro-bind /etc/passwd /etc/passwd \
--ro-bind /etc/group /etc/group \
--ro-bind /etc/hosts /etc/hosts \
--ro-bind /etc/nsswitch.conf /etc/nsswitch.conf \
--ro-bind /etc/nix /etc/nix \
--symlink /usr/bin/env /usr/bin/env \
# --- Filesystem: user ---
--tmpfs "$HOME" \
--bind "$HOME/.claudebox" "$HOME/.claude" \
--bind "$CWD" "$CWD" \
--chdir "$CWD" \
# --- Exec ---
-- claude --dangerously-skip-permissions "$@"
[VERIFIED: tested bwrap invocations on host confirm this structure works]
Pattern 3: Environment Allowlist with CLAUDEBOX_EXTRA_ENV
What: Loop over allowlisted vars, only pass those that are set.
ALLOWLIST=(HOME PATH TERM EDITOR LANG LC_ALL NIX_SSL_CERT_FILE SSL_CERT_FILE ANTHROPIC_API_KEY USER SHELL XDG_RUNTIME_DIR)
# Build --setenv args array
SETENV_ARGS=()
for var in "${ALLOWLIST[@]}"; do
if [[ -v "$var" ]]; then
SETENV_ARGS+=(--setenv "$var" "${!var}")
fi
done
# Handle CLAUDEBOX_EXTRA_ENV
if [[ -v CLAUDEBOX_EXTRA_ENV ]]; then
IFS=',' read -ra EXTRAS <<< "$CLAUDEBOX_EXTRA_ENV"
for var in "${EXTRAS[@]}"; do
if [[ -v "$var" ]]; then
SETENV_ARGS+=(--setenv "$var" "${!var}")
fi
done
fi
[ASSUMED: bash array + indirect variable pattern is standard]
Pattern 4: Git Identity Generation
What: Read host git config, write minimal .gitconfig inside sandbox.
GIT_NAME=$(git config --global user.name 2>/dev/null || echo "Claude User")
GIT_EMAIL=$(git config --global user.email 2>/dev/null || echo "claude@localhost")
# Create temp gitconfig for the sandbox
GITCONFIG_TMP=$(mktemp)
cat > "$GITCONFIG_TMP" <<EOF
[user]
name = $GIT_NAME
email = $GIT_EMAIL
[safe]
directory = *
EOF
Then use --ro-bind "$GITCONFIG_TMP" "$HOME/.gitconfig" in the bwrap call. Clean up the tmpfile on exit with a trap.
[ASSUMED: git config reading is straightforward]
Pattern 5: Claude Code as Dependency
What: Claude Code needs to be available inside the sandbox PATH.
Key finding: The host has claude-code 2.1.70 installed via a Nix derivation at /nix/store/4960jbc91nlkdm7fbqb9p1b6gi0x2dq0-claude-code. It's a bash wrapper that execs node with cli.js. The nixpkgs version is 2.0.51 (older).
Approach: Do NOT add claude-code as a runtimeInput of writeShellApplication. Instead, accept it as a flake input or expect it on the host PATH. The script should discover claude from the host's PATH before --clearenv strips it. Capture the full path to claude at script startup: CLAUDE_BIN=$(command -v claude), then exec $CLAUDE_BIN inside bwrap.
[VERIFIED: claude binary is at a nix store path, will survive --clearenv if referenced by full path]
Anti-Patterns to Avoid
- Mounting host
~/.gitconfig: Contains credential helpers, pager, aliases referencing binaries not in sandbox. Generate a minimal one instead. - Mounting host
~/.claude: Requirement says mount~/.claudeboxAS~/.claude. Keeps sandbox state separate. - Using
--unshare-net: Phase 1 needs network access. Network isolation is Phase 2 (NET-01, NET-02). - Denylist env approach: Must use allowlist (
--clearenv+--setenv), never selectively--unsetenv.
Don't Hand-Roll
| Problem | Don't Build | Use Instead | Why |
|---|---|---|---|
| Shell script derivation | Manual mkDerivation | writeShellApplication |
Automatic shellcheck, set -euo pipefail, runtimeInputs PATH |
| Package index for comma | Manual nix-index database generation | comma-with-db from nix-index-database flake |
Self-contained, updated with flake lock |
| SSL cert detection | Custom cert-finding logic | Bind-mount /etc/ssl + /etc/static + set NIX_SSL_CERT_FILE |
NixOS cert chain is well-known, just mount the paths |
| User namespace setup | Manual uid/gid mapping | bwrap defaults | bwrap handles user namespace automatically on NixOS |
Common Pitfalls
Pitfall 1: NixOS /etc Symlink Chains
What goes wrong: SSL certs fail because /etc/ssl/certs/ca-certificates.crt symlinks to /etc/static/ssl/certs/ca-certificates.crt which symlinks to /nix/store/.... Mounting only /etc/ssl without /etc/static breaks the chain.
Why it happens: NixOS manages /etc via symlinks to /etc/static which itself symlinks to the Nix store.
How to avoid: Mount BOTH /etc/ssl and /etc/static read-only. The Nix store mount covers the final target.
Warning signs: curl: (77) error setting certificate or empty curl responses.
[VERIFIED: tested on host -- mounting /etc/ssl alone causes cat /etc/ssl/certs/ca-certificates.crt to fail; adding /etc/static fixes it]
Pitfall 2: /etc/nix/nix.conf for Experimental Features
What goes wrong: nix shell and nix eval fail with "experimental feature 'nix-command' is disabled".
Why it happens: The host's /etc/nix/nix.conf enables experimental-features = nix-command flakes. Without it, nix commands inside sandbox don't know about flakes.
How to avoid: Mount /etc/nix read-only inside the sandbox.
Warning signs: nix shell or nix eval errors about experimental features.
[VERIFIED: tested -- without /etc/nix mounted, nix eval fails with exactly this error]
Pitfall 3: Mount Ordering for CWD Under HOME
What goes wrong: CWD mount is invisible because HOME tmpfs is mounted after it.
Why it happens: bwrap processes mount arguments in order. Later mounts can shadow earlier ones.
How to avoid: Order: --tmpfs / -> --tmpfs $HOME -> --bind $CWD $CWD. Most specific mounts go last.
Warning signs: CWD appears empty inside sandbox.
[ASSUMED: standard bwrap behavior -- mounts are processed left-to-right]
Pitfall 4: PATH Inside Sandbox
What goes wrong: writeShellApplication runtimeInputs prepends to the host PATH. But --clearenv clears PATH. The script needs to capture the Nix-constructed PATH before --clearenv wipes it, and pass it into the sandbox.
Why it happens: The wrapper script runs on the host with runtimeInputs PATH. bwrap --clearenv clears everything inside.
How to avoid: Capture SANDBOX_PATH="$PATH" at script top (this is the runtimeInputs-constructed PATH). Pass it via --setenv PATH "$SANDBOX_PATH" into bwrap. Remove any non-nix-store paths if paranoid.
Warning signs: Commands not found inside sandbox.
[VERIFIED: writeShellApplication prepends runtimeInputs to PATH; --clearenv removes it]
Pitfall 5: Nix Daemon Socket Needs Write Access
What goes wrong: nix shell fails to download packages because daemon socket is mounted read-only.
Why it happens: The Unix socket requires read-write access for nix client to talk to the daemon.
How to avoid: Use --bind (rw) not --ro-bind for /nix/var/nix. The daemon also needs to write to store paths (but those go through the daemon, not the client).
Warning signs: "error connecting to daemon" or permission denied on socket.
[VERIFIED: tested with --bind /nix/var/nix /nix/var/nix -- nix eval works]
Pitfall 6: /etc/passwd and /etc/group Required
What goes wrong: Various tools (git, nix, node) fail when they can't resolve the current user.
Why it happens: They call getpwuid/getgrgid which reads /etc/passwd and /etc/group.
How to avoid: Mount /etc/passwd and /etc/group read-only.
Warning signs: "I have no name!" prompt, git errors about user identity.
[ASSUMED: standard Unix behavior, confirmed by testing that bwrap shows uid 65534 (nobody) without these mounts]
Pitfall 7: Claude Code MCP Config Injection
What goes wrong: The host's claude-code Nix derivation injects --mcp-config pointing to a Nix store path with host-specific MCP servers (e.g., charlie-comunica, charlie-memory referencing ~/agent/).
Why it happens: The host's Nix package wraps claude with hardcoded MCP paths.
How to avoid: This is actually fine for Phase 1 -- the MCP servers won't be accessible inside the sandbox (no ~/agent/ mounted) and will silently fail. Future phases might want to strip or override this. No action needed now.
[VERIFIED: checked the MCP config at /nix/store/5iv9id24chdvf39929rya0rvyjrl0p8f-claude-code-mcp-config.json -- references host paths]
Pitfall 8: /usr/bin/env Missing
What goes wrong: Scripts with #!/usr/bin/env bash shebangs fail.
Why it happens: tmpfs root has no /usr/bin/env. Many scripts and Node.js npm scripts use this shebang.
How to avoid: --symlink /usr/bin/env "$(which env)" or --symlink $(which env) /usr/bin/env. bwrap supports --symlink to create symlinks inside the sandbox.
Warning signs: "bad interpreter: /usr/bin/env: no such file or directory".
[ASSUMED: standard issue with minimal sandboxes]
Code Examples
Flake Structure
# flake.nix
{
description = "claudebox - sandboxed Claude Code";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
nix-index-database = {
url = "github:nix-community/nix-index-database";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = { self, nixpkgs, nix-index-database, ... }:
let
system = "x86_64-linux";
pkgs = nixpkgs.legacyPackages.${system};
comma-with-db = nix-index-database.packages.${system}.comma-with-db;
in {
packages.${system} = {
claudebox = pkgs.writeShellApplication {
name = "claudebox";
runtimeInputs = [
pkgs.bubblewrap
pkgs.coreutils
pkgs.git
pkgs.curl
pkgs.jq
pkgs.ripgrep
pkgs.fd
pkgs.nix
comma-with-db
pkgs.bash
pkgs.nodejs
];
text = builtins.readFile ./claudebox.sh;
};
default = self.packages.${system}.claudebox;
};
};
}
[ASSUMED: flake structure based on standard nixpkgs patterns]
Signal Handling and Exit Code
# At the end of claudebox.sh -- exec replaces the shell process
# so signals go directly to bwrap->claude, and exit code passes through
exec bwrap \
... \
-- "$CLAUDE_BIN" --dangerously-skip-permissions "$@"
[VERIFIED: exec ensures PID 1 in the script is bwrap, Ctrl+C propagates to children]
/usr/bin/env Symlink
# In the bwrap args -- coreutils provides env
--symlink "$(command -v env)" /usr/bin/env
Note: --symlink creates TARGET LINK_NAME (dest is the symlink path). The env binary is in coreutils which is in the sandbox PATH.
[ASSUMED: bwrap --symlink syntax]
State of the Art
| Old Approach | Current Approach | When Changed | Impact |
|---|---|---|---|
legacyPackages for comma-with-db |
packages output |
Recent | Must use nix-index-database.packages.${system}.comma-with-db [VERIFIED: deprecation warning on legacyPackages] |
| claude-code from npm | claude-code from nixpkgs | 2025 | Available as pkgs.claude-code but version 2.0.51 vs host's 2.1.70 [VERIFIED: nix eval] |
| bwrap 0.9.x | bwrap 0.11.0 | 2025 | Current nixpkgs has 0.11.0 [VERIFIED: nix eval + host binary] |
Assumptions Log
| # | Claim | Section | Risk if Wrong |
|---|---|---|---|
| A1 | bwrap processes mounts left-to-right, later mounts shadow earlier | Pitfalls #3 | Wrong mount ordering could hide CWD |
| A2 | /etc/passwd and /etc/group are needed for user resolution | Pitfalls #6 | Tools might fail with "no name" if omitted |
| A3 | --symlink creates symlinks inside sandbox with syntax --symlink TARGET LINKNAME |
Code Examples | /usr/bin/env shebang scripts would fail if wrong |
| A4 | jq, ripgrep, fd are standard nixpkgs packages | Standard Stack | Build would fail if package names differ |
| A5 | flake.nix structure with writeShellApplication + builtins.readFile | Code Examples | Nix build would fail if API differs |
Open Questions
-
Claude Code source: host vs flake input
- What we know: Host has claude-code 2.1.70 (custom derivation with MCP injection), nixpkgs has 2.0.51
- What's unclear: Should claudebox depend on the host's claude or bundle its own?
- Recommendation: Discover claude from host PATH at runtime (
CLAUDE_BIN=$(command -v claude)). This avoids version management and respects the host's claude-code configuration. The script should fail fast with a clear error ifclaudeis not found.
-
XDG_RUNTIME_DIR inside sandbox
- What we know: It's in the allowlist (SAND-03), typically
/run/user/1000on the host - What's unclear: Whether to bind-mount the host's XDG_RUNTIME_DIR or create a tmpfs one
- Recommendation: Set
--setenv XDG_RUNTIME_DIR /tmpinside the sandbox (D-04 says sandbox-generated). Don't mount the host's runtime dir as it may contain secret sockets.
- What we know: It's in the allowlist (SAND-03), typically
-
~/.claudeboxcreation- What we know: SAND-08 says bind-mount
~/.claudeboxas~/.claude - What's unclear: Who creates
~/.claudeboxif it doesn't exist? - Recommendation: Script should
mkdir -p ~/.claudeboxbefore bwrap invocation if it doesn't exist.
- What we know: SAND-08 says bind-mount
Environment Availability
| Dependency | Required By | Available | Version | Fallback |
|---|---|---|---|---|
| bubblewrap | Sandbox core | Yes | 0.11.0 | -- |
| nix | Package management | Yes | Lix 2.93.3 | -- |
| git | VCS operations | Yes | available on host | -- |
| curl | HTTP requests | Yes | 8.17.0 in nixpkgs | -- |
| nodejs | Claude Code runtime | Yes | 24.13.0 | -- |
| claude-code | The wrapped tool | Yes | 2.1.70 on host | nixpkgs 2.0.51 |
| comma-with-db | On-demand packages | Yes | 2.3.3 via flake | -- |
| Nix daemon socket | nix shell/comma | Yes | /nix/var/nix/daemon-socket/socket | -- |
Missing dependencies with no fallback: None.
Project Constraints (from CLAUDE.md)
- Stack: Nix derivation + shell script only. No Docker, systemd, or external dependencies beyond nixpkgs.
- Sandbox: Own bwrap call. Not delegating to Claude Code's
--sandboxor Nix's build sandbox. - Env model: Allowlist, not denylist. Start empty, add explicitly.
- Commits: Conventional commits, minimal/succinct messages.
- NixOS: Changes go through the flake.
Sources
Primary (HIGH confidence)
- Host bubblewrap 0.11.0 --
bwrap --version,bwrap --help, live sandbox tests - Host Nix/Lix 2.93.3 --
nix --version,nix evalcommands - nixpkgs bubblewrap --
nix eval nixpkgs#bubblewrap.version= "0.11.0" - nix-index-database flake --
nix eval+nix flake showconfirmedpackages.x86_64-linux.comma-with-db(2.3.3) - Claude Code binary inspection -- wrapper chain confirmed: bash -> bash (env setup) -> node cli.js
- NixOS /etc structure -- live inspection of symlink chains for resolv.conf, ssl, hosts, nsswitch.conf
- Live sandbox tests -- confirmed: clearenv, tmpfs root, nix store mount, daemon socket, DNS resolution, SSL (with /etc/static)
Secondary (MEDIUM confidence)
- Host
/etc/nix/nix.conf-- confirmed experimental-features setting needed inside sandbox - Host
~/.claude/directory -- confirmed .credentials.json, config/, history.jsonl structure
Tertiary (LOW confidence)
- bwrap
--symlinksyntax -- from training data, not tested in this session
Metadata
Confidence breakdown:
- Standard stack: HIGH -- all packages verified in nixpkgs and on host
- Architecture: HIGH -- core patterns verified with live sandbox tests
- Pitfalls: HIGH -- most pitfalls discovered and verified through testing
- Flake structure: MEDIUM -- writeShellApplication API assumed from training, not doc-verified
Research date: 2026-04-09 Valid until: 2026-05-09 (stable tools, 30-day window)