380 lines
15 KiB
Markdown
380 lines
15 KiB
Markdown
---
|
|
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"
|
|
---
|
|
|
|
<objective>
|
|
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.
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
|
@$HOME/.claude/get-shit-done/templates/summary.md
|
|
</execution_context>
|
|
|
|
<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
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: Create flake.nix with all inputs and writeShellApplication</name>
|
|
<files>flake.nix</files>
|
|
<read_first>
|
|
.planning/phases/01-minimal-viable-sandbox/01-RESEARCH.md
|
|
</read_first>
|
|
<action>
|
|
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).
|
|
</action>
|
|
<verify>
|
|
<automated>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"</automated>
|
|
</verify>
|
|
<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>
|
|
<done>flake.nix exists with correct flake structure, all runtime dependencies, and readFile of claudebox.sh</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: Create claudebox.sh with complete bwrap sandbox logic</name>
|
|
<files>claudebox.sh</files>
|
|
<read_first>
|
|
.planning/phases/01-minimal-viable-sandbox/01-RESEARCH.md
|
|
.planning/phases/01-minimal-viable-sandbox/01-CONTEXT.md
|
|
</read_first>
|
|
<action>
|
|
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" <<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:
|
|
```bash
|
|
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:
|
|
```bash
|
|
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):
|
|
```bash
|
|
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)**
|
|
|
|
```bash
|
|
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.
|
|
</action>
|
|
<verify>
|
|
<automated>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"</automated>
|
|
</verify>
|
|
<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>
|
|
<done>claudebox.sh exists with complete bwrap invocation covering all SAND-*, TOOL-*, GIT-*, and UX-06 requirements</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<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>
|
|
|
|
<verification>
|
|
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
|
|
</verification>
|
|
|
|
<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>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/01-minimal-viable-sandbox/01-01-SUMMARY.md`
|
|
</output>
|