---
name: macos-cleaner
description: >-
  Diagnoses and safely reclaims macOS disk space. Use when a Mac is low on
  storage, reports “Caching needs more space,” shows large Apple Content
  Caching or AssetCacheManagerUtil usage, or needs analysis of caches, logs,
  application remnants, large or duplicate files, Docker or OrbStack,
  Chromium code-sign clones, Homebrew, npm, pip, Xcode, and other developer
  storage. Routes known suspects to targeted read-only diagnosis before broad
  scans, distinguishes nominal path accounting from physical release, requires
  an impact-and-recovery plan plus explicit confirmation before state changes,
  and verifies disk space and co-resident services afterward.
---

# macOS Cleaner

Diagnose the actual source of disk pressure, reclaim only approved space, and prove that the cleanup did not damage user data or co-resident services.

## Entry router

Choose the narrowest path that can answer the request:

| User signal | Route |
|---|---|
| Apple Content Caching, `AssetCacheManagerUtil`, `CacheUsed`, `ActualCacheUsed`, iCloud cache, or “Caching needs more space” | Read `references/apple_content_caching.md` completely before probing or proposing commands |
| Docker, OrbStack, images, containers, or volumes | Read `references/docker_analysis.md`; inspect every object and never use prune-family commands |
| Docker build cache | Measure with `docker builder du`; this skill reports it but does not delete it because Docker exposes category-wide prune controls rather than per-record intent |
| Chrome, ChromeDriver, Playwright, Codex Computer Use, Edge, or Chromium has large `*.code_sign_clone` directories | Read `references/chromium_code_sign_clones.md` completely; use the bundled analyzer to separate active, inactive, and unknown exact children and never equate `du` with physical release |
| A named cache, directory, application, or service is already the suspect | Inspect that target first and read the matching semantics in `references/cleanup_targets.md`; do not start a home-directory or whole-disk scan |
| The source is genuinely unknown | Use the general analysis workflow below; Mole is optional, not the universal first step |

User-provided scope exclusions override every generic scan suggestion. Do not inspect personal directories, credentials, databases, application state, or unrelated services when the user excludes them.

## Safety and authorization contract

