246 lines
10 KiB
Markdown
246 lines
10 KiB
Markdown
---
|
|
phase: 05-per-project-instance-isolation
|
|
plan: 02
|
|
type: execute
|
|
wave: 2
|
|
depends_on:
|
|
- 05-01
|
|
files_modified:
|
|
- claudebox.sh
|
|
- test-gc.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)"
|
|
- path: "test-gc.sh"
|
|
provides: "GC integration test covering stale removal, valid preservation, empty-dir safety"
|
|
contains: "gc_instances"
|
|
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"
|
|
---
|
|
|
|
<objective>
|
|
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. test-gc.sh with three test cases.
|
|
</objective>
|
|
|
|
<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>
|
|
|
|
<context>
|
|
@.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
|
|
|
|
<interfaces>
|
|
<!-- From Plan 01: per-project instance structure on host -->
|
|
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
|
|
```
|
|
</interfaces>
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: Add --gc flag and gc_instances function</name>
|
|
<files>claudebox.sh</files>
|
|
<read_first>
|
|
- 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)
|
|
</read_first>
|
|
<action>
|
|
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`).
|
|
</action>
|
|
<verify>
|
|
<automated>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"</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- 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)
|
|
</acceptance_criteria>
|
|
<done>
|
|
`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.
|
|
</done>
|
|
</task>
|
|
|
|
<task type="auto" tdd="true">
|
|
<name>Task 2: Add GC integration test</name>
|
|
<files>test-gc.sh</files>
|
|
<read_first>
|
|
- claudebox.sh (the gc_instances function from Task 1)
|
|
</read_first>
|
|
<behavior>
|
|
- 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"
|
|
</behavior>
|
|
<action>
|
|
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`.
|
|
</action>
|
|
<verify>
|
|
<automated>bash test-gc.sh && echo "GC TESTS PASSED"</automated>
|
|
</verify>
|
|
<done>
|
|
test-gc.sh runs three test cases covering stale removal, valid preservation, and empty-dir safety. All pass.
|
|
</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<threat_model>
|
|
## 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. |
|
|
</threat_model>
|
|
|
|
<verification>
|
|
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)
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- `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
|
|
</success_criteria>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/05-per-project-instance-isolation/05-02-SUMMARY.md`
|
|
</output>
|