claudebox/.planning/phases/01-minimal-viable-sandbox/01-01-PLAN.md
Christopher Mühl 71790d714b
docs(01): create phase plan
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-09 11:02:11 +02:00

15 KiB

phase plan type wave depends_on files_modified autonomous requirements must_haves
01-minimal-viable-sandbox 01 execute 1
flake.nix
claudebox.sh
true
SAND-01
SAND-02
SAND-03
SAND-04
SAND-05
SAND-06
SAND-07
SAND-08
SAND-09
SAND-10
SAND-11
SAND-12
SAND-13
SAND-14
SAND-15
TOOL-01
TOOL-02
TOOL-03
GIT-01
GIT-02
NIX-01
NIX-02
NIX-03
UX-06
truths artifacts key_links
Running `nix build` in the project root produces a working `claudebox` binary
claudebox launches Claude Code inside bwrap with only allowlisted env vars
Secret paths (~/.ssh, ~/.gnupg, ~/.aws, etc.) are not visible inside the sandbox
Git works inside the sandbox with user identity from host
comma and nix shell work inside the sandbox for on-demand tool installation
Ctrl+C terminates the session cleanly; exit code passes through
path provides contains
flake.nix Nix flake with claudebox as default package writeShellApplication
path provides contains
claudebox.sh Shell script body with bwrap sandbox invocation exec bwrap
from to via pattern
flake.nix claudebox.sh builtins.readFile ./claudebox.sh builtins.readFile ./claudebox.sh
from to via pattern
claudebox.sh bwrap exec bwrap with --clearenv exec bwrap.*--clearenv
from to via pattern
claudebox.sh claude CLAUDE_BIN resolved from host PATH before clearenv CLAUDE_BIN=.*command -v claude
Create the complete claudebox Nix flake and shell script that launches Claude Code inside a bubblewrap sandbox with full environment isolation, filesystem isolation, secret hiding, git support, and tool provisioning.

Purpose: This is the entire deliverable for Phase 1 -- a working claudebox command. Output: flake.nix and claudebox.sh in the project root.

<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/01-minimal-viable-sandbox/01-CONTEXT.md @.planning/phases/01-minimal-viable-sandbox/01-RESEARCH.md Task 1: Create flake.nix with all inputs and writeShellApplication flake.nix .planning/phases/01-minimal-viable-sandbox/01-RESEARCH.md Create `flake.nix` in the project root with the following exact structure:
{
  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;
      };
    };
}

Key points per user decisions:

  • Per D-02: comma-with-db comes from the nix-index-database flake, using packages.${system}.comma-with-db (not legacyPackages).
  • Per NIX-01/NIX-02: Flake with pinned inputs. nixpkgs.follows ensures single nixpkgs instance.
  • Per NIX-03: default package alias so nix run and nix profile install work.
  • Per SAND-01/SAND-10: writeShellApplication produces the binary and wires runtimeInputs into PATH.
  • Claude Code is NOT in runtimeInputs -- it's discovered from host PATH at runtime (see Research Pattern 5). grep -q 'writeShellApplication' flake.nix && grep -q 'comma-with-db' flake.nix && grep -q 'nix-index-database' flake.nix && grep -q 'builtins.readFile ./claudebox.sh' flake.nix && echo "PASS" || echo "FAIL" <acceptance_criteria>
    • flake.nix contains description = "claudebox - sandboxed Claude Code"
    • flake.nix contains nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"
    • flake.nix contains nix-index-database.packages.${system}.comma-with-db
    • flake.nix contains name = "claudebox"
    • flake.nix contains builtins.readFile ./claudebox.sh
    • flake.nix contains all 11 runtimeInputs: bubblewrap, coreutils, git, curl, jq, ripgrep, fd, nix, comma-with-db, bash, nodejs
    • flake.nix contains default = self.packages.${system}.claudebox </acceptance_criteria> flake.nix exists with correct flake structure, all runtime dependencies, and readFile of claudebox.sh
Task 2: Create claudebox.sh with complete bwrap sandbox logic claudebox.sh .planning/phases/01-minimal-viable-sandbox/01-RESEARCH.md .planning/phases/01-minimal-viable-sandbox/01-CONTEXT.md Create `claudebox.sh` in the project root. This is the shell script body read by `writeShellApplication` (which adds `set -euo pipefail` and prepends runtimeInputs to PATH automatically). The script must implement the following sections in order:

Section 1: Resolve claude binary from host PATH (before clearenv strips it)

CLAUDE_BIN=$(command -v claude) || {
  echo "error: claude not found in PATH" >&2
  echo "Install Claude Code first: https://docs.anthropic.com/en/docs/claude-code" >&2
  exit 1
}

Section 2: Capture sandbox PATH

The runtimeInputs-constructed PATH is available as $PATH at this point. Capture it for passing into the sandbox:

SANDBOX_PATH="$PATH"

Section 3: Record CWD

CWD=$(pwd)

Section 4: Ensure ~/.claudebox exists

