Skip to content

mac-storage-cleaner: v2.0.0 hardening update - #792

Open
JubaKitiashvili wants to merge 9 commits into
davila7:mainfrom
JubaKitiashvili:update-mac-storage-cleaner-v2
Open

mac-storage-cleaner: v2.0.0 hardening update#792
JubaKitiashvili wants to merge 9 commits into
davila7:mainfrom
JubaKitiashvili:update-mac-storage-cleaner-v2

Conversation

@JubaKitiashvili

@JubaKitiashvili JubaKitiashvili commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Update of the merged skill (#721) to v2.0.0 — a substantial safety-hardening release. Upstream repo/changelog: https://github.com/JubaKitiashvili/mac-storage-cleaner/blob/main/CHANGELOG.md

What changed (all 7 files, +1012/−66):

  • "Never" tier is now enforced in code, not just documentation: subtree denials for Photos libraries, iOS backups (MobileSync), Keychains, Mail/Messages data, and ~/.ssh/~/.aws/~/.gnupg — the validator refuses them even if the reasoning layer were ever wrong, and it now also runs as defense-in-depth inside every automatic deletion loop.
  • --dry-run (full preview, zero deletions/log writes, guards applied identically to a real run), a user whitelist file (case-insensitive, protects subtrees), and fail-closed process guards (Xcode toolchain, Gradle daemon, running Electron apps — "can't tell" always means "skip").
  • Keep-N retention instead of full wipes: DeviceSupport keeps the 2 newest OS versions; auto-updating AI CLIs keep their active version, resolved via launcher symlink (never mtime).
  • Three-stage reversible Trash chain (/usr/bin/trash → Finder → same-volume mv) with per-method audit logging; deletion refuses to run if the audit log is unwritable.
  • Expanded coverage: Android Studio/SDK, Carthage/Poetry/mise/Composer/RubyGems/conda (owner command only), Handoff clipboard buffers (60-min age gate), crash reports (30-day age gate), guarded Electron/Chromium app caches.
  • Honest accounting: partial removals reported as partial, unmeasurable sizes as size?, whitelist-aware survey totals, 5 MB log rotation.

For the security scan (same notes as #721, still true): the skill makes no network calls and no credential access; every rm -rf is gated behind a hard-coded allowlist plus the new deny-list validator; ~/.ssh/keychain/backup paths appear only inside the refuse-to-touch protection lists. It now ships with a 68-test bats suite upstream, including a property-tested dangerous-path corpus (every corpus entry must be refused) and adversarial symlink cases — audited by an independent multi-model review panel before release.

Happy to adjust anything — thanks for maintaining this!


Summary by cubic

Upgrades mac-storage-cleaner to v3.0.1 to prevent unintended deletions and enforce bulk protections. Old: clean-safe.sh deleted by default; New: it previews by default and requires --apply. Also fixes an octal parsing bug in the Trash size cap so bulk limits are always enforced.

  • Migration: pass --apply for real deletions; unknown flags or multiple args now exit 2.
  • Bulk protections: trash-items.sh refuses >100 items or >5GB without --force; cap enforcement fix prevents fail-open cases.
  • Area: components (cli-tool/components/skills/productivity/mac-storage-cleaner); adds agents/openai.yaml; no new components; no docs/components.json regeneration.
  • Env vars unchanged: MSC_WHITELIST_FILE, MSC_TRASH_BIN, MSC_DEVICE_SUPPORT_KEEP, MSC_AI_AGENTS_KEEP, MSC_DRY_RUN, MSC_ALLOW_UNLOGGED; no new secrets.

Written for commit 627e088. Summary will update on new commits.

Review in cubic

Never-tier enforced mechanically (subtree denials for Photos/backups/
Keychains/Mail/SSH keys, wired into every deletion loop), --dry-run,
user whitelist, fail-closed process guards, keep-N retention, three-stage
reversible Trash chain, expanded coverage (Android/iOS tooling, Electron
caches, Handoff buffers), honest accounting. 68-test suite upstream.
@github-actions github-actions Bot added the review-pending Component PR awaiting maintainer review label Aug 11, 2026
@github-actions

Copy link
Copy Markdown
Contributor

👋 Thanks for contributing, @JubaKitiashvili!

This PR touches cli-tool/components/** and has been marked review-pending.

What happens next

  1. 🤖 Automated security audit runs and posts results on this PR.
  2. 👀 Maintainer review — a human reviewer validates the component with the component-reviewer agent (format, naming, security, clarity).
  3. Merge — once approved, your PR is merged to main.
  4. 📦 Catalog regeneration — the component catalog is rebuilt automatically.
  5. 🚀 Live on aitmpl.com — your component appears on the website after deploy.

While you wait

  • Check the Security Audit comment below for any issues to fix.
  • Make sure your component follows the contribution guide.

This is an automated message. No action is required from you right now — a maintainer will review soon.

@greptile-apps

greptile-apps Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The update substantially expands and hardens the macOS storage-cleaning skill, including preview-by-default cleanup, protected-path validation, retention policies, audit logging, process guards, and reversible Trash handling.

  • Adds cross-agent discovery metadata and invocation instructions.
  • Adds whitelist, dry-run, retention, process-safety, and protected-path enforcement.
  • Expands cache coverage and improves deletion accounting and logging.

Confidence Score: 4/5

The PR is not yet safe to merge because the outstanding process guard can still mistake an unrelated process for the application that owns a cache and skip required cleanup.

The cleaner still searches the complete process snapshot for the raw Application Support directory name, so a generic name such as Code can match an unrelated command and incorrectly classify the owning application as running.

Files Needing Attention: cli-tool/components/skills/productivity/mac-storage-cleaner/scripts/clean-safe.sh

Important Files Changed

Filename Overview
cli-tool/components/skills/productivity/mac-storage-cleaner/scripts/clean-safe.sh Adds preview-by-default cleanup and multiple guarded deletion flows, but the previously reported unscoped substring process check remains.
cli-tool/components/skills/productivity/mac-storage-cleaner/scripts/lib.sh Adds protected-path validation, portable version retention, whitelist handling, process helpers, logging rotation, and reversible Trash fallbacks; the previously reported retention defects are fixed for the documented environment.
cli-tool/components/skills/productivity/mac-storage-cleaner/scripts/trash-items.sh Adds strict argument handling, dry-run behavior, audit-log enforcement, blast-radius limits, path validation, and explicit result reporting.
cli-tool/components/skills/productivity/mac-storage-cleaner/SKILL.md Documents preview-first operation, expanded safety guarantees, cross-agent script discovery, configuration, and cleanup behavior.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  U[User or agent invokes cleaner] --> P{Preview or apply?}
  P -->|Preview| G[Run whitelist, path, age, and process guards]
  P -->|Apply| L{Audit log writable?}
  L -->|No| X[Refuse destructive run]
  L -->|Yes| G
  G --> R{Target permitted and idle?}
  R -->|No| S[Skip or refuse]
  R -->|Yes| D[Remove safe cache or move item to Trash]
  D --> A[Record outcome and reclaimed size]
Loading

Reviews (9): Last reviewed commit: "mac-storage-cleaner: v3.0.1 — fix octal ..." | Re-trigger Greptile

Comment on lines +323 to +329
if command -v pgrep >/dev/null 2>&1; then
if pgrep -x "$app" >/dev/null 2>&1; then guard_rc=0
else guard_rc=$?; [ "$guard_rc" -eq 1 ] || guard_rc=2; fi
else
guard_rc=2
fi
if [ "$guard_rc" != 1 ]; then

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Process-name guard fails open

When a running app's process name differs in spelling or capitalization from its Application Support directory, pgrep -x "$app" returns 1 and the cleanup treats the app as idle, deleting cache directories while they are in use and potentially disrupting the app or leaving partially rewritten cache state.

@JubaKitiashvili

Copy link
Copy Markdown
Contributor Author

@davila7 ready for review 🙏 — and a heads-up on the red X's, same story as #721:

  • SkillSpector (changed skills): the scan itself finished clean — 0 HIGH/CRITICAL findings (the composed result table is visible in the job log) and the skillspector check-run resolved neutral. The workflow step then failed only on Resource not accessible by integration while posting its comment — the standard read-only GITHUB_TOKEN restriction for fork PRs.
  • Security Audit: it audits the whole repo, and the 410 failed components are pre-existing (top offenders in the log: vercel-edge-function, prompt-engineer, neon-expert, agent-overview, unused-code-cleaner). mac-storage-cleaner appears nowhere in the failure list. That workflow also died on the same fork-token comment restriction.

A privileged re-run ("approve and run") should turn SkillSpector green; the Security Audit result reflects repo-wide state rather than this PR.

Context for the diff size (+1012/−66): v2.0.0 is a safety-hardening release — the "never delete" tier (Photos libraries, iOS backups, Keychains, Mail/Messages, ~/.ssh/~/.aws/~/.gnupg) is now mechanically refused by the path validator rather than being documentation-only, with the validator also wired into every automatic deletion loop as defense-in-depth. Full changelog: https://github.com/JubaKitiashvili/mac-storage-cleaner/blob/main/CHANGELOG.md — happy to adjust anything!

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 issues found across 7 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="cli-tool/components/skills/productivity/mac-storage-cleaner/scripts/find-extras.sh">

<violation number="1" location="cli-tool/components/skills/productivity/mac-storage-cleaner/scripts/find-extras.sh:61">
P2: When `Current` cannot be resolved, this report marks every framework version as removable, allowing the user to trash the browser's active framework and break the app. Treat an unresolved target as unknown and skip that framework (or mark all versions unsafe) instead of defaulting to `none`.</violation>
</file>

<file name="cli-tool/components/skills/productivity/mac-storage-cleaner/scripts/lib.sh">

<violation number="1" location="cli-tool/components/skills/productivity/mac-storage-cleaner/scripts/lib.sh:78">
P2: Keep-N can delete one of the newest real DeviceSupport versions when a symlinked directory is present: this test counts the symlink, but `clean-safe.sh` later skips emitted symlinks. Excluding symlinks before incrementing `i` keeps N real versions and avoids an unnecessary re-download.</violation>

<violation number="2" location="cli-tool/components/skills/productivity/mac-storage-cleaner/scripts/lib.sh:239">
P2: The deny validator only exact-matches the system-user roots (`/system`, `/library`, `/usr`, `/usr/local`, `/var`, `...`), while subtree denials are applied only to the MobileSync/Keychains/Mail/Messages/`.ssh`/`.aws`/`.gnupg`/Photos list. A path strictly below a system root (e.g. `/System/Library/...`, `/usr/local/...`, `/var/folders/...`) is therefore not refused, and the ancestor-symlink re-check uses the same equality-only list, so a `$HOME` symlink pointing into `/System`/`/Users`/etc. can smuggle a deeper path past the string checks yet not be caught on resolution. That undercuts the "Never tier mechanically refused" guarantee this PR advertises. Consider denying these roots as subtrees (and any ancestor-resolved path beneath them) rather than exact leaves.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread cli-tool/components/skills/productivity/mac-storage-cleaner/scripts/clean-safe.sh Outdated
Comment thread cli-tool/components/skills/productivity/mac-storage-cleaner/scripts/clean-safe.sh Outdated
Comment on lines +61 to +62
cur=$(readlink "$fw/Current" 2>/dev/null)
cur=$(basename "${cur:-none}")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When Current cannot be resolved, this report marks every framework version as removable, allowing the user to trash the browser's active framework and break the app. Treat an unresolved target as unknown and skip that framework (or mark all versions unsafe) instead of defaulting to none.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At cli-tool/components/skills/productivity/mac-storage-cleaner/scripts/find-extras.sh, line 61:

<comment>When `Current` cannot be resolved, this report marks every framework version as removable, allowing the user to trash the browser's active framework and break the app. Treat an unresolved target as unknown and skip that framework (or mark all versions unsafe) instead of defaulting to `none`.</comment>

<file context>
@@ -50,6 +50,30 @@ for d in "$HOME/Library/Containers/"*; do
+  "/Applications/Microsoft Edge.app/Contents/Frameworks/Microsoft Edge Framework.framework/Versions" \
+  "/Applications/Brave Browser.app/Contents/Frameworks/Brave Browser Framework.framework/Versions"; do
+  [ -d "$fw" ] || continue
+  cur=$(readlink "$fw/Current" 2>/dev/null)
+  cur=$(basename "${cur:-none}")
+  for v in "$fw"/*; do
</file context>
Suggested change
cur=$(readlink "$fw/Current" 2>/dev/null)
cur=$(basename "${cur:-none}")
cur=$(readlink "$fw/Current" 2>/dev/null) || {
echo " (Current link unreadable; skipping this framework)"
continue
}
[ -n "$cur" ] || {
echo " (Current link empty; skipping this framework)"
continue
}
cur=$(basename "$cur")
[ -d "$fw/$cur" ] || {
echo " (Current target unavailable; skipping this framework)"
continue
}

Comment thread cli-tool/components/skills/productivity/mac-storage-cleaner/scripts/survey.sh Outdated
i=0
while IFS= read -r child; do
[ -n "$child" ] || continue
[ -d "$dir/$child" ] || continue

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Keep-N can delete one of the newest real DeviceSupport versions when a symlinked directory is present: this test counts the symlink, but clean-safe.sh later skips emitted symlinks. Excluding symlinks before incrementing i keeps N real versions and avoids an unnecessary re-download.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At cli-tool/components/skills/productivity/mac-storage-cleaner/scripts/lib.sh, line 78:

<comment>Keep-N can delete one of the newest real DeviceSupport versions when a symlinked directory is present: this test counts the symlink, but `clean-safe.sh` later skips emitted symlinks. Excluding symlinks before incrementing `i` keeps N real versions and avoids an unnecessary re-download.</comment>

<file context>
@@ -40,8 +41,48 @@ SAFE_PATHS=(
+  i=0
+  while IFS= read -r child; do
+    [ -n "$child" ] || continue
+    [ -d "$dir/$child" ] || continue
+    i=$((i + 1))
+    [ "$i" -gt "$n" ] && printf '%s\n' "$child"
</file context>
Suggested change
[ -d "$dir/$child" ] || continue
[ -d "$dir/$child" ] && [ ! -L "$dir/$child" ] || continue

Comment thread cli-tool/components/skills/productivity/mac-storage-cleaner/scripts/clean-safe.sh Outdated
Comment thread cli-tool/components/skills/productivity/mac-storage-cleaner/scripts/clean-safe.sh Outdated
# disabling every home-relative deny rule while the /users/<name> carve-out
# in the case statement below still grants access underneath it.
while [ "${home_lower%/}" != "$home_lower" ]; do home_lower="${home_lower%/}"; done
for r in / /system /library /applications /usr /usr/local /bin /sbin /etc \

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The deny validator only exact-matches the system-user roots (/system, /library, /usr, /usr/local, /var, ...), while subtree denials are applied only to the MobileSync/Keychains/Mail/Messages/.ssh/.aws/.gnupg/Photos list. A path strictly below a system root (e.g. /System/Library/..., /usr/local/..., /var/folders/...) is therefore not refused, and the ancestor-symlink re-check uses the same equality-only list, so a $HOME symlink pointing into /System//Users/etc. can smuggle a deeper path past the string checks yet not be caught on resolution. That undercuts the "Never tier mechanically refused" guarantee this PR advertises. Consider denying these roots as subtrees (and any ancestor-resolved path beneath them) rather than exact leaves.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At cli-tool/components/skills/productivity/mac-storage-cleaner/scripts/lib.sh, line 239:

<comment>The deny validator only exact-matches the system-user roots (`/system`, `/library`, `/usr`, `/usr/local`, `/var`, `...`), while subtree denials are applied only to the MobileSync/Keychains/Mail/Messages/`.ssh`/`.aws`/`.gnupg`/Photos list. A path strictly below a system root (e.g. `/System/Library/...`, `/usr/local/...`, `/var/folders/...`) is therefore not refused, and the ancestor-symlink re-check uses the same equality-only list, so a `$HOME` symlink pointing into `/System`/`/Users`/etc. can smuggle a deeper path past the string checks yet not be caught on resolution. That undercuts the "Never tier mechanically refused" guarantee this PR advertises. Consider denying these roots as subtrees (and any ancestor-resolved path beneath them) rather than exact leaves.</comment>

<file context>
@@ -106,33 +155,198 @@ human_kb () {
+  # disabling every home-relative deny rule while the /users/<name> carve-out
+  # in the case statement below still grants access underneath it.
+  while [ "${home_lower%/}" != "$home_lower" ]; do home_lower="${home_lower%/}"; done
+  for r in / /system /library /applications /usr /usr/local /bin /sbin /etc \
+           /var /private /opt /opt/homebrew /users /volumes /dev /tmp \
+           "$home_lower" "$home_lower/library" "$home_lower/desktop" \
</file context>

- Electron guard: both-directions case-insensitive process match (no fail-open on name mismatch)
- DeviceSupport keep-N: version-ordered (sort -rV), not mtime; AI loop same
- Symlinked retention roots refused (keep-N, AI, DiagnosticReports, Electron base)
- Handoff age gate content-aware (skips if any descendant <60min)
- NUL-delimited find enumeration in all find-driven loops
- Unresolved Current -> skip framework suggestions
- Survey: Electron vs browser-profile caches labeled separately
- trash-items exit codes: previewed/missing counters, documented contract
@JubaKitiashvili

Copy link
Copy Markdown
Contributor Author

All review findings addressed in 16ccbb0d (v2.0.1) — thanks @greptile-apps and cubic, several of these were real catches:

  • Greptile P1 (Electron guard fails open on name mismatch) — confirmed real (pgrep -x discord misses a Discord process). The probe is now a both-directions, case-insensitive check (pgrep -qix exact OR pgrep -qif command-line substring): over-matching only ever costs an extra skip, and only a clean double miss counts as idle.
  • Greptile (DeviceSupport retention by mtime) — confirmed; re-connecting an old device could indeed out-mtime a newer OS version. Retention now enumerates via a newline-safe glob and orders with sort -rV (fail-closed if -V is unavailable: keeps everything). The AI-CLI version loop switched to the same version-sort for consistency.
  • cubic P0 (symlinked retention roots) — keep-N, AI-versions, DiagnosticReports, and the Electron scan base now refuse symlinked roots explicitly (logged + counted) instead of following them through [ -d ].
  • cubic P1 (Handoff age gate) — now content-aware: a candidate directory is skipped if any descendant changed in the last 60 minutes, even when its own mtime is old.
  • cubic P1 (newline filenames) — every find-driven deletion loop now uses -print0 with read -d '' via process substitution (bash 3.2-safe, counters stay in the calling shell); retention loops use glob enumeration, which never parses names.
  • cubic P2 ×3 — unresolved Current now skips that framework's suggestions; the survey separates auto-cleared Electron caches from manual browser-profile caches; trash-items.sh exit codes now track previewed/missing so a mixed valid+refused dry-run exits 0 and only a nothing-eligible-and-something-refused run exits 2.

Upstream test suite grew 68 → 76 with regression tests for each of these (version-ordering-beats-mtime, both-direction process matching, symlinked-base refusal, content-fresh Handoff buffers, exit-code contract). Full changelog: https://github.com/JubaKitiashvili/mac-storage-cleaner/blob/main/CHANGELOG.md#201--2026-08-11

@greptile-apps

greptile-apps Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

All six fixes are verified in the diff — the implementations match the descriptions exactly:

  • Electron guard (clean-safe.sh, Electron loop): pgrep -qix "$app" (exact, case-insensitive) OR pgrep -qif "$app" (substring), with proper tri-state — only rc==1 from both calls is considered idle.
  • DeviceSupport version sort (lib.sh:version_sorted_children): glob enumeration → LC_ALL=C sort -rV, with an explicit sort -V availability probe that returns nothing (keep-all) on failure. Fail-closed on missing sort -V is the right call.
  • Symlinked roots (clean-safe.sh): explicit [ -L "$base" ] guards before [ -d ] on all four retention-root loops (DeviceSupport, AI CLIs, DiagnosticReports, Application Support).
  • Handoff content freshness (clean-safe.sh): find "$p" -mmin -60 -print -quit on any candidate directory; skip if any descendant is recent — correctly catches an old-mtime parent with active children.
  • Newline-safe find loops: all four deletion loops use -print0 with while IFS= read -r -d '' p; done < <(find ...) via process substitution — counters accumulate in the calling shell, not a pipe subshell.
  • Bonus from lib.sh: IFS='' guard around glob expansion in collect() also landed — prevents word-splitting on a $HOME that contains spaces.

Clean release. LGTM.

Tip: You can customize Greptile's behavior for this repo with .greptile/rules.md and .greptile/config.json.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 5 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread cli-tool/components/skills/productivity/mac-storage-cleaner/scripts/clean-safe.sh Outdated
Comment thread cli-tool/components/skills/productivity/mac-storage-cleaner/scripts/clean-safe.sh Outdated
Comment thread cli-tool/components/skills/productivity/mac-storage-cleaner/scripts/clean-safe.sh Outdated
Comment thread cli-tool/components/skills/productivity/mac-storage-cleaner/scripts/lib.sh Outdated
Comment thread cli-tool/components/skills/productivity/mac-storage-cleaner/scripts/survey.sh Outdated
- Literal metachar-proof process probe (ps + grep -iF; regex pgrep -f dropped)
- Control-char dir names fail closed in retention enumeration
- Handoff freshness scan rc-aware (unreadable descendant => skip)
- DiagnosticReports restricted to report artifacts (+Retired)
- survey -mindepth 2 alignment
- Subtree denies for /System /bin /sbin /dev /private/var/db
@JubaKitiashvili

Copy link
Copy Markdown
Contributor Author

Round-2 findings addressed in 4071888f (v2.0.2) — with one respectful rebuttal backed by a live test:

Fixed (5):

  • Regex metachars in the process probe (P1) — confirmed real: pgrep -if "App (Beta)" treats the parens as grouping, so the literal process name cleanly misses (rc 1 → fail-open). Replaced both pgrep probes with a literal snapshot check — ps -axo comm=,command= + grep -qiF — which is metachar-proof, case-insensitive, matches name AND command line, and (by construction) can no longer swallow a failed first probe: a failed/empty ps snapshot is unknown state ⇒ skip.
  • Control-char directory names in retention (P2) — names containing control characters are now skipped entirely in version_sorted_children (never counted, never deleted) so the line protocol can't be corrupted.
  • Handoff freshness scan (P1) — now rc-aware: an unreadable descendant makes find return non-zero ⇒ candidate skipped, not treated as safely old.
  • DiagnosticReports scope (P2) — deletion restricted to report artifacts (*.ips/*.crash/*.diag/*.spin/*.hang/*.panic/*.shutdownStall files + the legacy Retired dir); unrelated files/dirs survive.
  • Survey -mindepth 2 (P3) — aligned with clean-safe so a top-level Application Support/Cache can't be listed as auto-clearable. Also added narrow subtree denies for /System, /bin, /sbin, /dev, /private/var/db (the earlier system-root comment) — /Library, /usr/local, /private/tmp children stay allowed deliberately (leftover-plist cleanup, Homebrew, temp caches).

Rebuttal (1): "stock macOS sort does not implement -V" — tested on this stock install: /usr/bin/sort is 2.3-Apple (199) and printf '18.5 (22F76)\n16.0 (20A362)\n17.5 (21F79)\n' | sort -rV orders correctly (BSD sort gained -V years ago). So version-ordered retention does run on the target platform; the availability probe stays anyway as fail-closed protection for hypothetical older systems — if -V were ever missing, retention keeps everything rather than guessing.

Suite is now 80 tests upstream, with regressions for each item above. Changelog: https://github.com/JubaKitiashvili/mac-storage-cleaner/blob/main/CHANGELOG.md#202--2026-08-11

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 3 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread cli-tool/components/skills/productivity/mac-storage-cleaner/scripts/clean-safe.sh Outdated
… pipeline

Retired/ is no longer rm -rf'd as a unit: its report files are enumerated
individually through the same age/whitelist/validate/dry-run checks as
top-level reports; the directory itself is never deleted.
@JubaKitiashvili

Copy link
Copy Markdown
Contributor Author

Addressed in v2.0.3 — valid catch. Retired is no longer removed recursively as a unit: its report files now walk the exact same per-item pipeline as top-level reports (30-day age check on each file, per-child whitelist, path validation, dry-run preview, honest partial/failure reporting), and the directory itself is never deleted. Three new regression tests cover it: an old report inside Retired is removed while the dir survives; a fresh report inside an old-mtime Retired survives; a whitelisted child survives with the skip reported. Suite: 83 tests. Changelog: https://github.com/JubaKitiashvili/mac-storage-cleaner/blob/main/CHANGELOG.md#203--2026-08-11

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 1 file (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread cli-tool/components/skills/productivity/mac-storage-cleaner/scripts/clean-safe.sh Outdated
@JubaKitiashvili

Copy link
Copy Markdown
Contributor Author

Addressed in v2.0.4 — aligned the code up to the comment rather than the comment down: _msc_remove_old_report now has the same chmod -R u+w retry and partial-removal accounting (freed bytes credited via size-diff, partial log action) as the keep-N and app-cache loops, and its doc comment now states all three outcomes precisely. One nuance found while writing the regression test: for single FILES a chmod on the item itself can't fix a parent-dir unlink block, so the deterministic test asserts the honest skipped reporting path for a read-only parent (suite now 84 tests). Thanks for the consistency catch.

version_sorted_children () {
local dir="$1" d
[ -d "$dir" ] || return 0
if ! printf '1\n' | sort -V >/dev/null 2>&1; then return 0; fi

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Retention silently becomes a no-op

On the documented default macOS environment, BSD sort rejects -V, so this branch emits no children and both DeviceSupport and AI CLI retention loops remove nothing. Older versions remain on disk and the cleaner provides no warning that the advertised retention cleanup was skipped.

@JubaKitiashvili

Copy link
Copy Markdown
Contributor Author

@greptile-apps re "retention silently becomes a no-op" — two parts:

On the factual claim: stock macOS /usr/bin/sort (2.3-Apple, BSD) does support -V. Verified live on a stock install: printf '18.5 (22F76)\n16.0 (20A362)\n17.5 (21F79)\n' | /usr/bin/sort -rV orders correctly (BSD sort inherited -V from FreeBSD's sort years ago; it's the long option --version-sort that's GNU-only). So on the documented default environment the primary path runs fine — same evidence posted for the identical claim in round 2.

On the fair underlying concern: a silent keep-everything on any hypothetical -V-less sort was still a real weakness, so v2.0.5 removes the debate entirely: _version_sort_desc now falls back to a portable awk comparator (first four numeric components, zero-padded lexical key) when the -V probe fails — retention produces a correct version-descending order on ANY POSIX sort, and can no longer no-op silently. Two new regression tests pin the fallback path (probe forced to fail via a -V-rejecting sort stub: DeviceSupport keep-2 outcome identical to the primary path; semver 1.0.117 > 1.0.20 > 1.0.2 ordering verified). Suite: 86 tests.

@greptile-apps

greptile-apps Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Fair correction on both counts — you're right that BSD sort inherited -V from FreeBSD, so the "silently becomes a no-op" framing was wrong for the documented target environment. I'll retract that factual claim.

The v2.0.5 fallback (_version_sort_desc with a portable awk comparator when the -V probe fails) also cleanly resolves the hypothetical concern, and pinning it with a -V-rejecting sort stub in regression tests is exactly the right way to guarantee the fallback path stays exercised. No further action needed from my side here.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 1 file (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread cli-tool/components/skills/productivity/mac-storage-cleaner/scripts/lib.sh Outdated
@JubaKitiashvili

Copy link
Copy Markdown
Contributor Author

Valid catch on the fallback's component depth — addressed in v2.0.6:

  • The awk fallback key now covers the first eight numeric components (was four), which handles every realistic version shape including 5-component DeviceSupport builds like 18.5.1 (22G100) — regression test pins 22G100 > 22G86.
  • Fail-closed on overflow: a name with more than eight numeric runs makes the helper emit nothing at all — retention keeps everything rather than guess an ordering it can't guarantee (your second suggested resolution), pinned by a 9-component test.
  • The lexical tie-break now only triggers when the full 8-component numeric keys are EQUAL, where it correctly orders Apple build letters (22F76 < 22G76 — pinned by test).

All three paths run under a -V-rejecting sort stub so the fallback stays exercised in CI. Suite: 89 tests. And thanks @greptile-apps for the gracious retraction above 🙏

@greptile-apps

greptile-apps Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

The implementation in _version_sort_desc looks solid. The 8-component depth, overflow sentinel, and fail-closed return path all check out correctly:

  • Overflow detection (if (match(s, /[0-9]+/)) { print "__MSC_VERSION_OVERFLOW__"; next }) fires only after all 8 slots are consumed, so a 9-component name emits the sentinel rather than a malformed key.
  • case "$out" in *__MSC_VERSION_OVERFLOW__*) return 0 ;; returns early with no output, so version_sorted_childrenkeep_newest_n_children propagates empty output → zero deletion candidates. That's the correct fail-closed shape.
  • The lexical tie-break on $0 (the untouched full name) only kicks in when two names produce equal 8-component numeric keys, which is exactly when 22F76 < 22G76 ordering is needed and correct.

The -V-rejecting stub keeping the fallback exercised in CI is a nice touch too. No further concerns here — this resolution is complete.

BREAKING: clean-safe.sh previews by default; deleting requires --apply.
trash-items.sh refuses bulk batches (>100 items / 5GB) without --force and
rejects unknown flags instead of treating them as paths.

Also: portable skill-directory resolver (works when installed by any agent,
not just Claude Code), portable frontmatter, declared-behavior section for
skill scanners, 131 tests.
@JubaKitiashvili

Copy link
Copy Markdown
Contributor Author

Updated to v3.0.0 (8e05ac62) — this is a bigger change than the earlier v2.0.x updates, so here is what a reviewer should know.

The reason for the major version: the destructive default is gone.
clean-safe.sh with no argument now previews and deletes nothing; deleting requires --apply. --dry-run still works as an alias, and MSC_DRY_RUN=1 overrides --apply (the env var can only ever make a run safer). The motivation is exactly the cross-agent situation this repo serves: several agents execute shell commands with no approval prompt at all, so a destructive default meant an agent could delete caches the user never saw proposed.

trash-items.sh gained two guards: it refuses a batch over 100 eligible items or 5 GB with exit 4 unless --force is passed (a preview is never refused), and it now rejects unknown leading flags with exit 2 instead of treating them as paths — previously trash-items.sh --dry-run <paths> printed "not found: --dry-run" and then trashed the remaining paths for real.

The fix that makes it work outside Claude Code. Every command block in SKILL.md previously resolved the skill directory as ~/.claude/skills only, so an install by any other agent — or a project-scoped install — failed with No such file or directory. It now searches every standard skill root (Codex, Cursor, opencode, Antigravity, Windsurf, Hermes, .agents/skills, project-scoped) plus an MSC_SKILL_ROOT escape hatch for anything unlisted. Verified installed and resolving on Codex CLI 0.144.5, Cursor 3.15.19, opencode 1.14.22, Windsurf 2.3.15 and Antigravity 2.5.0 — recorded in docs/compat/ upstream, with the "does the agent auto-trigger it" assertion marked unverified rather than assumed, since that needs an interactive session.

For the security scan: SKILL.md now carries a mechanism-level declared behavior section enumerating every destructive operation (the exact rm -rf targets, the chmod -R u+w retry, mv into ~/.Trash, the osascript Finder call, brew cleanup, conda clean, simctl delete unavailable), what is mechanically refused, and the fact that there is no sudo and no network access anywhere. Also note dist/mac-storage-cleaner.skill is gone upstream — it was a stale build that contradicted the current code.

Upstream: 131 tests, CI on macos-latest (bats + shellcheck), tag v3.0.0. Changelog: https://github.com/JubaKitiashvili/mac-storage-cleaner/blob/main/CHANGELOG.md#300--2026-08-14 — happy to adjust anything.

Comment on lines +423 to +424
elif printf '%s\n' "$_pt" | LC_ALL=C grep -qiF "$app"; then
st=0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Substring guard skips idle apps

When an idle app has a generic Application Support directory name such as Code, grep -qiF can match that text in an unrelated process's executable path or arguments, causing the cleaner to classify the app as running and skip its cache directories.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 4 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="cli-tool/components/skills/productivity/mac-storage-cleaner/SKILL.md">

<violation number="1" location="cli-tool/components/skills/productivity/mac-storage-cleaner/SKILL.md:205">
P2: The doc says a preview with `MSC_DRY_RUN=1` is "never refused", but `trash-items.sh` does not skip the bulk guard in dry-run mode. The bulk check at line 117 (`if [ "$FORCE" != 1 ] && { [ "$eligible_n" -gt "$MAX_ITEMS" ] || ... }; then ... exit 4`) never tests `DRY`, so a batch over the 100-item/5 GB cap is refused (exit 4) even with `MSC_DRY_RUN=1`. An agent following this instruction to preview a large batch will be refused and pushed toward `--force` (which also disables the item cap). Either make the guard honor dry-run (refuse only real runs) or fix the doc to say that the 100-item/5 GB cap applies even to previews.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

**Bulk operations need confirmation.** `trash-items.sh` refuses a batch of more than 100
eligible items or 5 GB and exits 4, because several agents run shell commands without
asking the user first. Show the user the list (a preview with `MSC_DRY_RUN=1` is never
refused), get their explicit go-ahead, then re-run with `--force` as the first argument.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The doc says a preview with MSC_DRY_RUN=1 is "never refused", but trash-items.sh does not skip the bulk guard in dry-run mode. The bulk check at line 117 (if [ "$FORCE" != 1 ] && { [ "$eligible_n" -gt "$MAX_ITEMS" ] || ... }; then ... exit 4) never tests DRY, so a batch over the 100-item/5 GB cap is refused (exit 4) even with MSC_DRY_RUN=1. An agent following this instruction to preview a large batch will be refused and pushed toward --force (which also disables the item cap). Either make the guard honor dry-run (refuse only real runs) or fix the doc to say that the 100-item/5 GB cap applies even to previews.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At cli-tool/components/skills/productivity/mac-storage-cleaner/SKILL.md, line 205:

<comment>The doc says a preview with `MSC_DRY_RUN=1` is "never refused", but `trash-items.sh` does not skip the bulk guard in dry-run mode. The bulk check at line 117 (`if [ "$FORCE" != 1 ] && { [ "$eligible_n" -gt "$MAX_ITEMS" ] || ... }; then ... exit 4`) never tests `DRY`, so a batch over the 100-item/5 GB cap is refused (exit 4) even with `MSC_DRY_RUN=1`. An agent following this instruction to preview a large batch will be refused and pushed toward `--force` (which also disables the item cap). Either make the guard honor dry-run (refuse only real runs) or fix the doc to say that the 100-item/5 GB cap applies even to previews.</comment>

<file context>
@@ -131,6 +199,14 @@ the path is on the tool's deny list (system/user roots) — never work around a
+**Bulk operations need confirmation.** `trash-items.sh` refuses a batch of more than 100
+eligible items or 5 GB and exits 4, because several agents run shell commands without
+asking the user first. Show the user the list (a preview with `MSC_DRY_RUN=1` is never
+refused), get their explicit go-ahead, then re-run with `--force` as the first argument.
+Never pass `--force` pre-emptively. If `du` can't fully measure the batch (some paths are
+unreadable), the size guard is skipped for that run — with an on-screen warning and a
</file context>

@JubaKitiashvili

Copy link
Copy Markdown
Contributor Author

Thanks — three findings came back on v3.0.0. One was a real fail-open and is fixed in 627e0887 (v3.0.1); the other two I checked against the code and they don't reproduce. Detail on each, since two of them are worth the reviewers' time either way:

1. cubic P2 — MSC_MAX_TRASH_GB=08 skips the size cap. CONFIRMED, FIXED. Good catch, and it was exactly the failure class this tool exists to prevent. The limits were validated as all-digit strings, but 08 is an invalid octal literal in bash arithmetic. Reproduced on /bin/bash 3.2:

$ MAX_GB=08; echo $(( MAX_GB * 1024 * 1024 ))
/bin/bash: 08: value too great for base (error token is "08")

The expansion errored, the enclosing [ … -gt … ] then failed, and the size cap silently did not fire. Both limits are now normalized once with $((10#$MAX_ITEMS)) / $((10#$MAX_GB)) right after validation, so the arithmetic and the user-facing ${MAX_GB}GB message agree. I scoped the rest rather than assuming: MSC_MAX_TRASH_ITEMS reaches only test -gt and MSC_DEVICE_SUPPORT_KEEP/MSC_AI_AGENTS_KEEP reach only test -lt and awk -v — all three parse leading-zero values as decimal, verified individually, so this was the only vulnerable site. Three regression tests added (134 total).

2. cubic P2 — a preview is refused by the bulk guard. Does not reproduce. The cap is inside the dry-run guard: if [ "$DRY" != 1 ]; then opens at trash-items.sh:70 and its fi is at :139, with the cap at :117-123 nested inside. Empirically, a 101-item batch under MSC_DRY_RUN=1 exits 0, prints would trash for every item, and leaves all 101 files in place. I think line 117 was read without the enclosing block.

3. greptile P1 — the substring process guard can match an unrelated process. True, and deliberate. ps -axo comm=,command= + grep -qiF is a literal match precisely because the previous regex form (pgrep -f) let an app directory named e.g. App (Beta) never match its own process — a fail-open on a running app. Over-matching costs an unnecessary skip (a cache is left alone); under-matching deletes a running app's cache mid-write. For a tool whose brand is not losing user data, that asymmetry is worth paying, and the skip is reported to the user rather than silent. Open to a better signal if there is one that doesn't reintroduce the regex hole.

Upstream: v3.0.1 tagged, 134 tests, CI green on macos-latest.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review-pending Component PR awaiting maintainer review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant