---
phase: 01-minimal-viable-sandbox
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- flake.nix
- claudebox.sh
autonomous: true
requirements:
- 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
must_haves:
truths:
- "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"
artifacts:
- path: "flake.nix"
provides: "Nix flake with claudebox as default package"
contains: "writeShellApplication"
- path: "claudebox.sh"
provides: "Shell script body with bwrap sandbox invocation"
contains: "exec bwrap"
key_links:
- from: "flake.nix"
to: "claudebox.sh"
via: "builtins.readFile ./claudebox.sh"
pattern: "builtins.readFile ./claudebox.sh"
- from: "claudebox.sh"
to: "bwrap"
via: "exec bwrap with --clearenv"
pattern: "exec bwrap.*--clearenv"
- from: "claudebox.sh"
to: "claude"
via: "CLAUDE_BIN resolved from host PATH before clearenv"
pattern: "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.
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
@.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:
```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;
};
};
}
```
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"
- 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`
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)**
```bash
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:
```bash
SANDBOX_PATH="$PATH"
```
**Section 3: Record CWD**
```bash
CWD=$(pwd)
```
**Section 4: Ensure ~/.claudebox exists**
```bash
mkdir -p "$HOME/.claudebox"
```
**Section 5: Generate minimal .gitconfig (per D-05)**
Read host git identity, write a temp .gitconfig, set up cleanup trap:
```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")
GITCONFIG_TMP=$(mktemp)
trap 'rm -f "$GITCONFIG_TMP"' EXIT
cat > "$GITCONFIG_TMP" <
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"
- 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
claudebox.sh exists with complete bwrap invocation covering all SAND-*, TOOL-*, GIT-*, and UX-06 requirements
## 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. |
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
- 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