1. **Separate observation from mutation.** Complete a read-only diagnosis first. Do not delete, stop a service, edit settings, install or upgrade tools, or run a cleanup preview that may mutate state during that phase.
2. **Confirm the exact target.** On a remote Mac, record the current host identity before any other work. Never infer the machine from an IP, old PID, directory name, or prior report.
3. **Plan before asking.** Before any state change, list every command, what it changes, expected physical space reclaimed, impact, recoverability, and postconditions. Then stop if the user requested a plan-only phase.
4. **Require explicit approval.** If the user supplies an exact confirmation phrase, require that phrase. Otherwise ask for unmistakable approval of the listed commands and targets. Approval for one plan does not authorize a fallback or a wider cleanup.
5. **Use precise supported controls — and check them for known defects first.** Prefer an application's supported cache-management command or an exact object ID. "Official" and "precise" are not evidence of safety, so before the first supported-control command runs, do a known-issue check on that exact command at the installed version: a search of the tool's issue tracker for the command name, plus its changelog for fixes landing in a later release than the one installed. A command with an open, version-relevant defect is not a supported control for this skill — treat it like the category-wide commands in the next rule and either upgrade past the fix or use an exact-path alternative. If no supported control exists, an exact application-owned cache directory may be removed only after verifying its owner, confirming the application is stopped or the directory is otherwise inactive, explaining rebuild/redownload impact, and receiving approval. Never target a broad cache root or active application state.
6. **Never use Docker prune-family commands.** This includes image, container, volume, system, builder, and buildx prune. Category-wide deletion cannot express per-object user intent.
7. **Avoid broad destructive shell forms.** Do not recommend or execute broad `rm -rf` or glob deletion. For exact approved ordinary files, prefer Finder Trash. The bundled legacy helper permanently deletes and has only the limited guards documented below; never treat it as equivalent to Trash.
8. **Preserve valuable state.** Never target user documents, credentials, SSH material, active databases, application configuration, or running-service state merely to increase the reported savings. Read `references/safety_rules.md` before any file deletion.
9. **Execution follows the user's authorization.** If the user asks only for analysis or wants to run commands personally, hand off the commands. If the user asks the agent to fix the machine and explicitly confirms the scoped plan, execute the exact approved commands and verify them. Unattended recurring deletion logic needs separate approval before it is written or enabled.
10. **Fail fast.** An unexpected non-zero command, a mismatched postcondition, an unexpected target, or a changed dependency stops the cleanup. Interpret documented probe statuses such as `lsof` exit 1 with empty output before deciding they are failures. Report the partial state; do not improvise a fallback.
11. **Before promising physical release from any deletion, name the mechanism — and get it from the creating command, not from a guess.** A `df` gap between a nominal `du` total and actual reclaim means one candidate mechanism is at work, and **the strongest evidence is the verbatim command that created the folder**, because the copy verb alone decides the space semantics. Find it in session history before theorizing; a folder-name or size-based inference is not a mechanism. Only if no command can be found does the gap stay `unknown` — do not substitute the most plausible-sounding mechanism for one you can demonstrate.

    The mechanisms, distinguished by what `du` reports and what deletion releases (all four measured on 2026-09-19, 1 GiB source, drift-controlled):

    | Created by | `du` nominal | Deleting the copy releases | Fingerprint |
    |---|---|---|---|
    | `cp` (bare) / `cp -a` | full, counted per path | full | independent inodes, independent extents |
    | **`cp -c` / `cp -cR`** (clonefile) | **full, counted per path** | **≈0** | **different inode, `nlink=1`, shared extent** |
    | `ln` / `cp -l` (hard link) | **counted once** — sibling reads `0` | **≈0** | **same inode, `nlink=2`** |
    | local APFS snapshot | full | ≈0 until snapshots are thinned | `tmutil listlocalsnapshots /` |

    Two traps this table exists to kill. **The clonefile and hard-link rows are opposite on `du` but identical on deletion** — both release ≈0, so "deleting it freed nothing" cannot tell them apart, while `du` can: a hard-linked sibling counts once (or as `0`), a clonefile copy counts full. And **calibrating the wrong copy verb proves nothing**: a 98 GiB gap was twice attributed to the wrong mechanism because the probe used bare `cp` when the folder had been built with `cp -cR`. Before any deletion, run the probe with the *same flags as the creating command*. Clonefile is worth flagging as the common case — macOS `cp` defaults to it for `-c`, and directories copied for a delivery/kit routinely carry it.

## Phase contract

Use this state machine for every cleanup:

1. **Observe — read-only.** Capture identity, disk baseline, the suspected subsystem's status and configuration, physical allocation, and critical-service health.
1b. **Authorize stateful inspection when unavoidable.** If deeper evidence requires creating a temporary container, pulling an image, mounting a volume, or writing a snapshot, first finish the metadata-only observation, list the exact inspection commands and their side effects, and obtain separate approval. Inspection approval is not cleanup approval.
2. **Plan — no mutation.** Pass the Phase 2 entry gate first (four steps: target confirmation, classification table, gate rules, checker — see below), then explain findings, commands, impact, recovery, expected release, and success criteria. Stop at the confirmation gate.
3. **Execute — approved scope only.** Re-read live state immediately before acting, then run each approved command separately and check its exit status and postcondition.
4. **Verify — independent readback.** Measure disk space and subsystem state again, recheck protected services, and observe long enough to detect immediate refill.

Do not compress phases 2 and 3 into one message. A plan printed beside a cleanup command is not a confirmation gate.

## Phase 1: read-only diagnosis

### Establish the baseline

At minimum, capture:

```bash
/bin/date "+%F %T %Z %z"
/usr/sbin/scutil --get ComputerName
/usr/sbin/scutil --get LocalHostName
/usr/bin/sw_vers
/bin/df -k /System/Volumes/Data
/bin/df -h /System/Volumes/Data
```

Use `df -k` for calculations and `df -h` for the human-readable report. Treat an extension, label, or old report as a hint until the live command confirms it.

Establish the success target before an unknown-source scan. Copy a user-supplied free-space or capacity target exactly. If the user supplied none, report the current values and ask for a target in GiB, capacity percentage, or both; do not invent one. A named-suspect diagnosis may continue without a cleanup target, but the ordered unknown-source scan cannot claim a stop condition until the target is explicit.

