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:

  1. 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.removeItem directly from a rule or module. (R01, R02, R08, R16)
  2. 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)
  3. 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)
  4. Never operate on a non-local (network/NAS) filesystem without an explicit, separately-labeled opt-in. (R13)
  5. 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)
  6. Never touch the app’s own quarantine store as a scan target. Excluded at the engine level by resolved identity, not by rule. (R17)
  7. 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)
  8. 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)
  9. 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)
  10. Never call an unscoped prune/delete-all primitive (e.g., docker volume prune without 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:

Sources