Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -573,13 +573,15 @@ 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.

| 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 |
Expand Down
9 changes: 9 additions & 0 deletions internal/scanner/global.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 18 additions & 0 deletions internal/scanner/global_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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:
Expand Down
Loading