Keep this first phase read-only. Do not run `scripts/cleanup_report.py` yet: it creates a local state directory and snapshot file. Preserve the command output in the report instead. On a remote target, always run the direct `df` commands on that host; the local helper must not measure the controller Mac by mistake.

### Follow the named suspect before broad scans

- Query the subsystem's own status and settings.
- Measure physical allocation with a bounded `du` on the exact data path only when permissions and user scope allow it.
- Distinguish logical content size, sparse-file apparent size, purgeable space, and physically allocated bytes.
- For APFS clones, label `du` as path-accounted or nominal rather than physical: shared extents can be attributed to every path, so actual release remains unknown until deletion and `df` readback.
- For a suspected growing log, record exact file sizes at two or more timestamps. One large file or one recent mtime does not prove sustained growth.
- Capture the current process, listeners, launch mechanism, and supported health probe of any co-resident service the user marks as critical. Re-resolve PIDs at each checkpoint.

If the known suspect alone can meet the user's free-space target, do not scan unrelated personal or development directories “just in case.”

### General analysis when the source is unknown

Run the smallest ordered sequence that can identify enough physical space to meet the target. Stop only when candidates with supported exact actions and defensible expected physical release can meet it. Raw allocation totals, logical cache sizes, shared Docker layers, Trash moves, and unverified “potential savings” do not satisfy the stop condition.

Do not make the physical-confidence ranking hide the user's largest visible
hotspot. When a nominal/path-accounted candidate is larger than the recommended
action, show both numbers and explain why its physical release is uncertain.
Rank by defensible physical release, safety, and effort; report nominal size as
a separate column instead of silently treating it as either zero or fully
reclaimable.

| Order / signal | Read-only action | Stop or continue |
|---|---|---|
| 1. Always | Capture identity and `df -k/-h`; inventory user exclusions | Stop on target mismatch |
| 2. Cache/log pressure, and `~/Library/Caches`, `~/Library/Logs`, plus the XDG dev caches are approved read scopes | `uv run scripts/analyze_caches.py --user-only --include-dev` | Stop when measured candidates can meet the target |
| 3. Developer tools are present and the script's fixed scope is approved | `uv run scripts/analyze_dev_env.py` reads Docker/package managers plus existing `~/Projects`, `~/workspace`, `~/dev`, `~/src`, and `~/code` roots | Route Docker/OrbStack findings to their dedicated reference; skip this helper when any fixed root is out of scope |
| 4. Uninstalled-app residue is plausible and its fixed roots are approved | `uv run scripts/find_app_remnants.py` reads `/Applications`, `~/Applications`, and four documented `~/Library` application-state roots | Treat every result as a candidate, never proof of abandonment; skip when that scope is not approved |
| 5. A content-bearing path is explicitly approved | `uv run scripts/analyze_large_files.py --threshold 100MB --path "<approved-path>"` | Do not substitute `~`, Downloads, Documents, or the data-volume root when no path was approved |
| 6. Still unknown after bounded checks, and the user explicitly approves Mole's fixed broad scan roots | Read `references/mole_integration.md` and use `mo analyze` through a TTY | Mole cannot accept an arbitrary path scope; skip it when approval is narrower than its documented roots |

An `<approved-path>` is an exact path the user named or explicitly accepted after its scope was described. If none exists, skip large-file and duplicate-content scanning, state that this evidence branch was not authorized, and continue with non-content-bearing evidence. Do not install or upgrade Mole during a read-only phase unless the user separately authorizes that change.

Mole's analyzer scans a fixed set that includes the home directory, application data, system libraries, applications, and volumes. Navigation inside the results does not make the underlying scan path-scoped. If that broad read scope is not approved, do not run Mole; stop with the bounded evidence already collected or ask for the missing scan authorization in the plan.

For an explicitly approved duplicate-file investigation, read the “Optional duplicate files” section in `references/cleanup_targets.md`. It is read-only and never uses an automatic-delete option.