mkdir -p "$HOME/.claudebox"

Section 5: Generate minimal .gitconfig (per D-05)

Read host git identity, write a temp .gitconfig, set up cleanup trap:

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")

GITCONFIG_TMP=$(mktemp)
trap 'rm -f "$GITCONFIG_TMP"' EXIT

cat > "$GITCONFIG_TMP" <<GITEOF
[user]
    name = $GIT_NAME
    email = $GIT_EMAIL
[safe]
    directory = *
GITEOF

Section 6: Build environment --setenv args array (per D-03, D-04, SAND-02, SAND-03)

Sandbox-generated vars (D-04) are set directly, never from host:

ENV_ARGS=(
  --setenv HOME "$HOME"
  --setenv USER "$USER"
  --setenv PATH "$SANDBOX_PATH"
  --setenv SHELL /bin/bash
  --setenv TMPDIR /tmp
  --setenv XDG_RUNTIME_DIR /tmp
  --setenv NIX_SSL_CERT_FILE /etc/ssl/certs/ca-certificates.crt
  --setenv SSL_CERT_FILE /etc/ssl/certs/ca-certificates.crt
)

Allowlisted host vars -- only pass if set on host:

HOST_ALLOWLIST=(TERM EDITOR LANG LC_ALL ANTHROPIC_API_KEY)
for var in "${HOST_ALLOWLIST[@]}"; do
  if [[ -v "$var" ]]; then
    ENV_ARGS+=(--setenv "$var" "${!var}")
  fi
done

CLAUDEBOX_EXTRA_ENV escape hatch (per D-03, comma-separated):

if [[ -v CLAUDEBOX_EXTRA_ENV ]]; then
  IFS=',' read -ra EXTRAS <<< "$CLAUDEBOX_EXTRA_ENV"
  for var in "${EXTRAS[@]}"; do
    var="${var// /}"  # trim whitespace
    if [[ -n "$var" ]] && [[ -v "$var" ]]; then
      ENV_ARGS+=(--setenv "$var" "${!var}")
    fi
  done
fi

Section 7: exec bwrap (per SAND-04 through SAND-15, UX-06, D-01)

exec bwrap \
  --clearenv \
  "${ENV_ARGS[@]}" \
  --tmpfs / \
  --proc /proc \
  --dev /dev \
  --tmpfs /tmp \
  --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 "$(command -v env)" /usr/bin/env \
  --tmpfs "$HOME" \
  --bind "$HOME/.claudebox" "$HOME/.claude" \
  --ro-bind "$GITCONFIG_TMP" "$HOME/.gitconfig" \
  --bind "$CWD" "$CWD" \
  --chdir "$CWD" \
  -- "$CLAUDE_BIN" --dangerously-skip-permissions "$@"

