claudebox/.planning/phases/05-per-project-instance-isolation/05-01-PLAN.md

21 KiB

phase plan type wave depends_on files_modified autonomous requirements must_haves
05-per-project-instance-isolation 01 execute 1
claudebox.sh
.planning/REQUIREMENTS.md
false
INST-01
INST-02
INST-03
truths artifacts key_links
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
CLAUDE_JSON_FILE mount for ~/.claude.json is preserved after Phase 5 changes
path provides contains
claudebox.sh compute_canonical_root function, instance initialization, new mount layout compute_canonical_root
path provides contains
claudebox.sh BWRAP_ARGS with direct ~/.claude bind and overlay mounts --bind "$HOME/.claude" "$HOME/.claude"
path provides contains
claudebox.sh CLAUDE_JSON_FILE conditional mount preserved --bind "$CLAUDE_JSON_FILE" "$HOME/.claude.json"
path provides contains
.planning/REQUIREMENTS.md INST-01 through INST-04 requirement definitions INST-04
from to via pattern
claudebox.sh (compute_canonical_root) INSTANCE_DIR variable sha256sum of canonical root path sha256sum.*cut -c1-16
from to via pattern
claudebox.sh (BWRAP_ARGS) bwrap execution overlay mounts after parent bind --bind.*.claude/projects
Rewrite claudebox mount architecture from symlink-based to direct bind + overlay, implement per-project instance isolation, and register INST-01 through INST-04 requirements.

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. REQUIREMENTS.md updated with INST-01 through INST-04.

<execution_context> @/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 </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/REQUIREMENTS.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):

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 (CLAUDE_JSON_FILE detection, lines 114-122 — MUST BE PRESERVED):

# Claude Code config file mount (~/.claude.json)
# Stores auth tokens and user preferences; must be read-write so Claude Code
# can update tokens and write backups without prompting for re-auth.
CLAUDE_JSON_FILE="$HOME/.claude.json"
if [[ -f "$CLAUDE_JSON_FILE" ]]; then
  CLAUDE_JSON_MOUNT=true
else
  CLAUDE_JSON_MOUNT=false
fi

From claudebox.sh (BWRAP_ARGS, lines 364-398 — MUST BE REPLACED):

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 (CLAUDE_JSON conditional mount, lines 386-388 — MUST BE PRESERVED):

if [[ "$CLAUDE_JSON_MOUNT" == true ]]; then
  BWRAP_ARGS+=(--bind "$CLAUDE_JSON_FILE" "$HOME/.claude.json")
fi

From claudebox.sh (credential mount, lines 104-112):

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, CLAUDE_JSON_FILE detection at lines 114-122, CLAUDE_JSON_MOUNT conditional at lines 386-388, 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, Pitfall 6 re CLAUDE_JSON) - .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):

# 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):

# 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:

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:

  --bind "$HOME/.claudebox" "$HOME/.claudebox"
  --symlink "$HOME/.claudebox" "$HOME/.claude"

With (per D-01, D-02, D-03, D-06):

  # 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. Preserve CLAUDE_JSON_MOUNT conditional block (currently lines 386-388). This block MUST remain after the BWRAP_ARGS array:

if [[ "$CLAUDE_JSON_MOUNT" == true ]]; then
  BWRAP_ARGS+=(--bind "$CLAUDE_JSON_FILE" "$HOME/.claude.json")
fi

This mounts ~/.claude.json (the root-level file, NOT inside ~/.claude/). It is independent of the Phase 5 architecture changes. Verify this block is still present and functional after the BWRAP_ARGS rewrite. Per RESEARCH.md Pitfall 6, this was uncommitted from Phase 4 and must be incorporated.

6. Update credential mount target (in the conditional block after BWRAP_ARGS, currently line ~390): Change:

BWRAP_ARGS+=(--bind "$CREDS_FILE" "$HOME/.claudebox/.credentials.json")

To:

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.

7. Update dry-run block (lines 318-361). Replace lines 348-355 (the mount echo lines for claudebox bind, symlink, and credential target): Replace:

    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:

    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 [[ "$CLAUDE_JSON_MOUNT" == true ]]; then
      echo "  --bind $CLAUDE_JSON_FILE $HOME/.claude.json \\"
    fi
    if [[ "$CREDS_MOUNT" == true ]]; then
      echo "  --bind $CREDS_FILE $HOME/.claude/.credentials.json \\"
    fi

Note: The CLAUDE_JSON dry-run echo MUST be preserved here as well.

8. Update print_audit mounts section (inside print_audit function, around line 276-283). Replace mount display lines: Replace:

  printf '  %-12s %s   (read-write)\n' "$HOME/.claude" "$HOME/.claudebox" >&2

With:

  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

