ReviewOS

also looking at this

stacks/system-cleaner

feat(orphans): scan Application Support and Containers

#29
Merged glennmichael123 wants to merge feat/orphan-scan-application-support into main
3 files +242 -19
9 days ago

Closes #26.

Orphan detection reached five roots — Caches, Logs, Saved Application State, WebKit, HTTPStorages — and matched entries only against /^(com|org|net|io)\./. Caches and logs are where an uninstalled app leaves megabytes. Application Support is where it leaves gigabytes, and it wasn't scanned.

Found by uninstalling Cursor and watching 5.6 GB survive — 4.1 GB of it in a directory the scan never opened, under a name the pattern would have rejected anyway.

What's added

  • Application Support, scanned twice — once for reverse-DNS directories, once for plain-named ones.
  • Containers — always bundle-ID named, so unambiguous.
  • Group Containers deliberately omitted. They carry a team-identifier prefix (9BNSXJN65R.com.example.app) and are shared between several apps of one vendor, so "is the owner gone?" isn't a question a single directory name can answer.
  • holdsUserData on OrphanedItem, plumbed through /orphaned-data, so the UI can refuse to pre-select these the way it can a cache.
  • A 90-day threshold for the user-data roots instead of 30.

The part that needs review

The issue proposed "flag any directory whose name matches no installed app." I didn't implement that — it reports live data as orphaned.

On the machine this was developed against:

DirectorySizeWhat it actually is
Google7.7 GBlive Chrome profile — history, bookmarks, saved passwords
Mozilla—Firefox
BraveSoftware—Brave
MobileSync—every iPhone backup on the machine, no owning .app at all

None of those names matches an installed app. All four would have been offered for deletion.

So plain-name orphans come from three guards instead:

  • KNOWN_APP_DIRS maps a directory to the apps that own it, and is the only source of plain-name orphans. An unrecognised directory is never flagged — false negatives cost disk space, false positives cost the user their data.
  • SYSTEM_DIR_NAMES excludes macOS's own plain-named directories outright.
  • An empty app index abandons plain-name matching entirely. If Spotlight is disabled, mdfind returns nothing and every mapped app reads as uninstalled; the bundle-ID roots still have NEVER_DELETE and the com.apple. rule to fall back on, this path has nothing but the index.

The tradeoff: plain-name coverage is only as good as KNOWN_APP_DIRS, which starts at 19 entries. Expanding it is cheap, but each entry is a judgement about what's safe to attribute — so it's a deliberate opt-in list rather than a heuristic.

Verification

Run against a working developer Mac: 63 orphans, 1.19 GB.

Two largest are true positives, confirmed by hand:

  • Steam — 1.09 GB, app since removed
  • Microsoft — 4 KB stub, no Office installed

It also correctly caught com.todesktop.230313mzl4w4u92, Cursor's leftover HTTPStorages entry.

And the ones that matter: Google, Mozilla, MobileSync, AddressBook, CloudDocs, Knowledge — all left alone.

tests/orphans.test.ts adds 6 tests covering the attribution map, the system-directory exclusions, the invariant that no directory appears in both tables, and three live-scan assertions — that nothing with an installed owner is reported, that no macOS-owned directory is reported, and that the holdsUserData flag matches the root type.

Checks

  • bun run test — 515 pass, 0 fail, across 57 files
  • bun run typecheck — clean
  • bun run lint — 0 errors

Follow-up, not in this PR

~/.cursor (1.0 GB) and ~/.local/share/cursor-agent (355 MB) are still invisible. The issue proposed dotfile and XDG roots, but the installed-app check is structurally wrong there: those directories belong to CLI tools, which aren't .app bundles, so everything under them reads as orphaned. ~/.ssh, ~/.aws and ~/.gnupg live in the same place. That needs a different mechanism and should be its own issue.

🤖 Generated with Claude Code

3 changed files on the files tab, with 0 review threads.

Sign in to comment on this pull request.