Mount ordering rationale (most general to most specific):

  1. --tmpfs / -- empty root (SAND-04)
  2. /proc, /dev, /tmp -- system essentials (SAND-11)
  3. /nix/store ro, /nix/var/nix rw -- Nix access (SAND-06, SAND-07, TOOL-03)
  4. /etc/* -- DNS, SSL, user resolution, nix config (SAND-12, SAND-13)
  5. /usr/bin/env symlink -- shebang support (Pitfall 8)
  6. $HOME tmpfs -- clean home dir
  7. ~/.claudebox as ~/.claude -- Claude config (SAND-08)
  8. .gitconfig -- git identity (GIT-01, GIT-02)
  9. $CWD -- project directory (SAND-05), most specific = last

Per D-01: all args after claudebox's own flags pass through via "$@". Phase 1 has no claudebox-specific flags (--yes/--dry-run/--check are Phase 2), so ALL args pass through. Per UX-06: --dangerously-skip-permissions is always injected before "$@". Per SAND-14/SAND-15: exec ensures no intermediate shell -- signals propagate, exit code passes through. Per SAND-09: Secret paths are never mounted. The tmpfs root and tmpfs HOME ensure nothing leaks. Only explicit bind-mounts above are visible.

IMPORTANT: Do NOT add #!/bin/bash or set -euo pipefail -- writeShellApplication adds these automatically. grep -q 'exec bwrap' claudebox.sh && grep -q 'clearenv' claudebox.sh && grep -q 'CLAUDE_BIN' claudebox.sh && grep -q 'CLAUDEBOX_EXTRA_ENV' claudebox.sh && grep -q 'GITCONFIG_TMP' claudebox.sh && grep -q 'dangerously-skip-permissions' claudebox.sh && echo "PASS" || echo "FAIL" <acceptance_criteria> - claudebox.sh does NOT contain #!/bin/bash or set -euo pipefail (writeShellApplication adds these) - claudebox.sh contains CLAUDE_BIN=$(command -v claude) with error handling on failure - claudebox.sh contains SANDBOX_PATH="$PATH" - claudebox.sh contains mkdir -p "$HOME/.claudebox" - claudebox.sh contains git config reading: git config --global user.name and git config --global user.email - claudebox.sh contains GITCONFIG_TMP=$(mktemp) and trap 'rm -f "$GITCONFIG_TMP"' EXIT - claudebox.sh contains safe.directory = * in the generated gitconfig - claudebox.sh contains ENV_ARGS array with --setenv for HOME, USER, PATH, SHELL, TMPDIR, XDG_RUNTIME_DIR, NIX_SSL_CERT_FILE, SSL_CERT_FILE - claudebox.sh contains HOST_ALLOWLIST with TERM, EDITOR, LANG, LC_ALL, ANTHROPIC_API_KEY - claudebox.sh contains CLAUDEBOX_EXTRA_ENV parsing with IFS=',' read -ra EXTRAS - claudebox.sh contains exec bwrap with --clearenv - claudebox.sh contains --tmpfs / before any other mount - claudebox.sh contains --ro-bind /nix/store /nix/store - claudebox.sh contains --bind /nix/var/nix /nix/var/nix (rw, not ro-bind) - claudebox.sh contains --ro-bind /etc/resolv.conf /etc/resolv.conf - claudebox.sh contains --ro-bind /etc/ssl /etc/ssl AND --ro-bind /etc/static /etc/static - claudebox.sh contains --ro-bind /etc/nix /etc/nix - claudebox.sh contains --ro-bind /etc/passwd /etc/passwd and --ro-bind /etc/group /etc/group - claudebox.sh contains --symlink for /usr/bin/env - claudebox.sh contains --tmpfs "$HOME" BEFORE the claudebox and CWD bind mounts - claudebox.sh contains --bind "$HOME/.claudebox" "$HOME/.claude" - claudebox.sh contains --ro-bind "$GITCONFIG_TMP" "$HOME/.gitconfig" - claudebox.sh contains --bind "$CWD" "$CWD" and --chdir "$CWD" - claudebox.sh contains -- "$CLAUDE_BIN" --dangerously-skip-permissions "$@" at the end - claudebox.sh does NOT contain any mount of ~/.ssh, ~/.gnupg, ~/.aws, ~/.config/gcloud, or /var/lib/tailscale </acceptance_criteria> claudebox.sh exists with complete bwrap invocation covering all SAND-, TOOL-, GIT-*, and UX-06 requirements

<threat_model>

Trust Boundaries

Boundary Description
Host -> Sandbox Environment variables cross from untrusted host env into sandbox via allowlist
Host -> Sandbox Filesystem paths cross via explicit bind mounts
Sandbox -> Host CWD is mounted read-write, so Claude can modify project files (intended)

STRIDE Threat Register

Threat ID Category Component Disposition Mitigation Plan
T-01-01 Information Disclosure Environment variables mitigate --clearenv + explicit allowlist. Only SAND-03 vars pass through. CLAUDEBOX_EXTRA_ENV is user-opt-in.
T-01-02 Information Disclosure Secret filesystem paths mitigate tmpfs root + tmpfs HOME. Only explicit bind mounts visible. ~/.ssh, ~/.gnupg, ~/.aws never mounted. Verify by absence in mount list.
T-01-03 Tampering Host filesystem via CWD mount accept CWD is intentionally rw -- Claude needs to edit project files. Scope is limited to the single directory.
T-01-04 Information Disclosure Git credential helpers mitigate Host ~/.gitconfig is NOT mounted. A minimal generated .gitconfig with only user.name, user.email, and safe.directory is used instead. No credential helper config enters sandbox.
T-01-05 Elevation of Privilege Nix daemon socket access accept Daemon socket is rw to allow nix shell. The daemon runs as root on host but nix client operations are normal user operations. Nix daemon has its own access controls.
T-01-06 Information Disclosure /etc/passwd, /etc/group accept Read-only mount of user database. Contains usernames/UIDs only, no password hashes (those are in /etc/shadow which is not mounted). Required for basic tool functionality.
T-01-07 Spoofing CLAUDE_BIN resolution accept claude binary is resolved from host PATH before sandbox. If attacker controls host PATH, they already have full host access. Not a sandbox boundary issue.
</threat_model>
After both tasks complete: 1. `nix flake check` passes (or at least doesn't error on flake structure) 2. `grep -c 'exec bwrap' claudebox.sh` returns 1 3. `grep -c 'clearenv' claudebox.sh` returns 1 4. No secret paths appear in claudebox.sh mounts: `grep -E '\.ssh|\.gnupg|\.aws|gcloud|tailscale' claudebox.sh` returns nothing

<success_criteria>

  • flake.nix and claudebox.sh exist in project root
  • flake.nix defines claudebox as default package with all 11 runtimeInputs
  • claudebox.sh implements complete bwrap sandbox with env allowlist, filesystem isolation, git identity, and tool provisioning
  • All 24 phase requirements (SAND-01 through SAND-15, TOOL-01 through TOOL-03, GIT-01, GIT-02, NIX-01 through NIX-03, UX-06) are addressed </success_criteria>
After completion, create `.planning/phases/01-minimal-viable-sandbox/01-01-SUMMARY.md`