diff --git a/docs/SPEC.md b/docs/SPEC.md index a437781..958bc1b 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -573,6 +573,7 @@ Each entry that is `caution` carries a consequence-of-deletion note in `recommen | `~/Library/Android/sdk/ndk` | deps | caution | | `~/Library/Android/sdk/build-tools` | runtime | caution | | `~/Library/Application Support/Cursor/{Cache,CachedData,Code Cache}` | cache | safe (cache subdirs only — settings live alongside) | +| `~/Library/Application Support/Claude/{Cache,Code Cache,GPUCache,DawnGraphiteCache,DawnWebGPUCache,GraphiteDawnCache}` | cache | safe (cache subdirs only — sessions and login state live alongside) | | `/private/var/folders/*/*/X/*.code_sign_clone` | cache | safe / caution (macOS only — Browser Temp, see below) | **Config roots and user state are deliberately excluded.** A home dotfile is treated as config unless it is unambiguously a package/build cache (like `~/.cache/uv`). Deleting an excluded tree is unrecoverable or credential loss, and the "caches" inside only reappear as install-time scaffolding — worthless against that risk, so devclean never offers them for deletion. @@ -580,6 +581,7 @@ Each entry that is `caution` carries a consequence-of-deletion note in `recommen | Excluded path | Why | |---|---| | `~/.claude` (whole tree) | session transcripts, project memory, agents, skills, plugins, todos | +| `~/Library/Application Support/Claude` (except the caches above) | Claude Desktop sessions and login state; the app downloads its Cowork VM image again after deletion | | `~/.codex`, `~/.gemini` | agent CLI state | | `~/Library/Caches/claude-cli-nodejs` | Claude Code state | | `~/.cursor` | extensions & settings | diff --git a/internal/scanner/global.go b/internal/scanner/global.go index 3e7f39d..ffc88c1 100644 --- a/internal/scanner/global.go +++ b/internal/scanner/global.go @@ -123,6 +123,15 @@ var globalCaches = []globalCache{ {"Library/Application Support/Cursor/Cache", model.CatCache, model.SafetySafe, "Cursor cache", "", nil}, {"Library/Application Support/Cursor/CachedData", model.CatCache, model.SafetySafe, "Cursor cached data", "", nil}, {"Library/Application Support/Cursor/Code Cache", model.CatCache, model.SafetySafe, "Cursor code cache", "", nil}, + // Only Claude Desktop's Chromium cache subdirectories. The rest of + // Application Support/Claude holds sessions and login state, and the app + // downloads its Cowork VM image (vm_bundles) again after deletion. + {"Library/Application Support/Claude/Cache", model.CatCache, model.SafetySafe, "Claude Desktop cache", "", nil}, + {"Library/Application Support/Claude/Code Cache", model.CatCache, model.SafetySafe, "Claude Desktop code cache", "", nil}, + {"Library/Application Support/Claude/GPUCache", model.CatCache, model.SafetySafe, "Claude Desktop GPU cache", "", nil}, + {"Library/Application Support/Claude/DawnGraphiteCache", model.CatCache, model.SafetySafe, "Claude Desktop Dawn Graphite cache", "", nil}, + {"Library/Application Support/Claude/DawnWebGPUCache", model.CatCache, model.SafetySafe, "Claude Desktop Dawn WebGPU cache", "", nil}, + {"Library/Application Support/Claude/GraphiteDawnCache", model.CatCache, model.SafetySafe, "Claude Desktop Graphite Dawn cache", "", nil}, } // GlobalScanner scans for shared, home-rooted developer caches that are not diff --git a/internal/scanner/global_test.go b/internal/scanner/global_test.go index 76c9900..9b795b9 100644 --- a/internal/scanner/global_test.go +++ b/internal/scanner/global_test.go @@ -116,6 +116,15 @@ func TestGlobalScanner_DetectsExpandedCatalog(t *testing.T) { // Settings live next to the caches and must never be picked up. mustWriteFile(t, filepath.Join(cursorRoot, "settings.json"), []byte("{}")) + claudeRoot := filepath.Join(home, "Library", "Application Support", "Claude") + claudeCache := filepath.Join(claudeRoot, "Cache") + mustMkdir(t, claudeCache) + mustWriteFile(t, filepath.Join(claudeCache, "blob"), make([]byte, 256)) + // The VM bundle sits next to the caches and must never be picked up. + claudeVM := filepath.Join(claudeRoot, "vm_bundles") + mustMkdir(t, claudeVM) + mustWriteFile(t, filepath.Join(claudeVM, "rootfs.img"), make([]byte, 256)) + s := newIsolatedGlobalScanner(t) results, err := s.Scan(context.Background(), home) if err != nil { @@ -167,6 +176,15 @@ func TestGlobalScanner_DetectsExpandedCatalog(t *testing.T) { if _, ok := byPath[cursorRoot]; ok { t.Errorf("Application Support/Cursor itself must not be reported (settings live there)") } + + if r, ok := byPath[claudeCache]; !ok || r.Safety != model.SafetySafe { + t.Errorf("expected Claude Desktop Cache subdir as safe, got %+v (found=%v)", r, ok) + } + for _, p := range []string{claudeRoot, claudeVM} { + if _, ok := byPath[p]; ok { + t.Errorf("%s must not be reported", p) + } + } } // TestGlobalScanner_ScopedRootExcludesHomeCaches documents the intended scoping: