21 KiB
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 05-per-project-instance-isolation | 01 | execute | 1 |
|
false |
|
|
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.mdFrom 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.
### 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.
<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> |
<success_criteria>
- Mount architecture uses direct
~/.claudebind 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>