**A large data folder is not a cache — proposing its deletion needs an evidence chain, not a size ranking.** When discovery surfaces a big project-asset / media / dataset directory (not a cache, not an app remnant), do NOT put it in the action set on size alone: read `references/proving-redundancy-before-deletion.md` and climb its ladder (file-level duplication → creation-origin → reference check → session-history tool-call census → .DS_Store manual-usage trace → the project's own decision records) before proposing anything. The deliverable is the evidence table; the unprovable row (purely manual usage) goes to the user, never gets papered over.

When discovery is fanned out to sub-agents, each returns candidates and measurements only — the classification, the acceptance, and the proposal happen in the session that runs the Phase 2 entry gate. A sub-agent's inventory is input to the classification table, never the plan.

### Docker and OrbStack

Read `references/docker_analysis.md` before reporting Docker savings. List every image, container, and volume individually; inspect references and database-like contents; use actual sparse-file allocation rather than apparent size. A resource reported as dangling is not proof that its data is worthless. Build-cache measurement is supported, but build-cache deletion is deliberately out of scope because the available Docker controls are prune-family operations.

## Phase 2: report and stop at the gate

### Phase 2 entry gate — four steps before any plan text

A real 2026-09-19 run followed the Phase 1 machinery to the letter and still shipped three bad plans: it proposed preserve-by-default caches (npm `_cacache`, Playwright browsers, Homebrew) as a "low-risk combo" because the rule lives in `references/cleanup_targets.md` — a file the discovery workflow never opens; it proposed `uv cache prune` as zero-impact from a `--help` line plus size ratios, never verifying semantics or the installed version; and it led with 2 GB items beside a 91 GB candidate because no free-space target had been set. The rules that would have caught all three already existed in this skill — in a reference the procedure never opened, which makes them not rules in practice. A prose checklist is only one level better than the reference it summarizes: an independent review of the first draft of this gate found thirteen blocking defects, the deepest being that the gate itself had no mechanical enforcement. Step 4 is therefore a script, not a promise — run it, or the plan does not exist.

1. **Confirm the free-space target.** A named-suspect diagnosis may continue without one (the Phase 1 exemption); an unknown-source scan may not — if the user supplied none, ask before ranking. A scan without a target has no stop condition and produces size-sorted noise.
2. **Open `references/cleanup_targets.md`** and classify every candidate the discovery produced into this table, written to a file (step 4 parses it):

   | Target | Nominal size | Physical confidence | Class | Governing rule (verbatim quote) | Expected physical release + basis | Restoration cost | Verdict |
   |---|---|---|---|---|---|---|---|
   | exact target: a path, a service setting (e.g. `AssetCacheManagerUtil` or a plist key), or a supported control (e.g. `chrome://settings/clearBrowserData`) | du output | path-accounted (Phase 1's label for APFS-shared or nominal du) / sparse-aware / engine-reported (a subsystem's own accounting, e.g. Docker `system df`) / unknown — never enter a nominal number as release | PRESERVE / PROPOSABLE / REBUILDABLE / USER-DATA / USER-DECISION | a quote from `cleanup_targets.md`, the route's dedicated reference, or this skill's own SKILL.md (the checker verifies every quote against the whole bundle and prints per-file provenance, so SKILL.md-only quotes stay visible); "no rule found" is itself a finding | an estimate plus how it was derived; unknown is an allowed value | redownload/rebuild cost, or n/a | in action set / not in action set / unlocked by user (quote the direction) |

   - **PRESERVE** — every row of the preserve-by-default table in `cleanup_targets.md` (opened in this step, quoted in the table — step 4 verifies the quotes, so a fabricated quote fails). Out of the action set unless (a) verified never-used evidence exists for the target, or (b) the user explicitly names that target for cleaning knowing the cost. Accepting a cost you stated is not (b): the user must name the target themselves.
   - **PROPOSABLE** — verified never-used evidence (a dead project, an explicit user statement, an artifact check). Size ratios are not evidence: "the cache holds 586 environments, 28 project venvs exist" proves nothing about the other 558.
   - **REBUILDABLE** — not in the preserve-by-default table, has a supported management command or cheap rebuild (Homebrew, pip), restoration cost stated in the row.
   - **USER-DATA** — never in an action set; report the location, never a command.
   - **USER-DECISION** — the user's workflow or ownership knowledge decides; ask, don't propose. Also the home for targets no reference covers ("no rule found"): the plan asks before proposing anything for them.

   If no reference anywhere covers a target, say "no rule found" and classify it USER-DECISION — do not stretch an unrelated rule to cover it.

3. **Apply the gate rules.** Step 4's checker enforces each one mech