Rules
Everything risky is data, not code: which paths must never be touched, which directory belongs to which bundle ID, which names mark regenerable state.
bleach rules # print the effective rules
bleach rules --init # copy them to ~/.config/bleach/rules.yamlOverlays can only add protections
Your overlay is merged over the defaults with one deliberate asymmetry:
- List entries are appended. You cannot remove a shipped protection by overriding a list.
- Scalars are replaced. Thresholds are yours to set.
- Maps are merged, with your value winning on a key collision.
So an overlay can make bleach more careful but never less. If you genuinely need to act on something the defaults protect, that's what --allow-review and hand-editing a plan are for.
Sections
protected_bundle_prefixes
Bundle-ID prefixes that are always protected.
protected_bundle_prefixes:
- com.apple.
- group.com.apple.
- com.microsoft.VSCode # workspace state is not reconstructibleprotected_path_contains
Case-insensitive substrings that force PROTECTED anywhere in the path.
protected_path_contains:
- CloudDocs
- Mobile Documents
- MobileSync # iOS device backups: huge and irreplaceable
- Keychains
- /Mail/
- /Messages/
- CallHistory
- FileProviderprotected_names
Exact leaf names, always protected. Credentials and key material live here.
protected_names:
- .ssh
- .gnupg
- .aws
- .kube
- .docker
- .password-store
- .netrc
- .TrashThese 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.
aliases
Directory name → bundle identifier, for names that normalisation and fuzzy matching can't bridge.
aliases:
"Code - Insiders": com.microsoft.VSCodeInsiders
"Claude": com.anthropic.claudefordesktop
"Freelens": app.freelens.Freelens
"BambuStudio": com.bambulab.bambu-studio
"Steam": com.valvesoftware.steamThe shipped table is small on purpose. Every entry in it was verified against a real LaunchServices registry rather than guessed, because a wrong alias silently mis-attributes a directory, which is worse than leaving it unresolved.
To find the right identifier for something on your machine:
/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/\
LaunchServices.framework/Support/lsregister -dump \
| grep -B8 'identifier:.*yourapp'regenerable_patterns
Case-insensitive regexes on the candidate name marking state that is safe to clear even when the owner is installed and running.
regenerable_patterns:
- "-updater$" # electron-updater download leftovers
- "^Cache$"
- "Caches?$"
- "^Cached"
- "GPUCache"
- "ShaderCache"
- "Crashpad"
- "\\.log$"delegated_cleanups
When a tool ships its own cleanup with its own retention policy, bleach reports the size and prints the command rather than reimplementing a policy it doesn't understand.
delegated_cleanups:
"Homebrew": "brew cleanup --prune=all"
"pnpm": "pnpm store prune"
".npm": "npm cache clean --force"
".m2": "rm -rf ~/.m2/repository (redownloaded by the next build)"
".gradle": "gradle --stop, then rm -rf ~/.gradle/caches"
".rustup": "rustup toolchain list, then rustup toolchain uninstall <old>"Delegated entries land in REVIEW, so they're reported but never acted on. This is the single highest-value section: on the development machine it surfaced roughly 50 GB of cleanup that the owning tools do correctly and bleach would do badly.
Delegated entries are checked before the nested-app-bundle protection, so ms-playwright — whose browser caches contain registered .app bundles but are fully reinstallable — gets reported rather than silently protected.
Thresholds
stale_days: 180 # no internal modification for this long = stale
keep_versions: 1 # versioned siblings to keep when the owner is installed
min_actionable_bytes: 10485760 # 10 MB; smaller candidates are reported, never proposedmin_actionable_bytes keeps a 40 KB orphan out of your plan. keep_versions: 2 is reasonable if you occasionally roll back an IDE.
A worked overlay
# ~/.config/bleach/rules.yaml
# Never touch my scratch dirs, whatever bleach thinks.
protected_names:
- .experiments
- .localstack
protected_path_contains:
- /ClientWork/
# My in-house tool's bundle ID isn't discoverable from its directory name.
aliases:
"AcmeDesigner": com.acme.designer
# Our build tool's scratch space is always safe to clear.
regenerable_patterns:
- "^acme-build-"
# Bazel knows its own retention policy far better than bleach does.
delegated_cleanups:
".cache/bazel": "bazel clean --expunge"
# I want a shorter staleness window and two IDE versions kept.
stale_days: 90
keep_versions: 2Verify it loaded:
bleach rules | head -40
bleach scan --rules ~/.config/bleach/rules.yaml --tier orphan--rules takes an explicit path, which is handy for testing an overlay without installing it.