--- phase: 05-per-project-instance-isolation plan: 02 type: execute wave: 2 depends_on: - 05-01 files_modified: - claudebox.sh autonomous: true requirements: - INST-04 must_haves: truths: - "Running `claudebox --gc` removes instance directories whose project root no longer exists on disk" - "`claudebox --gc` prints removed paths to stderr and exits without launching Claude" - "`claudebox --gc` does nothing harmful when projects/ directory is empty" artifacts: - path: "claudebox.sh" provides: "gc_instances function and --gc flag handling" contains: "gc_instances" - path: "claudebox.sh" provides: "--gc in flag parsing case statement" contains: "--gc)" key_links: - from: "claudebox.sh (--gc flag)" to: "gc_instances function" via: "GC_MODE=true triggers gc_instances call" pattern: "GC_MODE.*gc_instances" - from: "gc_instances" to: "~/.claudebox/projects/*/project-root" via: "reads project-root file, checks if path exists on disk" pattern: "project-root" --- Add `--gc` flag and garbage collection function to claudebox for cleaning up stale per-project instance directories. Purpose: Prevent unbounded growth of `~/.claudebox/projects/` by providing a way to remove instance directories whose project root no longer exists on the host filesystem. Output: Updated claudebox.sh with --gc flag, gc_instances function, and GC dispatch logic. @/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 @.planning/phases/05-per-project-instance-isolation/05-01-SUMMARY.md Host layout after Plan 01: ``` ~/.claudebox/projects/<16-char-hash>/ project-root # plaintext file containing canonical root path -home-user-code-myproject/ # Claude Code writes here ``` From claudebox.sh (flag parsing after Plan 01): ```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 ``` Task 1: Add --gc flag and gc_instances function claudebox.sh - claudebox.sh (entire file — especially flag parsing block and the CHECK_MODE dispatch block pattern) - .planning/phases/05-per-project-instance-isolation/05-CONTEXT.md (D-11, D-12 decisions) - .planning/phases/05-per-project-instance-isolation/05-RESEARCH.md (Pattern 3: GC Implementation, Pitfall 7: glob on empty dir) Modify claudebox.sh with these changes: **1. Add GC_MODE flag variable** (line 5, after `SHELL_MODE=false`): ```bash GC_MODE=false ``` **2. Add --gc case to flag parsing** (inside the while/case block, after the `--shell)` case): ```bash --gc) GC_MODE=true ;; ``` **3. Add gc_instances function** (insert after the `compute_canonical_root` function, before the instance initialization block). This is a standalone function: ```bash # Garbage-collect stale instance directories (D-11, INST-04) gc_instances() { local removed=0 local projects_dir="$HOME/.claudebox/projects" if [[ ! -d "$projects_dir" ]]; then echo "No projects directory found at $projects_dir" >&2 return fi for dir in "$projects_dir"/*/; do [[ -d "$dir" ]] || continue local root_file="$dir/project-root" [[ -f "$root_file" ]] || continue local root_path root_path=$(< "$root_file") if [[ ! -d "$root_path" ]]; then rm -rf "$dir" echo "Removed: $dir (project root gone: $root_path)" >&2 (( removed++ )) || true fi done echo "GC complete: $removed instance(s) removed." >&2 } ``` **4. Add GC dispatch block** (insert after the CHECK_MODE dispatch block, before the ANSI formatting section). Follow the same pattern as CHECK_MODE — run the function and exit: ```bash # --gc: remove stale instance directories and exit (D-12, INST-04) if [[ "$GC_MODE" == true ]]; then gc_instances exit 0 fi ``` This ensures `--gc` exits immediately after GC, never launches Claude (same pattern as `--check`). bash -n claudebox.sh && grep -q 'GC_MODE=false' claudebox.sh && grep -q -- '--gc)' claudebox.sh && grep -q 'gc_instances' claudebox.sh && grep -q 'project-root' claudebox.sh && grep -q 'GC complete:' claudebox.sh && echo "ALL CHECKS PASSED" - claudebox.sh passes `bash -n` syntax check - claudebox.sh contains `GC_MODE=false` variable initialization - claudebox.sh contains `--gc) GC_MODE=true ;;` in the case statement (or `--gc) GC_MODE=true ;;`) - claudebox.sh contains `gc_instances()` function definition - gc_instances function contains `for dir in "$projects_dir"/*/;` loop - gc_instances function contains `[[ -d "$dir" ]] || continue` (Pitfall 7 guard) - gc_instances function contains `[[ -f "$root_file" ]] || continue` (defensive skip) - gc_instances function contains `rm -rf "$dir"` for stale dirs - gc_instances function contains `echo "Removed:` output to stderr - gc_instances function contains `echo "GC complete:` summary to stderr - claudebox.sh contains `if [[ "$GC_MODE" == true ]]; then` dispatch block - The GC dispatch block calls `gc_instances` followed by `exit 0` - The GC dispatch block appears BEFORE the ANSI formatting section (so it exits early like --check) `claudebox --gc` iterates `~/.claudebox/projects/*/project-root`, removes directories whose recorded project root no longer exists on disk, prints each removal to stderr, prints a summary count, and exits without launching Claude. Task 2: Add GC integration test test-gc.sh - claudebox.sh (the gc_instances function from Task 1) - Test 1: Creating a fake instance dir with a project-root pointing to a non-existent path, then running gc_instances, removes the dir - Test 2: Creating a fake instance dir with a project-root pointing to an existing path, then running gc_instances, keeps the dir - Test 3: Running gc_instances on an empty projects/ dir produces "GC complete: 0" Create `test-gc.sh` as a standalone bash test script that sources the `gc_instances` function from claudebox.sh (or redefines it inline for isolation) and verifies the three behaviors above. The test should: 1. Create a temporary directory structure mimicking `~/.claudebox/projects/` 2. Override `HOME` to point to the temp dir 3. Create test instance dirs: one with a valid project-root, one with a stale project-root, one empty projects/ dir 4. Call `gc_instances` and verify: - Stale dir was removed - Valid dir was kept - Empty dir produces "0 instance(s) removed" 5. Exit 0 on success, exit 1 on failure with diagnostic output Make the script executable with `chmod +x test-gc.sh`. bash test-gc.sh && echo "GC TESTS PASSED" test-gc.sh runs three test cases covering stale removal, valid preservation, and empty-dir safety. All pass. ## Trust Boundaries | Boundary | Description | |----------|-------------| | project-root file content to rm -rf | The path read from project-root is used to decide whether to delete the instance dir; a tampered project-root could prevent GC or cause unexpected behavior | | user CLI input (--gc flag) to filesystem deletion | --gc triggers rm -rf on instance dirs; only dirs under ~/.claudebox/projects/ are affected | ## STRIDE Threat Register | Threat ID | Category | Component | Disposition | Mitigation Plan | |-----------|----------|-----------|-------------|-----------------| | T-05-06 | Tampering | project-root file content | accept | The project-root file is written by claudebox itself and lives under user-owned ~/.claudebox/. A user tampering with their own files is not a threat. GC only deletes the instance dir, not the path recorded in project-root. | | T-05-07 | Denial of Service | --gc deletes wrong dirs | mitigate | GC loop is scoped to `$HOME/.claudebox/projects/*/` only; `rm -rf` target is `$dir` which is always under that prefix. Cannot escape to arbitrary paths. | | T-05-08 | Elevation of Privilege | rm -rf via symlink in projects/ | mitigate | Instance dirs are created by claudebox's own `mkdir -p`; if a symlink appears in projects/, the `[[ -d "$dir" ]]` check follows symlinks but `rm -rf` on a symlink to a directory would delete the symlink target. Low risk since ~/.claudebox/ is user-writable anyway. Accept for personal tool. | 1. `bash -n claudebox.sh` passes 2. `claudebox --gc` runs without error on current system (prints "GC complete: 0 instance(s) removed." if no stale dirs) 3. `bash test-gc.sh` passes all three test cases 4. `claudebox --gc` does NOT launch Claude Code (exits immediately) - `claudebox --gc` removes stale instance directories and exits - Valid instance directories (project root still exists) are preserved - Empty projects/ directory does not cause errors - GC output goes to stderr with removal details and summary count After completion, create `.planning/phases/05-per-project-instance-isolation/05-02-SUMMARY.md`