P0-08: Risk register — every way this app can hurt a user
A disk cleaner’s entire value proposition is “trust me to delete the right things.” Every incident below is a real, cited case of a comparable tool getting that wrong. This register exists so we design the destructive path — quarantine, the single delete API, the rules manifest — against real failure modes instead of imagined ones.
Scoring: likelihood H/M/L, blast radius Critical (irreplaceable data or root compromise) / High (real damage, usually recoverable with effort) / Medium (annoying, cheaply recoverable). Sorted by likelihood × blast radius, worst first.
Risk register
| ID | Risk | Likelihood | Blast radius | Mitigation | Owner |
|---|---|---|---|---|---|
| R01 | Orphaned-file heuristic matches a still-installed app’s data (bundle-ID / name-prefix matching is fuzzy) | H — this is the exact bug class Pearcleaner shipped in production: #477 “Removed important data from Library/Application Support”, #203 “Orphaned Files showing up for apps that are not deleted”. Fuzzy matching on app leftovers is inherently heuristic. | Critical — deletes an installed, working app’s data (settings, saved games, license files); not “junk” at all. Recoverable only if quarantined first. | Orphan detection must require a positive “app is truly gone” signal (no bundle at any known install path, not just a name/prefix match) before a path is even proposed; every orphan match ships through quarantine, never direct delete; show the user the owning-app guess and let them veto per item. | P3-A1..A6 (orphan detection logic), P3-K2 (rules engine matching), P3-K4 (all deletes go through quarantine) |
| R02 | An app’s live data (SQLite DBs, license files, save games under Application Support) misclassified as “cache” |
H — same root cause as R01, called out separately because it’s a rule-authoring mistake, not a detection-heuristic mistake: a manifest rule targets a directory that’s a mix of true cache and live state. A MacPaw-adjacent report: a long-time user warned that CleanMyMac 2 and 3 “damaged the Photos Library” and stopped trusting cleaner tools from that vendor entirely (Apple Community thread). | Critical — same as R01; the canonical failure is deleting a database file that looks disposable but has no regeneration path. | Rules manifest treats Caches/ and Application Support/ as different trust tiers by default (cache-named directories are default-safe, Application Support requires an explicit per-app allowlist entry with a human-reviewed reason); P4-01 rule tests assert no rule targets a path containing files newer than N days or containing recognized DB/save-file extensions without an explicit override flag. |
P3-K2 (rules engine), P1-09 (manifest review), P4-01 (rule tests) |
| R03 | Removing a launch agent/daemon that a VPN, backup, or security tool depends on | H — recurring, multiply-corroborated pattern: Onyx breaking Astrill VPN login (Mac-Forums thread), CleanMyMac X deleting launch agents and breaking Cloudflare WARP connectivity badly enough the user had to factory-reset (Apple Community thread), general community consensus that “cleaner apps interacting badly with system protections” is one of the most common macOS forum complaints. | High — loss of VPN/backup/security functionality, sometimes requiring reinstall or (per one report) a full OS reset; not data loss per se but breaks the exact tools protecting the user’s data. | Launch agent/daemon removal is opt-in only, never part of a “Smart Scan” default sweep; hard denylist of known-critical labels (com.apple.* backup/security related, common VPN vendor prefixes) that the app will never offer to remove; every offered agent shows its owning app/vendor by parsing the plist, not just the filename. |
P3-J1..J4 (junk/launch-item modules), P1-09 (manifest review) |
| R04 | Privileged helper accepts unauthenticated XPC connections → any local process (malware) executes root commands through it | M — requires local code execution first, but the vulnerability class is endemic to exactly this kind of tool: Pearcleaner itself shipped this bug — GHSA-gr2j-65fh-8pvc, unauthenticated runCommand executing arbitrary shell as root because shouldAcceptNewConnection returned true unconditionally. Same pattern hit Forklift, GOG Galaxy, Acustica Audio’s HelperTool, and Sparkle’s updater (CVE-2025-10016/10015). |
Critical — full root compromise of the machine by any unprivileged local process, not just misuse by the user. | Helper’s shouldAcceptNewConnection must verify the caller’s code signature (team ID + bundle ID, via SecCodeCopyGuestWithAttributes/csops), reject everything else; helper exposes a minimal, fully-typed command surface (never a raw shell-string executor); helper is installed via SMAppService/SMJobBless with a hardened entitlement, audited in P4-03. |
P3-S1 (privileged helper) |
| R05 | A bad deletion rule ships in an app update and fires identically on every install at once | L–M for current userbase size, but the mechanism is proven at scale by security-vendor auto-update incidents: McAfee’s 2010 DAT update falsely flagged a core Windows file and bricked “tens of thousands” of machines simultaneously (Goodreads/press summary); Sophos 2012 update flagged its own updater as malware across its install base (PCWorld); CrowdStrike’s 2024 config push crashed 8.5M machines in hours (reporting summary). Same blast shape as a bad rules-manifest push. | Critical — simultaneous, correlated damage across the entire install base plus a reputational event, not a one-user incident. | Rules manifest is versioned and staged (canary % rollout before 100%), every rule change requires the P1-09 review + P4-01 automated test pass before merge, and the app can pin to the last-known-good manifest version with a kill switch if a bad rule is detected post-release. | P1-09 (manifest review), P3-K2 (rules engine versioning), P4-01 (rule tests) |
| R06 | Docker volume pruning destroys persistent data that does not regenerate (databases, uploads) | M — if the app offers a “clean Docker” junk category. Directly reported: docker volume prune wiping volumes on running containers described as a “disaster” with no backup (docker/for-linux #1237); an over-broad docker system prune -af cron job destroyed critical data considered “unused,” costing three hours to rebuild (techresolve.blog). |
Critical — database/upload data with no filesystem backup and no regeneration path. | Never call docker volume prune (unscoped); if a Docker junk module ships, it only targets dangling images and build cache (regenerable by definition), never named volumes, and only when the daemon reports zero attached containers, stopped or running. |
P3-J1..J4 (junk modules), P1-09 (manifest scope decision — should this ship in v1 at all) |
| R07 | Deleting an evicted iCloud Drive/Dropbox placeholder either does nothing locally (fine) or propagates as a delete to the cloud copy (not fine); a related macOS 14.4 bug independently destroyed file version history on eviction | M — mechanism is well documented: post-Sonoma, evicted files become “dataless” (SF_DATALESS-flagged, ~3KB on disk, no data extents) rather than visible stub files (Eclectic Light explainer); backup tooling that doesn’t understand this state can interpret a missing local placeholder as a user-initiated delete and propagate it to the cloud service ([same source]); separately, a macOS 14.4 bug caused eviction itself to trigger deletion of saved file versions (TidBITS Talk thread). | Critical if it propagates cloud-side — the user’s only copy of a file, gone, and “usually reversible via the cloud service’s interface, but tedious” is not a guarantee. | Detect SF_DATALESS/kCFURLIsUbiquitousItemKey before any operation; a dataless placeholder is reported as 0 bytes reclaimable and is never a delete target — full stop, regardless of rule match. Never call APIs that evict-and-delete; only ever act on fully-materialized local files. |
P3-F1..F3 (files modules), P3-K1 (scan engine — dataless-file detection) |
| R08 | Duplicate finder deletes the original and keeps the copy, or naively hardlinks over a file it misidentified as the duplicate | M — the duplicate-finder domain’s own documentation calls this out as a fundamental hazard: false positives from reaching the same file via two paths (symlinks, bind mounts) lose data, and “hardlinking” isn’t actually safe either because most implementations delete-then-link, so a wrong “original” call loses data even through the “safe” path (rmlint docs — Cautions). A long-time cleaner-tool user’s independent advice: never let a duplicate finder auto-delete photos, always compare manually, because higher-resolution/edited versions get misidentified as the redundant copy (Apple Community thread). | Critical — irreplaceable photos/documents; “duplicate” framing gives false confidence since the user assumes a copy survives. | Duplicate resolution is never fully automatic: identity is content-hash-based (not name/size heuristics), the “original” is the one the user picks or a deterministic, disclosed rule (oldest path, not last-scanned), and the loser is quarantined, never hardlinked-over in place. Symlinks and multiple paths to the same inode are dedup’d by inode before hashing so the same file is never presented as its own duplicate. | P3-F1..F3 (files/duplicate module), P3-K4 (destructive-operation API) |
| R09 | Killing a background process while it’s mid-write to a file the app is about to delete or has already deleted | M — generic but real risk any time a scan runs against live directories (browser profile mid-save, app writing a cache file it still needs this session). | High — corrupted or truncated file; usually recoverable from app-level backup or re-download, but can corrupt a profile/database file beyond app-level repair. | Before delete, re-check the file isn’t open for write (lsof-equivalent via proc_pidfds/F_GETPATH) and that mtime hasn’t changed since the scan; skip and flag rather than force-delete files that are actively held open. |
P3-K1 (scan engine), P3-K4 (destructive-operation API) |
| R10 | Stripping unused .lproj language files from an app bundle to “slim” it, breaking the code signature and hence future updates/launches |
M — well-documented mechanism: macOS code signing covers the entire bundle as a unit, so removing any resource file (including localization) invalidates it, producing EXC_CRASH (Code Signature Invalid) at next launch — seen directly in the wild, e.g. KeePassXC issue #4496; this is exactly the failure mode of the classic “Monolingual”/app-slimming genre of cleaner feature. |
High — the app becomes unlaunchable and often won’t self-update either (Sparkle/App Store update checks can also choke on an invalid signature), forcing a full reinstall; not data loss but a self-inflicted outage. | Do not ship a “remove language files” / app-slimming feature in v1. If ever added, it must re-sign the bundle after modification (ad-hoc at minimum) and verify launch + codesign --verify before reporting success — otherwise this category stays out of the rules manifest entirely. |
P1-09 (manifest scope decision), P3-A1..A6 (apps modules, if ever implemented) |
| R11 | Malware-scanning-style heuristic quarantines a legitimate system or third-party file as a false positive | L for v1 (product is a cleaner, not an AV, so this only matters if a “suspicious app” heuristic is added) but the pattern is catastrophic at scale when it happens: McAfee’s 2010 DAT bricked tens of thousands of Windows machines on a false positive, Sophos’s 2012 update flagged Java/Adobe/Google/its own installer as malware, CrowdStrike 2024 crashed 8.5M machines from a bad content update (all cited under R05). | Critical — same shape as R05: correlated fleet-wide damage from one bad heuristic, on files the user never asked to have evaluated. | The rules manifest is scoped to disk-space reclamation only — this app makes no malware/threat judgments about files, ever (see hard refusal rules). If threat-detection is ever considered, it ships as a separate, opt-in, non-destructive reporting feature, never wired to the delete path. | P1-09 (manifest scope), P3-K2 (rules engine — enforce scope boundary) |
| R12 | Scan or delete operates on files inside a Time Machine backup volume | L (only reachable if scan roots aren’t restricted) but Critical if it happens — Time Machine backups on APFS live as normal-looking files/snapshots on a mounted volume; a generic recursive scan that isn’t volume-aware could walk into /Volumes/<TM disk>/Backups.backupdb or an APFS backup volume and “reclaim” backup data, destroying the one thing meant to survive every other risk in this table. |
Critical — destroys the safety net for every other risk here; the backup is often the only copy of already-deleted originals. | Scan engine enumerates mounted volumes via DiskArbitration/statfs and hard-excludes any volume whose role is Time Machine backup (com.apple.TMBackup / API-reported backup role), or any path under a Backups.backupdb structure, unconditionally, before any rule is evaluated. |
P3-K1 (scan engine) |
| R13 | Scan or delete operates on a network/NAS-mounted volume | M if scan roots include arbitrary user-chosen folders (e.g., a Desktop symlinked to a NAS, or SMB-mounted home directory) — network filesystems have weaker consistency guarantees, slower/failing deletes, and can be shared with other users/machines. | High — deleting a file another machine or user is actively relying on; partial-failure deletes leave inconsistent state; TOCTOU window (R16) is much wider over network latency. | Scan engine checks statfs f_flags for MNT_LOCAL and refuses to scan/delete on non-local filesystems by default, with an explicit, separately-labeled opt-in if network scanning is ever supported. |
P3-K1 (scan engine) |
| R14 | Case-sensitivity assumptions break path matching (APFS supports both case-sensitive and case-insensitive formatting; the same folder name can exist twice on a case-sensitive volume) | L — most Macs run case-insensitive APFS, but case-sensitive is supported and used deliberately by some developers. | Medium — a rule intended for Cache matches or misses cache/CACHE inconsistently depending on volume format, causing either a missed cleanup or a wrong-item match. |
Rules engine never does ad hoc string case-folding; path matching always goes through the same volume-format-aware comparison API, and P4-01 rule tests run against both a case-sensitive and case-insensitive APFS fixture volume. | P3-K1 (scan engine), P4-01 (rule tests) |
| R15 | A symlink inside a scanned directory resolves outside the intended scan root, and the delete path follows it | M — a stray or malicious symlink under, e.g., ~/Library/Caches/SomeApp pointing at ~/Documents or / turns a routine cache clean into an arbitrary-path delete; this is the filesystem-level version of the exact “reaching the same file via two paths” trap documented for duplicate finders (R08) generalized to any scan. |
Critical — unbounded: whatever the symlink points to, inside the user’s permissions, is fair game. | Scan engine never follows symlinks when walking a tree for delete candidates (O_NOFOLLOW / lstat not stat); every path is canonicalized (realpath) and re-verified to still be inside the declared scan root immediately before the actual unlink call, not just at scan time. |
P3-K1 (scan engine), P3-K4 (destructive-operation API) |
| R16 | TOCTOU: the file at a path changes between scan time and the (possibly much later, user-confirmed) delete time | M — a Smart Scan can run minutes before the user reviews and confirms; in that window a sync client, another app, or the user themselves can replace what’s at that path with something new and wanted. | High — deletes content the scan never actually saw; recoverable only via quarantine. | The destructive-operation API re-identifies the target immediately before delete (inode number + mtime + size, or content hash for high-value classes) and re-checks it still matches what the rule engine flagged; any mismatch aborts that item and reports it as skipped, never silently deletes whatever is currently there. | P3-K4 (destructive-operation API) |
| R17 | A rule (or an overly broad scan root) matches and deletes the app’s own quarantine store | L (self-inflicted, requires a path-overlap bug) but structurally dangerous because it undermines every other mitigation in this table. | Critical — destroys the one undo mechanism the whole product’s safety story depends on; every prior “safely quarantined” deletion becomes unrecoverable retroactively. | The quarantine store’s path is hard-excluded from every scan root at the engine level (not via a rule that could be edited/misconfigured), checked by identity (resolved absolute path) rather than string prefix, and covered by a dedicated P4-03 test that asserts no rule/scan pass can ever enumerate it. | P3-K3 (quarantine/undo/restore), P3-K1 (scan engine) |
| R18 | User runs a scan/clean while a build, render, or long-running job is writing to what looks like a disposable cache (DerivedData, render caches, Docker build cache) | H — this is the developer/creative-pro version of R01/R02: DerivedData, node_modules/.cache, render-farm scratch directories all look exactly like “junk” by path convention while actively backing a running job. |
High — hours of build/render work lost or a build that fails confusingly mid-flight; usually re-runnable but costly, and for some render pipelines the source state may not be trivially reproducible. | Cache-category rules check for recent write activity (mtime within a short window) and open-file-handle status before offering a path for deletion, same mechanism as R09; Smart Scan surfaces a warning banner rather than auto-including anything under known build-tool cache roots if the owning process is currently running. | P3-J1..J4 (junk modules), P3-K1 (scan engine), P3-K5 (Smart Scan orchestrator) |
Hard refusal rules
Things the app must simply refuse to do, ever, regardless of what the user (or a rule, or a UI shortcut) asks for:
- Never permanently delete on the first action. Every destructive operation goes through quarantine (P3-K3) via the single audited delete API (P3-K4) — there is no code path that calls
unlink/FileManager.removeItemdirectly from a rule or module. (R01, R02, R08, R16) - Never delete a dataless/evicted iCloud or cloud-sync placeholder, and never call an API that could propagate a local delete to the cloud copy. A 0-byte-on-disk placeholder is reported as 0 bytes reclaimable, period. (R07)
- Never operate on a Time Machine backup volume, or any volume whose role is a backup destination. Excluded at the scan-root level, unconditionally, not via an editable rule. (R12)
- Never operate on a non-local (network/NAS) filesystem without an explicit, separately-labeled opt-in. (R13)
- Never follow a symlink when resolving a delete target, and never delete a path that doesn’t re-verify as inside its declared scan root at delete time. (R15, R16)
- Never touch the app’s own quarantine store as a scan target. Excluded at the engine level by resolved identity, not by rule. (R17)
- Never expose an unauthenticated or under-authenticated privileged helper. Every XPC call is checked against the caller’s code signature before any privileged action runs; no raw-shell-string execution surface, ever. (R04)
- Never make a malware/threat-safety judgment about a file. The rules manifest’s job is disk-space reclamation only — this product does not decide what’s “dangerous,” only what’s regenerable junk. (R11)
- Never ship a rules-manifest change straight to 100% of installs. Every rule change is staged/canaried and passes P4-01 automated tests and P1-09 human review first; a kill switch can roll back a bad manifest fleet-wide. (R05)
- Never call an unscoped prune/delete-all primitive (e.g.,
docker volume prunewithout narrowing to dangling images/build cache) — no operation is allowed to define “unused” more broadly than “definitely regenerable and definitely unreferenced.” (R06)
Test spec seed for P4-03 (data-loss safety suite)
One concrete test per high-severity risk (score ≥ 6), proving the mitigation actually holds:
- R01/R02 — live-app-data protection: Fixture with an installed app whose
Application Supportfolder contains a file with a fresh mtime and a recognized DB extension. Run orphan detection + Smart Scan. Assert: the file is never proposed as a delete candidate, or if proposed, is quarantined (not permanently deleted) and requires explicit per-item confirmation. - R03 — launch agent denylist: Fixture launch agents with labels matching the VPN/backup/security denylist patterns. Run the junk-item scan. Assert: none of them ever appear as removable candidates, regardless of “unused” heuristics.
- R04 — privileged helper auth: Spin up the helper, connect from a process with an unrelated/unsigned identity (or a spoofed bundle ID). Assert: every privileged call is rejected before any filesystem/process action occurs; assert the helper has no raw-command-execution entry point at all (static check, not just a runtime probe).
- R05 — manifest rollback: Ship a manifest version flagged bad (via kill switch). Assert: the app falls back to the last-known-good manifest and performs zero deletes under the bad version, even if already fetched.
- R06 — Docker scope: Fixture Docker daemon with one named volume with data and one dangling image. Run the Docker junk module. Assert: the volume is never a delete candidate under any circumstance; only the dangling image is.
- R07 — dataless file skip: Fixture file with
SF_DATALESSset (or thekCFURLIsUbiquitousItemKeyubiquity flag simulated). Run scan. Assert: reported reclaimable size for that item is 0 and it is excluded from all delete batches. - R08 — duplicate identity: Fixture: two hardlinks to one inode presented via different scan paths, plus a true duplicate pair where the “older” file is actually the user’s edited/higher-resolution version. Assert: the same-inode pair is never proposed as a duplicate pair; assert the resolution requires either explicit user pick or the disclosed deterministic rule, and the loser goes to quarantine, never a direct hardlink-over.
- R15 — symlink escape: Fixture: a scan root containing a symlink pointing outside the root (e.g., to a sibling temp directory with a canary file). Run scan + delete. Assert: the canary file outside the root is untouched, and the scan does not enumerate anything reached only via the symlink target.
- R16 — TOCTOU: Fixture: scan identifies file A; before the delete call executes, replace file A’s content/inode out from under the pending delete (simulating a race). Assert: the delete API detects the identity mismatch and aborts that item as “skipped — changed since scan” rather than deleting whatever now exists at that path.
- R18 — active-job protection: Fixture: a cache-category path with a held-open file descriptor (simulating an active build/render). Run Smart Scan. Assert: the path is excluded or flagged with a warning, not silently included in a default “clean everything” batch.
Sources
- Pearcleaner #477 — Removed important data from Library/Application Support
- Pearcleaner #203 — Orphaned Files showing up for apps that are not deleted
- Pearcleaner GHSA-gr2j-65fh-8pvc — Unauthenticated access to privileged XPC helper allows root command execution
- theevilbit — Secure coding XPC Services, Part 3: Incorrect client verification
- Forklift LPE writeup (Insinuator.net)
- Onyx breaking Astrill VPN (Mac-Forums)
- CleanMyMac X deleting launch agents, breaking Cloudflare WARP (Apple Community)
- Long-time user on CleanMyMac 2/3 damaging Photos Library (Apple Community)
- rmlint documentation — Cautions (why it’s hard to write a dupe finder)
- docker/for-linux #1237 — volume prune deleted all volumes even on running containers
- techresolve.blog — what important data can actually be lost when pruning
- Eclectic Light Company — Explainer: File Provider and cloud services (dataless files)
- TidBITS Talk — macOS 14.4 bug deletes old versions of iCloud Drive files that are evicted locally
- KeePassXC #4496 — Cannot open app after latest macOS update: Code Signature Invalid
- McAfee 2010 false-positive update incident summary
- PCWorld — Sophos antivirus glitch causes false positive chaos (2012)
- CrowdStrike 2024 outage summary — 8.5M Windows devices