- Replace --bind ~/.claudebox + --symlink with direct --bind ~/.claude ~/.claude - Add compute_canonical_root() function using git rev-parse --git-common-dir - Add per-project INSTANCE_DIR via sha256sum[:16] of canonical git root - Overlay projects/ with per-project hash dir for isolated conversation history - Overlay history.jsonl and SANDBOX.md as file-level bind mounts - Update credential mount target from ~/.claudebox to ~/.claude - Add CLAUDE_JSON_FILE (~/.claude.json) detection and conditional bind mount - Remove stale CLAUDE.md injection logic (D-06: user's real CLAUDE.md used) - Update dry-run block and print_audit to reflect new mount layout - Update SANDBOX.md heredoc to remove ~/.claudebox reference
465 lines
24 KiB
Markdown
465 lines
24 KiB
Markdown
# 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-permissions` is always injected.
|
|
- **D-02:** Use `comma-with-db` from the `nix-community/nix-index-database` flake. 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_ENV` escape 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 via `CLAUDEBOX_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 `.gitconfig` inside the sandbox at launch time. Reads `user.name` and `user.email` from the host's git config, writes them plus `safe.directory = *` into the sandbox's `~/.gitconfig`. No host `.gitconfig` mounted.
|
|
|
|
### Claude's Discretion
|
|
- Mount ordering strategy for CWD-under-HOME (bwrap specifics)
|
|
- Exact tmpfs layout and /dev, /proc, /tmp setup
|
|
- How `--clearenv` + `--setenv` are 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
|
|
```nix
|
|
{
|
|
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:**
|
|
```nix
|
|
# 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).
|
|
|
|
```bash
|
|
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.
|
|
```bash
|
|
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.
|
|
```bash
|
|
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 `~/.claudebox` AS `~/.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
|
|
```nix
|
|
# 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
|
|
```bash
|
|
# 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
|
|
```bash
|
|
# 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 (RESOLVED)
|
|
|
|
1. **Claude Code source: host vs flake input**
|
|
- RESOLVED: 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 fails fast with a clear error if `claude` is not found.
|
|
|
|
2. **XDG_RUNTIME_DIR inside sandbox**
|
|
- RESOLVED: Set `--setenv XDG_RUNTIME_DIR /tmp` inside the sandbox (D-04 says sandbox-generated). Don't mount the host's runtime dir as it may contain secret sockets.
|
|
|
|
3. **`~/.claudebox` creation**
|
|
- RESOLVED: Script does `mkdir -p ~/.claudebox` before bwrap invocation if it doesn't exist.
|
|
|
|
## 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 `--sandbox` or 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 eval` commands
|
|
- nixpkgs bubblewrap -- `nix eval nixpkgs#bubblewrap.version` = "0.11.0"
|
|
- nix-index-database flake -- `nix eval` + `nix flake show` confirmed `packages.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 `--symlink` syntax -- 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)
|