Skip to content

Tiers

Every candidate lands in exactly one tier. The assignment is rule-driven and most-protective-first: bleach assigns the safest tier that applies, never the most aggressive one.

TierMeaningActionable?
PROTECTEDNever touchedno
CACHE-SAFERegenerable; the owner rebuilds it on demandyes
ORPHAN?No inventory source claims it, and it's staleyes
REVIEWAmbiguous, or holds user-data-shaped filesonly with --allow-review
UNKNOWNInsufficient evidenceno

"Actionable" means bleach plan will include it by default and the TUI will let you select it. Everything else is reported for context.

PROTECTED

Hard protections, any one of which is sufficient:

  • A live process is executing from the path. Deleting a running app's state is the most damaging thing this tool could do, so this is re-checked at apply time too.
  • A protected path substringCloudDocs, Mobile Documents, MobileSync (iOS device backups: huge and irreplaceable), Keychains, Mail, Messages, AddressBook, CallHistory, Photos, FileProvider, and more.
  • A protected name.ssh, .gnupg, .aws, .kube, .docker, .password-store, .netrc, .Trash. These are small enough that bleach would never propose them on size grounds anyway, but "would never propose" is not a safety guarantee and being explicit is.
  • A protected bundle prefixcom.apple.*, group.com.apple.*.
  • A registered .app bundle inside — the self-updater case.
  • A UUID-named sandbox container. Their metadata plist is entitlement-protected and unreadable, so an owner can never be established. Unknowable means untouchable.

There is also a soft protection: the owner is installed. This is the default verdict for an installed app's state, but unlike the above it can be demoted by version retention — which is how a stale IntelliJIdea2025.3 becomes actionable while 2026.2 stays protected.

CACHE-SAFE

Regenerable by definition. Safe for installed owners and orphans, because the owner rebuilds it.

Matched either by scan-root kind (Caches, Logs, Saved Application State, HTTPStorages, WebKit) or by name pattern:

-updater$          electron-updater download leftovers
^Cache$  Caches?$  ^Cached
Code Cache  GPUCache  GrShaderCache  ShaderCache  DawnCache
Crashpad  CrashReporter
^Service Worker$  ^logs?$  \.log$  ^tmp$  ^temp$

A cache that contains user-data-shaped files is demoted to REVIEW rather than staying here.

ORPHAN?

No inventory source claims the directory, it hasn't been touched inside the stale window (180 days by default), and nothing in it looks like user data.

Also reached by version retention: a version-scoped directory of an installed app, superseded by a newer sibling and stale, is an orphan even though its owner is very much present.

The question mark is honest. This is the tier where bleach is making a judgement call, and it's the one worth reading --explain output for.

REVIEW

The interesting bucket, and deliberately non-actionable. A candidate lands here when:

  • it's unowned but holds user-data-shaped files — Slack's 1.1 GB container on the development machine, three years stale but full of message databases;

  • it's unowned but recently active — something is still writing there, and bleach can't say what;

  • its owner ships its own cleanup command, so bleach reports the size and the command instead of reimplementing a retention policy it doesn't understand:

    Delegate these — the tool knows its own retention policy:
      11.5G   Homebrew    brew cleanup --prune=all
       1.8G   pnpm        pnpm store prune
       2.8G   IntelliJ…   JetBrains Toolbox > settings > clear old caches
  • it's a launchd job, where removal changes behaviour rather than reclaiming space.

REVIEW being large is honest, not a bug — on the development machine it holds 65 GB. It does need better subdivision; see Limitations.

UNKNOWN

The default. Insufficient evidence either way, or an actionable verdict on something below min_actionable_bytes (10 MB), so a 40 KB orphan can't clutter a plan.

Also where candidates land when they're superseded by finer-grained children, or when a plugin's tier hint wasn't recognised — an unknown hint degrades to report-only rather than to anything that acts.

Why the asymmetry

A missed orphan costs disk space. A false positive costs irreplaceable data. Those are not comparable, so the tiering is biased hard: ambiguity resolves toward REVIEW and UNKNOWN, and the two actionable tiers require positive evidence rather than merely the absence of contrary evidence.

Filtering by tier

sh
bleach scan --tier orphan       # orphan, orphan-likely, orphaned
bleach scan --tier cache-safe   # cache-safe, cache, safe
bleach scan --tier review
bleach scan --tier protected
bleach scan --tier unknown