9. 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 -- '--bind "$CLAUDE_JSON_FILE" "$HOME/.claude.json"' claudebox.sh && ! grep -q -- '--symlink..claudebox..claude' claudebox.sh && ! grep -q -- '--bind "$HOME/.claudebox" "$HOME/.claudebox"' claudebox.sh && echo "ALL CHECKS PASSED" <acceptance_criteria> - 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 contains --bind "$CLAUDE_JSON_FILE" "$HOME/.claude.json" (CLAUDE_JSON_MOUNT preserved per Pitfall 6) - 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 - Dry-run block echoes --bind $CLAUDE_JSON_FILE $HOME/.claude.json when CLAUDE_JSON_MOUNT is true - print_audit shows projects/ mount line with $INSTANCE_DIR and $CANONICAL_ROOT - SANDBOX.md heredoc does NOT contain ~/.claudebox - INST-03 satisfied by D-13: Claude Code manages own file concurrency; no locking mechanism needed in claudebox.sh. Two concurrent sessions share the same INSTANCE_DIR safely. </acceptance_criteria> 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, CLAUDE_JSON_FILE mount preserved, and all display/dry-run blocks updated to match. Old symlink approach completely removed.

Task 2: Register INST-01 through INST-04 in REQUIREMENTS.md .planning/REQUIREMENTS.md - .planning/REQUIREMENTS.md (current content — ends at AUTH-02) - .planning/phases/05-per-project-instance-isolation/05-RESEARCH.md (phase_requirements table with INST-01 through INST-04 descriptions) - .planning/ROADMAP.md (Phase 5 success criteria for cross-reference) Append the following section to `.planning/REQUIREMENTS.md`, after the `### Authentication Passthrough` section (after AUTH-02) and before the `### Network Isolation` section:
### Instance Isolation

- **INST-01**: Each project directory has isolated conversation history — launching claudebox in two different project directories produces separate histories with no cross-contamination
- **INST-02**: Git worktrees of the same repo share instance state with the main worktree (canonical root resolved via `git rev-parse --git-common-dir`)
- **INST-03**: Two concurrent claudebox sessions in the same project do not corrupt each other's state (satisfied architecturally: Claude Code manages its own file-level concurrency within its data dir; no locking needed per D-13)
- **INST-04**: `claudebox --gc` removes instance directories for project roots that no longer exist on disk

Also update the Traceability table at the bottom of the file to add:

| INST-01 | Phase 5 | Pending |
| INST-02 | Phase 5 | Pending |
| INST-03 | Phase 5 | Pending |
| INST-04 | Phase 5 | Pending |

And update the Coverage line to reflect the new count: v1 requirements: 31 total, v2 requirements (partial): 6 (was 2, adding 4 INST requirements). grep -q 'INST-01' .planning/REQUIREMENTS.md && grep -q 'INST-02' .planning/REQUIREMENTS.md && grep -q 'INST-03' .planning/REQUIREMENTS.md && grep -q 'INST-04' .planning/REQUIREMENTS.md && grep -q 'Instance Isolation' .planning/REQUIREMENTS.md && grep -c 'INST-0' .planning/REQUIREMENTS.md | grep -q '[4-9]' && echo "ALL CHECKS PASSED" <acceptance_criteria> - .planning/REQUIREMENTS.md contains ### Instance Isolation section header - .planning/REQUIREMENTS.md contains INST-01 with description mentioning "isolated conversation history" - .planning/REQUIREMENTS.md contains INST-02 with description mentioning "git worktrees" and "git rev-parse --git-common-dir" - .planning/REQUIREMENTS.md contains INST-03 with description mentioning "concurrent" and "D-13" - .planning/REQUIREMENTS.md contains INST-04 with description mentioning "--gc" and "project roots that no longer exist" - Traceability table contains rows for INST-01 through INST-04 mapped to Phase 5 - ### Instance Isolation section appears after ### Authentication Passthrough and before ### Network Isolation </acceptance_criteria> REQUIREMENTS.md contains INST-01 through INST-04 definitions with descriptions matching Phase 5 success criteria, traceability entries mapping all four to Phase 5, and updated coverage count.

Task 3: 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. CLAUDE_JSON_FILE mount preserved. 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 --shell -- ls ~/.claude/` and confirm plugins/, commands/, hooks/ or equivalent subdirs are visible 6. Run `claudebox --dry-run` and verify `--bind $HOME/.claude.json` line is present in the output Human confirms all 6 verification steps pass Mount architecture rewrite verified: direct ~/.claude bind works, per-project overlay produces isolated hash dirs, dry-run output is correct, CLAUDE_JSON mount preserved, Claude Code launches with plugins visible. Type "approved" or describe issues

<threat_model>

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
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
</threat_model>
1. `bash -n claudebox.sh` passes (no syntax errors) 2. `claudebox --dry-run` shows new mount layout with no old symlink/claudebox references 3. `claudebox --dry-run` shows `--bind $HOME/.claude.json` line 4. Two different project dirs produce different INSTANCE_HASH values 5. `~/.claudebox/projects//project-root` contains the correct canonical root 6. Claude Code launches successfully with plugins visible

<success_criteria>

  • 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
  • CLAUDE_JSON_FILE mount (--bind "$CLAUDE_JSON_FILE" "$HOME/.claude.json") preserved
  • Dry-run and audit display reflect new mount layout
  • INST-01 through INST-04 registered in REQUIREMENTS.md
  • INST-03 satisfied architecturally by D-13 (documented in acceptance criteria) </success_criteria>
After completion, create `.planning/phases/05-per-project-instance-isolation/05-01-SUMMARY.md`