--- phase: 05-per-project-instance-isolation plan: 01 type: execute wave: 1 depends_on: [] files_modified: - claudebox.sh autonomous: false requirements: - INST-01 - INST-02 - INST-03 must_haves: truths: - "Launching claudebox in project A produces conversation history isolated from project B" - "Launching claudebox from a git worktree shares instance state with the main worktree" - "All Claude Code plugins, skills, hooks, MCP configs, commands, and settings are visible inside the sandbox" - "Two concurrent sessions in the same project share the same instance dir without corruption" artifacts: - path: "claudebox.sh" provides: "compute_canonical_root function, instance initialization, new mount layout" contains: "compute_canonical_root" - path: "claudebox.sh" provides: "BWRAP_ARGS with direct ~/.claude bind and overlay mounts" contains: "--bind \"$HOME/.claude\" \"$HOME/.claude\"" key_links: - from: "claudebox.sh (compute_canonical_root)" to: "INSTANCE_DIR variable" via: "sha256sum of canonical root path" pattern: "sha256sum.*cut -c1-16" - from: "claudebox.sh (BWRAP_ARGS)" to: "bwrap execution" via: "overlay mounts after parent bind" pattern: "--bind.*\\.claude/projects" --- Rewrite claudebox mount architecture from symlink-based to direct bind + overlay, and implement per-project instance isolation. Purpose: Fix plugin/skill/hook visibility (all Claude Code config in ~/.claude becomes available) and scope conversation history per project directory so different projects never share state. Output: Updated claudebox.sh with new mount layout, canonical root computation, per-project hash directories, and updated dry-run/audit display. @/home/toph/code/tools/claudebox/.claude/get-shit-done/workflows/execute-plan.md @/home/toph/code/tools/claudebox/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/05-per-project-instance-isolation/05-CONTEXT.md @.planning/phases/05-per-project-instance-isolation/05-RESEARCH.md From claudebox.sh (flag parsing, lines 1-18): ```bash SKIP_AUDIT=false DRY_RUN=false CHECK_MODE=false SHELL_MODE=false CLAUDE_ARGS=() while (( $# > 0 )); do case "$1" in --yes|-y) SKIP_AUDIT=true ;; --dry-run) DRY_RUN=true ;; --check) CHECK_MODE=true ;; --shell) SHELL_MODE=true ;; --) shift; CLAUDE_ARGS+=("$@"); break ;; *) CLAUDE_ARGS+=("$1") ;; esac shift done ``` From claudebox.sh (BWRAP_ARGS, lines 364-398 — MUST BE REPLACED): ```bash BWRAP_ARGS=( --clearenv "${ENV_ARGS[@]}" --tmpfs / --proc /proc --dev /dev --tmpfs /tmp --ro-bind /nix/store /nix/store --bind /nix/var/nix /nix/var/nix ... --tmpfs "$HOME" --bind "$HOME/.claudebox" "$HOME/.claudebox" # OLD: remove --symlink "$HOME/.claudebox" "$HOME/.claude" # OLD: remove ) ``` From claudebox.sh (credential mount, lines 104-122): ```bash CREDS_FILE="$HOME/.claudebox/.credentials.json" CLAUDE_JSON_FILE="$HOME/.claude.json" ``` Task 1: Rewrite mount architecture and add per-project isolation claudebox.sh - claudebox.sh (entire file — current mount layout, credential logic, SANDBOX.md generation, CLAUDE.md injection, audit display, dry-run block, BWRAP_ARGS) - .planning/phases/05-per-project-instance-isolation/05-RESEARCH.md (verified patterns, mount order, anti-patterns) - .planning/phases/05-per-project-instance-isolation/05-CONTEXT.md (locked decisions D-01 through D-14) Modify claudebox.sh with these changes, in order: **1. Add `compute_canonical_root` function** (insert after `CWD=$(pwd)` on line 99): ```bash # Compute canonical project root — worktree-aware (D-08, INST-02) compute_canonical_root() { local cwd="$1" local git_common git_common=$(git -C "$cwd" rev-parse --git-common-dir 2>/dev/null) || { echo "$cwd" return } # git returns relative ".git" for normal repos; make absolute if [[ "$git_common" != /* ]]; then git_common="$cwd/$git_common" fi dirname "$(readlink -f "$git_common")" } ``` **2. Add instance initialization** (insert after the compute_canonical_root function): ```bash # Per-project instance isolation (D-04, D-07, D-09, D-10, INST-01) CANONICAL_ROOT=$(compute_canonical_root "$CWD") INSTANCE_HASH=$(printf '%s' "$CANONICAL_ROOT" | sha256sum | cut -c1-16) INSTANCE_DIR="$HOME/.claudebox/projects/$INSTANCE_HASH" mkdir -p "$INSTANCE_DIR" if [[ ! -f "$INSTANCE_DIR/project-root" ]]; then printf '%s\n' "$CANONICAL_ROOT" > "$INSTANCE_DIR/project-root" fi # Ensure history.jsonl source exists — bwrap bind requires source to exist (D-04) touch "$HOME/.claudebox/history.jsonl" ``` **3. Remove CLAUDE.md injection logic** (delete lines 166-174, the block starting with `# Ensure CLAUDE.md has @SANDBOX.md import`): Delete the entire block: ```bash CLAUDEMD="$HOME/.claudebox/CLAUDE.md" if [[ ! -f "$CLAUDEMD" ]]; then ... fi ``` Per D-06: user's real `~/.claude/CLAUDE.md` already has `@SANDBOX.md`. The SANDBOX.md file itself is still written to `~/.claudebox/SANDBOX.md` and will be bind-mounted as an overlay. The CLAUDE.md injection is no longer needed and would write to an invisible path. **4. Replace BWRAP_ARGS mount section** (lines 382-384). Replace these three lines: ```bash --bind "$HOME/.claudebox" "$HOME/.claudebox" --symlink "$HOME/.claudebox" "$HOME/.claude" ``` With (per D-01, D-02, D-03, D-06): ```bash # Phase 5: direct ~/.claude bind (D-01) — all plugins/skills/hooks/MCP visible --bind "$HOME/.claude" "$HOME/.claude" # Phase 5: overlay projects/ with per-project isolated dir (D-02, INST-01) --bind "$INSTANCE_DIR" "$HOME/.claude/projects" # Phase 5: overlay history.jsonl with sandbox-side file (D-03) --bind "$HOME/.claudebox/history.jsonl" "$HOME/.claude/history.jsonl" # Phase 5: inject SANDBOX.md as file overlay (D-06) --bind "$HOME/.claudebox/SANDBOX.md" "$HOME/.claude/SANDBOX.md" ``` **5. Update credential mount target** (in the conditional block after BWRAP_ARGS, currently line ~390): Change: ```bash BWRAP_ARGS+=(--bind "$CREDS_FILE" "$HOME/.claudebox/.credentials.json") ``` To: ```bash BWRAP_ARGS+=(--bind "$CREDS_FILE" "$HOME/.claude/.credentials.json") ``` Per Pitfall 3 in RESEARCH.md: the old target path `~/.claudebox/.credentials.json` no longer exists in the sandbox since `~/.claudebox` is no longer mounted. **6. Update dry-run block** (lines 318-361). Replace lines 348-355 (the mount echo lines for claudebox bind, symlink, and credential target): Replace: ```bash echo " --bind $HOME/.claudebox $HOME/.claudebox \\" echo " --symlink $HOME/.claudebox $HOME/.claude \\" ... if [[ "$CREDS_MOUNT" == true ]]; then echo " --bind $CREDS_FILE $HOME/.claudebox/.credentials.json \\" fi ``` With: ```bash echo " --bind $HOME/.claude $HOME/.claude \\" echo " --bind $INSTANCE_DIR $HOME/.claude/projects \\" echo " --bind $HOME/.claudebox/history.jsonl $HOME/.claude/history.jsonl \\" echo " --bind $HOME/.claudebox/SANDBOX.md $HOME/.claude/SANDBOX.md \\" if [[ "$CREDS_MOUNT" == true ]]; then echo " --bind $CREDS_FILE $HOME/.claude/.credentials.json \\" fi ``` **7. Update print_audit mounts section** (inside print_audit function, around line 276-283). Replace mount display lines: Replace: ```bash printf ' %-12s %s (read-write)\n' "$HOME/.claude" "$HOME/.claudebox" >&2 ``` With: ```bash printf ' %-12s %s (read-write)\n' "$HOME/.claude" "$HOME/.claude" >&2 printf ' %-12s %s (read-write, project: %s)\n' "projects/" "$INSTANCE_DIR" "$CANONICAL_ROOT" >&2 printf ' %-12s %s (read-write)\n' "history" "$HOME/.claudebox/history.jsonl" >&2 printf ' %-12s %s (read-only overlay)\n' "SANDBOX.md" "$HOME/.claudebox/SANDBOX.md" >&2 ``` **8. Update SANDBOX.md content** (the heredoc starting at line 127). Change the line: ``` Both ~/.claude and ~/.claudebox point to the same directory inside the sandbox. ``` To: ``` Your filesystem is isolated -- only the current working directory and essential system paths are mounted. Your ~/.claude directory is bind-mounted from the host, with per-project isolation for conversation history. ``` (Remove the `~/.claudebox` reference since it's no longer visible in the sandbox.) bash -n claudebox.sh && grep -q 'compute_canonical_root' claudebox.sh && grep -q 'INSTANCE_HASH' claudebox.sh && grep -q -- '--bind "$HOME/.claude" "$HOME/.claude"' claudebox.sh && grep -q -- '--bind "$INSTANCE_DIR" "$HOME/.claude/projects"' claudebox.sh && grep -q -- '--bind "$HOME/.claudebox/history.jsonl" "$HOME/.claude/history.jsonl"' claudebox.sh && grep -q -- '--bind "$CREDS_FILE" "$HOME/.claude/.credentials.json"' claudebox.sh && ! grep -q -- '--symlink.*\.claudebox.*\.claude' claudebox.sh && ! grep -q -- '--bind "$HOME/.claudebox" "$HOME/.claudebox"' claudebox.sh && echo "ALL CHECKS PASSED" - claudebox.sh passes `bash -n` syntax check - claudebox.sh contains `compute_canonical_root()` function with `git rev-parse --git-common-dir` inside it - claudebox.sh contains `INSTANCE_HASH=$(printf '%s' "$CANONICAL_ROOT" | sha256sum | cut -c1-16)` - claudebox.sh contains `mkdir -p "$INSTANCE_DIR"` - claudebox.sh contains `--bind "$HOME/.claude" "$HOME/.claude"` (D-01 direct bind) - claudebox.sh contains `--bind "$INSTANCE_DIR" "$HOME/.claude/projects"` (D-02 overlay) - claudebox.sh contains `--bind "$HOME/.claudebox/history.jsonl" "$HOME/.claude/history.jsonl"` (D-03 overlay) - claudebox.sh contains `--bind "$HOME/.claudebox/SANDBOX.md" "$HOME/.claude/SANDBOX.md"` (D-06 overlay) - claudebox.sh contains `--bind "$CREDS_FILE" "$HOME/.claude/.credentials.json"` (updated target) - claudebox.sh does NOT contain `--symlink "$HOME/.claudebox" "$HOME/.claude"` (old symlink removed) - claudebox.sh does NOT contain `--bind "$HOME/.claudebox" "$HOME/.claudebox"` (old bind removed) - claudebox.sh does NOT contain `CLAUDEMD="$HOME/.claudebox/CLAUDE.md"` (injection removed) - Dry-run block echoes `--bind $HOME/.claude $HOME/.claude` instead of old claudebox bind+symlink - Dry-run block echoes `--bind $INSTANCE_DIR $HOME/.claude/projects` - print_audit shows `projects/` mount line with `$INSTANCE_DIR` and `$CANONICAL_ROOT` - SANDBOX.md heredoc does NOT contain `~/.claudebox` claudebox.sh has new mount architecture (direct ~/.claude bind + overlays for projects/, history.jsonl, SANDBOX.md, credentials), per-project instance isolation via SHA-256 hash of canonical git root, and all display/dry-run blocks updated to match. Old symlink approach completely removed. Task 2: Verify mount architecture and per-project isolation claudebox.sh Human verifies the mount architecture rewrite works correctly end-to-end. Complete mount architecture rewrite: direct ~/.claude bind with per-project overlay isolation. Dry-run and audit display updated. 1. Run `claudebox --dry-run` from this repo — verify output shows `--bind $HOME/.claude $HOME/.claude` followed by `--bind $HOME/.claude/projects` (no `--symlink`, no `--bind ~/.claudebox ~/.claudebox`) 2. Run `claudebox --dry-run` from a different project dir — verify the INSTANCE_DIR path differs (different hash) 3. Run `ls ~/.claudebox/projects/` — verify a hash-named directory was created with a `project-root` file inside it 4. Run `cat ~/.claudebox/projects/*/project-root` — verify it contains the canonical project root path 5. Run `claudebox --yes` briefly (Ctrl+C after launch) — verify Claude Code starts and plugins/skills/hooks are visible (check with `ls ~/.claude/` inside sandbox if using --shell mode) Human confirms all 5 verification steps pass Mount architecture rewrite verified: direct ~/.claude bind works, per-project overlay produces isolated hash dirs, dry-run output is correct, Claude Code launches with plugins visible. Type "approved" or describe issues ## Trust Boundaries | Boundary | Description | |----------|-------------| | host filesystem to sandbox | bwrap bind mounts expose host paths inside sandbox; overlay order determines what's visible | | user input (CWD) to hash computation | CWD is user-controlled; used as input to SHA-256 hash for instance dir path | | project-root file to GC deletion | plaintext path stored in project-root file is trusted by GC to determine what to delete | ## STRIDE Threat Register | Threat ID | Category | Component | Disposition | Mitigation Plan | |-----------|----------|-----------|-------------|-----------------| | T-05-01 | Tampering | compute_canonical_root | mitigate | Use `readlink -f` to resolve symlinks to real paths before hashing; prevents symlink-based hash collisions | | T-05-02 | Information Disclosure | overlay mount order | mitigate | Overlay mounts MUST come AFTER parent `~/.claude` bind; bwrap last-mount-wins ensures isolated projects/ is what the sandbox sees, not the host's real projects/ | | T-05-03 | Tampering | INSTANCE_DIR path construction | mitigate | INSTANCE_DIR is built from `$HOME/.claudebox/projects/$INSTANCE_HASH` where INSTANCE_HASH is hex-only output of sha256sum|cut; no path traversal possible via hash | | T-05-04 | Information Disclosure | cross-project data | mitigate | Each project gets unique hash dir mounted as entire `~/.claude/projects/`; sandbox cannot see other projects' hash dirs | | T-05-05 | Denial of Service | unbounded instance dirs | accept | Instance dirs accumulate until `--gc` is run; acceptable for personal tool; GC is Phase 5 Plan 02 | 1. `bash -n claudebox.sh` passes (no syntax errors) 2. `claudebox --dry-run` shows new mount layout with no old symlink/claudebox references 3. Two different project dirs produce different INSTANCE_HASH values 4. `~/.claudebox/projects//project-root` contains the correct canonical root 5. Claude Code launches successfully with plugins visible - Mount architecture uses direct `~/.claude` bind with overlay mounts for projects/, history.jsonl, SANDBOX.md, and credentials - Per-project isolation via SHA-256[:16] of canonical git root path - Git worktrees resolve to same canonical root (via --git-common-dir) - Old symlink approach completely removed from claudebox.sh - Dry-run and audit display reflect new mount layout After completion, create `.planning/phases/05-per-project-instance-isolation/05-01-SUMMARY.md`