15 KiB
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 01-minimal-viable-sandbox | 01 | execute | 1 |
|
true |
|
|
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-dbcomes from thenix-index-databaseflake, usingpackages.${system}.comma-with-db(not legacyPackages). - Per NIX-01/NIX-02: Flake with pinned inputs.
nixpkgs.followsensures single nixpkgs instance. - Per NIX-03:
defaultpackage alias sonix runandnix profile installwork. - Per SAND-01/SAND-10:
writeShellApplicationproduces 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
- flake.nix contains
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):
--tmpfs /-- empty root (SAND-04)/proc,/dev,/tmp-- system essentials (SAND-11)/nix/storero,/nix/var/nixrw -- Nix access (SAND-06, SAND-07, TOOL-03)/etc/*-- DNS, SSL, user resolution, nix config (SAND-12, SAND-13)/usr/bin/envsymlink -- shebang support (Pitfall 8)$HOMEtmpfs -- clean home dir~/.claudeboxas~/.claude-- Claude config (SAND-08).gitconfig-- git identity (GIT-01, GIT-02)$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> |
<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>