
Macos Cleaner
- 1.2k installs
- 1.3k repo stars
- Updated August 4, 2026
- daymade/claude-code-skills
macos-cleaner is an agent skill that safely removes macOS caches, logs, and temporary files to reclaim disk space for developers whose development Mac is running low on storage.
About
macos-cleaner in daymade/claude-code-skills guides developers through reclaiming tens of gigabytes on a development Mac without breaking installed applications. The skill documents cleanup targets with explicit safety levels—~/Library/Caches for browser and app caches is marked safe to delete, with notes that first launches may be slower after cache removal. A security scan passed using gitleaks plus pattern-based validation (scanned 2026-06-05). Developers reach for macos-cleaner when Xcode, Docker, npm, or browser caches have consumed large chunks of disk and manual Finder cleanup is risky or incomplete. The reference explains what each target stores, expected impact after deletion, and which paths are safe versus cautionary. Triggers include low disk space warnings, bloated ~/Library/Caches, and routine Mac maintenance before installing large SDKs or containers.
- Targets user and system caches, Homebrew artifacts, logs, temporary files, and thumbnails
- Provides per-target safety ratings (🟢 safe) with expected impact and regeneration behavior
- Includes ready-to-run bash and brew cleanup commands for each category
- Reclaims 10-100+ GB depending on usage patterns
- Designed as a safe, repeatable maintenance workflow for developer machines
Macos Cleaner by the numbers
- 1,162 all-time installs (skills.sh)
- +60 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #170 of 1,435 DevOps & CI/CD skills by installs in the Skillselion catalog
- Security screen: CRITICAL risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/daymade/claude-code-skills --skill macos-cleanerAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1.2k |
|---|---|
| repo stars | ★ 1.3k |
| Security audit | 1 / 3 scanners passed |
| Last updated | August 4, 2026 |
| Repository | daymade/claude-code-skills ↗ |
How do you safely free disk space on a Mac?
Reclaim tens of gigabytes of disk space on a development Mac by safely removing caches, logs, and temporary files without breaking applications.
Who is it for?
Developers on macOS with low disk space who want guided, safety-rated cache and log cleanup instead of guessing which ~/Library paths to delete.
Skip if: Linux or Windows disk cleanup, enterprise MDM-managed Macs with restricted deletion policies, or uninstalling applications entirely.
When should I use this skill?
User reports low disk space on Mac, asks to clean caches or logs, or wants to reclaim storage from ~/Library/Caches and temp files safely.
What you get
Documented cleanup plan with safety-rated targets, reclaimed disk space, and cleared caches without breaking applications.
- Safety-rated cleanup target list
- Reclaimed disk space
By the numbers
- Security scan passed with gitleaks plus pattern-based validation on 2026-06-05
- Targets reclaiming tens of gigabytes of disk space
Files
macOS Cleaner
Overview
Intelligently analyze macOS disk usage and provide actionable cleanup recommendations to reclaim storage space. This skill follows a safety-first philosophy: analyze thoroughly, present clear findings, and require explicit user confirmation before executing any deletions.
Target users: Users with basic technical knowledge who understand file systems but need guidance on what's safe to delete on macOS.
Core Principles
1. Safety First, Never Bypass: NEVER execute dangerous commands (rm -rf, mo clean, etc.) without explicit user confirmation. No shortcuts, no workarounds. 2. Precision Deletion Only: Delete by specifying exact object IDs/names. Never use batch prune commands. 3. Every Object Listed: Reports must show every specific image, volume, container — not just "12 GB of unused images". 4. Value Over Vanity: Your goal is NOT to maximize cleaned space. Your goal is to identify what is truly useless vs valuable cache. Clearing 50GB of useful cache just to show a big number is harmful. 5. Network Environment Awareness: Many users (especially in China) have slow/unreliable internet. Re-downloading caches can take hours. A cache that saves 30 minutes of download time is worth keeping. 6. Impact Analysis Required: Every cleanup recommendation MUST include "what happens if deleted" column. Never just list items without explaining consequences. 7. Double-Check Before Delete: Verify each Docker object with independent cross-checks before deletion (see references/docker_analysis.md). 8. Patience Over Speed: Disk scans can take 5-10 minutes. NEVER interrupt or skip slow operations. Report progress to user regularly. 9. User Executes Cleanup: After analysis, provide the cleanup command for the user to run themselves. Do NOT auto-execute cleanup. 10. Conservative Defaults: When in doubt, don't delete. Err on the side of caution.
ABSOLUTE PROHIBITIONS:
- ❌ NEVER use
docker image prune,docker volume prune,docker system prune, or ANY prune-family command (exception:docker builder pruneis safe — build cache contains only intermediate layers, never user data) - ❌ NEVER use
docker container prune— stopped containers may be restarted at any time - ❌ NEVER run
rm -rfon user directories without explicit confirmation - ❌ NEVER run
mo cleanwithout--dry-runpreview first - ❌ NEVER skip analysis steps to save time
- ❌ NEVER append
--helpto Mole commands (onlymo --helpis safe) - ❌ NEVER present cleanup reports with only categories — every object must be individually listed
- ❌ NEVER recommend deleting useful caches just to inflate cleanup numbers
Workflow Decision Tree
User reports disk space issues
↓
Quick Diagnosis
↓
┌──────┴──────┐
│ │
Immediate Deep Analysis
Cleanup (continue below)
│ │
└──────┬──────┘
↓
Present Findings
↓
User Confirms
↓
Execute Cleanup
↓
Verify ResultsStep 1: Quick Diagnosis with Mole
Primary tool: Use Mole for disk analysis. It provides comprehensive, categorized results.
1.1 Pre-flight Checks
# Check Mole installation and version
which mo && mo --version
# If not installed
brew install tw93/tap/mole
# Check for updates (Mole updates frequently)
brew info tw93/tap/mole | head -5
# Upgrade if outdated
brew upgrade tw93/tap/mole1.2 Choose Analysis Method
IMPORTANT: Use mo analyze as the primary analysis tool, NOT mo clean --dry-run.
| Command | Purpose | Use When |
|---|---|---|
mo analyze | Interactive disk usage explorer (TUI tree view) | PRIMARY: Understanding what's consuming space |
mo clean --dry-run | Preview cleanup categories | SECONDARY: Only after mo analyze to see cleanup preview |
Why prefer `mo analyze`:
- Dedicated disk analysis tool with interactive tree navigation
- Allows drilling down into specific directories
- Shows actual disk usage breakdown, not just cleanup categories
- More informative for understanding storage consumption
1.3 Run Analysis via tmux
IMPORTANT: Mole requires TTY. Always use tmux from Claude Code.
CRITICAL TIMING NOTE: Home directory scans are SLOW (5-10 minutes or longer for large directories). Inform user upfront and wait patiently.
# Create tmux session
tmux new-session -d -s mole -x 120 -y 40
# Run disk analysis (PRIMARY tool - interactive TUI)
tmux send-keys -t mole 'mo analyze' Enter
# Wait for scan - BE PATIENT!
# Home directory scanning typically takes 5-10 minutes
# Report progress to user regularly
sleep 60 && tmux capture-pane -t mole -p
# Navigate the TUI with arrow keys
tmux send-keys -t mole Down # Move to next item
tmux send-keys -t mole Enter # Expand/select item
tmux send-keys -t mole 'q' # Quit when doneAlternative: Cleanup preview (use AFTER mo analyze)
# Run dry-run preview (SAFE - no deletion)
tmux send-keys -t mole 'mo clean --dry-run' Enter
# Wait for scan (report progress to user every 30 seconds)
# Be patient! Large directories take 5-10 minutes
sleep 30 && tmux capture-pane -t mole -p1.4 Progress Reporting
Report scan progress to user regularly:
📊 Disk Analysis in Progress...
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
⏱️ Elapsed: 2 minutes
Current status:
✅ Applications: 49.5 GB (complete)
✅ System Library: 10.3 GB (complete)
⏳ Home: scanning... (this may take 5-10 minutes)
⏳ App Library: pending
I'm waiting patiently for the scan to complete.
Will report again in 30 seconds...1.5 Present Final Findings
After scan completes, present structured results:
📊 Disk Space Analysis (via Mole)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Free space: 27 GB
🧹 Recoverable Space (dry-run preview):
➤ User Essentials
• User app cache: 16.67 GB
• User app logs: 102.3 MB
• Trash: 642.9 MB
➤ Browser Caches
• Chrome cache: 1.90 GB
• Safari cache: 4 KB
➤ Developer Tools
• uv cache: 9.96 GB
• npm cache: (detected)
• Docker cache: (detected)
• Homebrew cache: (detected)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Total recoverable: ~30 GB
⚠️ This was a dry-run preview. No files were deleted.Step 2: Deep Analysis Categories
Scan the following categories systematically. Reference references/cleanup_targets.md for detailed explanations.
Category 1: System & Application Caches
Locations to analyze:
~/Library/Caches/*- User application caches/Library/Caches/*- System-wide caches (requires sudo)~/Library/Logs/*- Application logs/var/log/*- System logs (requires sudo)
Analysis script:
scripts/analyze_caches.py --user-onlySafety level: 🟢 Generally safe to delete (apps regenerate caches)
Exceptions to preserve:
- Browser caches while browser is running
- IDE caches (may slow down next startup)
- Package manager caches (Homebrew, pip, npm)
Category 2: Application Remnants
Locations to analyze:
~/Library/Application Support/*- App data~/Library/Preferences/*- Preference files~/Library/Containers/*- Sandboxed app data
Analysis approach: 1. List installed applications in /Applications 2. Cross-reference with ~/Library/Application Support 3. Identify orphaned folders (app uninstalled but data remains)
Analysis script:
scripts/find_app_remnants.pySafety level: 🟡 Caution required
- ✅ Safe: Folders for clearly uninstalled apps
- ⚠️ Check first: Folders for apps you rarely use
- ❌ Keep: Active application data
Category 3: Large Files & Duplicates
Analysis script:
scripts/analyze_large_files.py --threshold 100MB --path ~Find duplicates (optional, resource-intensive):
# Use fdupes if installed
if command -v fdupes &> /dev/null; then
fdupes -r ~/Documents ~/Downloads
fiPresent findings:
📦 Large Files (>100MB):
━━━━━━━━━━━━━━━━━━━━━━━━
1. movie.mp4 4.2 GB ~/Downloads
2. dataset.csv 1.8 GB ~/Documents/data
3. old_backup.zip 1.5 GB ~/Desktop
...
🔁 Duplicate Files:
- screenshot.png (3 copies) 15 MB each
- document_v1.docx (2 copies) 8 MB eachSafety level: 🟡 User judgment required
Category 4: Development Environment Cleanup
Targets:
- Docker: images, containers, volumes, build cache
- Homebrew: cache, old versions
- Node.js:
node_modules, npm cache - Python: pip cache,
__pycache__, venv - Git:
.gitfolders in archived projects
Analysis script:
scripts/analyze_dev_env.pyExample findings:
🐳 Docker Resources:
- Unused images: 12 GB
- Stopped containers: 2 GB
- Build cache: 8 GB
- Orphaned volumes: 3 GB
Total potential: 25 GB
📦 Package Managers:
- Homebrew cache: 5 GB
- npm cache: 3 GB
- pip cache: 1 GB
Total potential: 9 GB
🗂️ Old Projects:
- archived-project-2022/.git 500 MB
- old-prototype/.git 300 MBCleanup commands (require confirmation):
# Homebrew cleanup (safe)
brew cleanup -s
# npm _npx only (safe - temporary packages)
rm -rf ~/.npm/_npx
# pip cache (use with caution)
pip cache purgeDocker cleanup - SPECIAL HANDLING REQUIRED:
⚠️ NEVER use these commands:
# ❌ DANGEROUS - deletes ALL volumes without confirmation
docker volume prune -f
docker system prune -a --volumes✅ Correct approach - per-volume confirmation:
# 1. List all volumes
docker volume ls
# 2. Identify which projects each volume belongs to
docker volume inspect <volume_name>
# 3. Ask user to confirm EACH project they want to delete
# Example: "Do you want to delete all volumes for 'ragflow' project?"
# 4. Delete specific volumes only after confirmation
docker volume rm ragflow_mysql_data ragflow_redis_dataSafety level: 🟢 Homebrew/npm cleanup, 🔴 Docker volumes require per-project confirmation
Step 2A-2C: Docker Deep Analysis
For Docker-heavy systems, follow the detailed per-object analysis and verification protocol (image/container/volume inspection, OrbStack sparse-file handling, and the database-volume red-flag rule) in references/docker_analysis.md. Core rule: verify every Docker object with independent cross-checks before deleting, and never use prune-family commands.
Step 3: Integration with Mole
Mole (https://github.com/tw93/Mole) is a command-line interface (CLI) tool for comprehensive macOS cleanup. It provides interactive terminal-based analysis and cleanup for caches, logs, developer tools, and more.
CRITICAL REQUIREMENTS:
1. TTY Environment: Mole requires a TTY for interactive commands. Use tmux when running from Claude Code or scripts. 2. Version Check: Always verify Mole is up-to-date before use. 3. Safe Help Command: Only mo --help is safe. Do NOT append --help to other commands.
Installation check and upgrade:
# Check if installed and get version
which mo && mo --version
# If not installed
brew install tw93/tap/mole
# Check for updates
brew info tw93/tap/mole | head -5
# Upgrade if needed
brew upgrade tw93/tap/moleUsing Mole with tmux (REQUIRED for Claude Code):
# Create tmux session for TTY environment
tmux new-session -d -s mole -x 120 -y 40
# Run analysis (safe, read-only)
tmux send-keys -t mole 'mo analyze' Enter
# Wait for scan (be patient - can take 5-10 minutes for large directories)
sleep 60
# Capture results
tmux capture-pane -t mole -p
# Cleanup when done
tmux kill-session -t moleAvailable commands (from `mo --help`):
| Command | Safety | Description |
|---|---|---|
mo --help | ✅ Safe | View all commands (ONLY safe help) |
mo analyze | ✅ Safe | Disk usage explorer (read-only) |
mo status | ✅ Safe | System health monitor |
mo clean --dry-run | ✅ Safe | Preview cleanup (no deletion) |
mo clean | ⚠️ DANGEROUS | Actually deletes files |
mo purge | ⚠️ DANGEROUS | Remove project artifacts |
mo uninstall | ⚠️ DANGEROUS | Remove applications |
Reference guide: See references/mole_integration.md for detailed tmux workflow and troubleshooting.
Multi-Layer Deep Exploration with Mole
For comprehensive analysis, perform multi-layer exploration (drilling into Home, Library, .cache, .npm, Downloads, etc.) rather than only top-level scans. The full TUI navigation walkthrough, recommended exploration tree, time expectations, and a complete example session are documented in references/mole_integration.md.
Anti-Patterns: What NOT to Delete
CRITICAL: The following items are often suggested for cleanup but should NOT be deleted in most cases. They provide significant value that outweighs the space they consume.
Items to KEEP (Anti-Patterns)
| Item | Size | Why NOT to Delete | Real Impact of Deletion |
|---|---|---|---|
| Xcode DerivedData | 10+ GB | Build cache saves 10-30 min per full rebuild | Next build takes 10-30 minutes longer |
| npm _cacache | 5+ GB | Downloaded packages cached locally | npm install redownloads everything (30min-2hr in China) |
| ~/.cache/uv | 10+ GB | Python package cache | Every Python project reinstalls deps from PyPI |
| Playwright browsers | 3-4 GB | Browser binaries for automation testing | Redownload 2GB+ each time (30min-1hr) |
| iOS DeviceSupport | 2-3 GB | Required for device debugging | Redownload from Apple when connecting device |
| Docker stopped containers | <500 MB | May restart anytime with docker start | Lose container state, need to recreate |
| ~/.cache/huggingface | varies | AI model cache | Redownload large models (hours) |
| ~/.cache/modelscope | varies | AI model cache (China) | Same as above |
| JetBrains caches | 1+ GB | IDE indexing and caches | IDE takes 5-10 min to re-index |
Why This Matters
The vanity trap: Showing "Cleaned 50GB!" feels good but:
- User spends next 2 hours redownloading npm packages
- Next Xcode build takes 30 minutes instead of 30 seconds
- AI project fails because models need redownload
The right mindset: "I found 50GB of caches. Here's why most of them are actually valuable and should be kept..."
What IS Actually Safe to Delete
| Item | Why Safe | Impact |
|---|---|---|
| Trash | User already deleted these files | None - user's decision |
| Homebrew old versions | Replaced by newer versions | Rare: can't rollback to old version |
| npm _npx | Temporary npx executions | Minor: npx re-downloads on next use |
| Orphaned app remnants | App already uninstalled | None - app doesn't exist |
| Specific unused Docker volumes | Projects confirmed abandoned | None - if truly abandoned |
Report Format Requirements
Every cleanup report MUST follow this format with impact analysis:
## Disk Analysis Report
### Classification Legend
| Symbol | Meaning |
|--------|---------|
| 🟢 | **Absolutely Safe** - No negative impact, truly unused |
| 🟡 | **Trade-off Required** - Useful cache, deletion has cost |
| 🔴 | **Do Not Delete** - Contains valuable data or actively used |
### Findings
| Item | Size | Classification | What It Is | Impact If Deleted |
|------|------|----------------|------------|-------------------|
| Trash | 643 MB | 🟢 | Files you deleted | None |
| npm _npx | 2.1 GB | 🟢 | Temp npx packages | Minor redownload |
| npm _cacache | 5 GB | 🟡 | Package cache | 30min-2hr redownload |
| DerivedData | 10 GB | 🟡 | Xcode build cache | 10-30min rebuild |
| Docker volumes | 11 GB | 🔴 | Project databases | **DATA LOSS** |
### Recommendation
Only items marked 🟢 are recommended for cleanup.
Items marked 🟡 require your judgment based on usage patterns.
Items marked 🔴 require explicit confirmation per-item.Docker Report: Required Object-Level Detail
Docker reports must list every individual object (each image, container, and volume), not just categories. See the object-level table templates in references/report_templates.md.
High-Quality Report Template
After multi-layer exploration, present findings using the detailed fill-in-the-blank template in references/report_templates.md.
Report Quality Checklist
Before presenting the report, verify:
- [ ] Every item has "Impact If Deleted" explanation
- [ ] 🟢 items are truly safe (Trash, _npx, old versions)
- [ ] 🟡 items require user decision (age info, usage patterns)
- [ ] 🔴 items explain WHY they should be kept
- [ ] Docker volumes listed by project, not blanket prune
- [ ] Network environment considered (China = slow redownload)
- [ ] No recommendations to delete useful caches just to inflate numbers
- [ ] Clear action items with exact commands
Step 4: Present Recommendations
Format findings into actionable recommendations with risk levels:
# macOS Cleanup Recommendations
## Summary
Total space recoverable: ~XX GB
Current usage: XX%
## Recommended Actions
### 🟢 Safe to Execute (Low Risk)
These are safe to delete and will be regenerated as needed:
1. **Empty Trash** (~12 GB)
- Location: ~/.Trash
- Command: `rm -rf ~/.Trash/*`
2. **Clear System Caches** (~45 GB)
- Location: ~/Library/Caches
- Command: `rm -rf ~/Library/Caches/*`
- Note: Apps may be slightly slower on next launch
3. **Remove Homebrew Cache** (~5 GB)
- Command: `brew cleanup -s`
### 🟡 Review Recommended (Medium Risk)
Review these items before deletion:
1. **Large Downloads** (~38 GB)
- Location: ~/Downloads
- Action: Manually review and delete unneeded files
- Files: [list top 10 largest files]
2. **Application Remnants** (~8 GB)
- Apps: [list detected uninstalled apps]
- Locations: [list paths]
- Action: Confirm apps are truly uninstalled before deleting data
### 🔴 Keep Unless Certain (High Risk)
Only delete if you know what you're doing:
1. **Docker Volumes** (~3 GB)
- May contain important data
- Review with: `docker volume ls`
2. **Time Machine Local Snapshots** (~XX GB)
- Automatic backups, will be deleted when space needed
- Command to check: `tmutil listlocalsnapshots /`Step 5: Execute with Confirmation
CRITICAL: Never execute deletions without explicit user confirmation.
Interactive confirmation flow:
# Example from scripts/safe_delete.py
def confirm_delete(path: str, size: str, description: str) -> bool:
"""
Ask user to confirm deletion.
Args:
path: File/directory path
size: Human-readable size
description: What this file/directory is
Returns:
True if user confirms, False otherwise
"""
print(f"\n🗑️ Confirm Deletion")
print(f"━━━━━━━━━━━━━━━━━━")
print(f"Path: {path}")
print(f"Size: {size}")
print(f"Description: {description}")
response = input("\nDelete this item? [y/N]: ").strip().lower()
return response == 'y'For batch operations:
def batch_confirm(items: list) -> list:
"""
Show all items, ask for batch confirmation.
Returns list of items user approved.
"""
print("\n📋 Items to Delete:")
print("━━━━━━━━━━━━━━━━━━")
for i, item in enumerate(items, 1):
print(f"{i}. {item['path']} ({item['size']})")
print("\nOptions:")
print(" 'all' - Delete all items")
print(" '1,3,5' - Delete specific items by number")
print(" 'none' - Cancel")
response = input("\nYour choice: ").strip().lower()
if response == 'none':
return []
elif response == 'all':
return items
else:
# Parse numbers
indices = [int(x.strip()) - 1 for x in response.split(',')]
return [items[i] for i in indices if 0 <= i < len(items)]Step 6: Verify Results
After cleanup, verify the results and report back:
# Compare before/after
df -h /
# Calculate space recovered
# (handled by scripts/cleanup_report.py)Report format:
✅ Cleanup Complete!
Before: 450 GB used (90%)
After: 385 GB used (77%)
━━━━━━━━━━━━━━━━━━━━━━━━
Recovered: 65 GB
Breakdown:
- System caches: 45 GB
- Downloads: 12 GB
- Homebrew cache: 5 GB
- Application remnants: 3 GB
⚠️ Notes:
- Some applications may take longer to launch on first run
- Deleted items cannot be recovered unless you have Time Machine backup
- Consider running this cleanup monthly
💡 Maintenance Tips:
- Set up automatic Homebrew cleanup: `brew cleanup` weekly
- Review Downloads folder monthly
- Enable "Empty Trash Automatically" in Finder preferencesBonus: Dockerfile Optimization Discoveries
When image analysis reveals oversized images, suggest multi-stage build optimization. See the before/after example and key techniques in references/docker_analysis.md.
⚠️ Safety Guidelines
Always Preserve
Never delete these without explicit user instruction:
~/Documents,~/Desktop,~/Picturescontent- Active project directories
- Database files (.db, .sqlite)
- Configuration files for active apps
- SSH keys, credentials, certificates
- Time Machine backups
⚠️ Require Sudo Confirmation
These operations require elevated privileges. Ask user to run commands manually:
- Clearing
/Library/Caches(system-wide) - Clearing
/var/log(system logs) - Clearing
/private/var/folders(system temp)
Example prompt:
⚠️ This operation requires administrator privileges.
Please run this command manually:
sudo rm -rf /Library/Caches/*
⚠️ You'll be asked for your password.💡 Backup Recommendation
Before executing any cleanup >10GB, recommend:
💡 Safety Tip:
Before cleaning XX GB, consider creating a Time Machine backup.
Quick backup check:
tmutil latestbackup
If no recent backup, run:
tmutil startbackupTroubleshooting
"Operation not permitted" errors
macOS may block deletion of certain system files due to SIP (System Integrity Protection).
Solution: Don't force it. These protections exist for security.
App crashes after cache deletion
Rare but possible. Solution: Restart the app, it will regenerate necessary caches.
Docker cleanup removes important data
Prevention: Always list Docker volumes before cleanup:
docker volume ls
docker volume inspect <volume_name>Resources
scripts/
analyze_caches.py- Scan and categorize cache directoriesfind_app_remnants.py- Detect orphaned application dataanalyze_large_files.py- Find large files with smart filteringanalyze_dev_env.py- Scan development environment resourcessafe_delete.py- Interactive deletion with confirmationcleanup_report.py- Generate before/after reports
references/
cleanup_targets.md- Detailed explanations of each cleanup targetmole_integration.md- How to use Mole, plus the multi-layer TUI exploration walkthroughdocker_analysis.md- Docker deep-analysis workflow (Step 2A-2C) and Dockerfile optimizationreport_templates.md- Detailed report templates (object-level Docker tables, full report layout)safety_rules.md- Comprehensive list of what to never delete
Usage Examples
Example 1: Quick Cache Cleanup
User request: "My Mac is running out of space, can you help?"
Workflow: 1. Run quick diagnosis 2. Identify system caches as quick win 3. Present findings: "45 GB in ~/Library/Caches" 4. Explain: "These are safe to delete, apps will regenerate them" 5. Ask confirmation 6. Provide the command for the user to run themselves: rm -rf ~/Library/Caches/* (per Core Principle 9, do not auto-execute) 7. After the user runs it, verify with df -h / and report: "Recovered 45 GB"
Example 2: Development Environment Cleanup
User request: "I'm a developer and my disk is full"
Workflow: 1. Run scripts/analyze_dev_env.py 2. Present Docker + npm + Homebrew findings 3. Explain each category 4. Provide cleanup commands with explanations 5. Let user execute (don't auto-execute Docker cleanup) 6. Verify results
Example 3: Finding Large Files
User request: "What's taking up so much space?"
Workflow: 1. Run scripts/analyze_large_files.py --threshold 100MB 2. Present top 20 large files with context 3. Categorize: videos, datasets, archives, disk images 4. Let user decide what to delete 5. Provide deletion commands for the user to run (or use scripts/safe_delete.py for interactive per-item confirmation) 6. Suggest archiving to external drive
Best Practices
1. Start Conservative: Begin with obviously safe targets (caches, trash) 2. Explain Everything: Users should understand what they're deleting 3. Show Examples: List 3-5 example files from each category 4. Respect User Pace: Don't rush through confirmations 5. Document Results: Always show before/after space usage 6. Educate: Include maintenance tips in final report 7. Integrate Tools: Suggest Mole for users who prefer GUI
When NOT to Use This Skill
- User wants automatic/silent cleanup (against safety-first principle)
- User needs Windows/Linux cleanup (macOS-specific skill)
- User has <10% disk usage (no cleanup needed)
- User wants to clean system files requiring SIP disable (security risk)
In these cases, explain limitations and suggest alternatives.
Security scan passed
Scanned at: 2026-06-05T00:05:02.933101
Tool: gitleaks + pattern-based validation
Content hash: 28f309886109b9ebe630bff04bfdbe3fb91ddb162218bfd78c36561ba5a0eeb0
macOS Cleanup Targets Reference
Detailed explanations of cleanup targets, their safety levels, and impact.
System Caches
~/Library/Caches
What it is: Application-level cache storage for user applications.
Contents:
- Browser caches (Chrome, Firefox, Safari)
- Application temporary files
- Download caches
- Thumbnail caches
- Font caches
Safety: 🟢 Safe to delete
Impact:
- Apps may be slower on first launch after deletion
- Websites may load slower on first visit (need to re-download assets)
- No data loss (caches are regenerated)
Size: Typically 10-100 GB depending on usage
Cleanup command:
rm -rf ~/Library/Caches/*/Library/Caches
What it is: System-level cache storage (shared across all users).
Safety: 🟢 Safe to delete (requires sudo)
Impact: Same as user caches, but system-wide
Cleanup command:
sudo rm -rf /Library/Caches/*Package Manager Caches
Homebrew Cache
Location: $(brew --cache) (typically ~/Library/Caches/Homebrew)
What it is: Downloaded package installers and build artifacts
Safety: 🟢 Safe to delete
Impact: Will need to re-download packages on next install/upgrade
Cleanup:
brew cleanup -s # Safe cleanup (removes old versions)
brew cleanup --prune=all # Aggressive cleanup (removes all cached downloads)npm Cache
Location: ~/.npm or configured cache directory
Safety: 🟢 Safe to delete
Impact: Packages will be re-downloaded when needed
Cleanup:
npm cache clean --forcepip Cache
Location: ~/Library/Caches/pip (macOS)
Safety: 🟢 Safe to delete
Impact: Packages will be re-downloaded when needed
Cleanup:
pip cache purge
# or for pip3
pip3 cache purgeApplication Logs
~/Library/Logs
What it is: Application log files
Safety: 🟢 Safe to delete
Impact: Loss of diagnostic information (only matters if debugging)
Typical size: 1-20 GB
Cleanup:
rm -rf ~/Library/Logs/*/var/log (System Logs)
What it is: System and service log files
Safety: 🟢 Safe to delete old logs (requires sudo)
Impact: Loss of system diagnostic history
Note: macOS automatically rotates logs, manual deletion rarely needed
Application Data
~/Library/Application Support
What it is: Persistent application data, settings, and databases
Safety: 🟡 Caution required
Contains:
- Application databases
- User preferences and settings
- Downloaded content
- Plugins and extensions
- Save games
When safe to delete:
- Application is confirmed uninstalled
- Folder belongs to trial software no longer used
- Folder is for outdated version of app (check first!)
When to KEEP:
- Active applications
- Any folder you're uncertain about
Recommendation: Use find_app_remnants.py to identify orphaned data
~/Library/Containers
What it is: Sandboxed application data (for App Store apps)
Safety: 🟡 Caution required
Same rules as Application Support - only delete for uninstalled apps
~/Library/Preferences
What it is: Application preference files (.plist)
Safety: 🟡 Caution required
Impact of deletion: App returns to default settings
When to delete:
- App is confirmed uninstalled
- Troubleshooting a misbehaving app (as last resort)
Development Environment
Docker
ABSOLUTE RULE: NEVER use any prune command (docker image prune, docker volume prune, docker system prune, docker container prune). Always delete by specifying exact object IDs or names.
Images
What it is: Container images (base OS + application layers)
Safety: 🟡 Requires per-image verification
Analysis:
# List all images sorted by size
docker images --format "table {{.ID}}\t{{.Repository}}:{{.Tag}}\t{{.Size}}\t{{.CreatedSince}}" | sort -k3 -h -r
# Identify dangling images
docker images -f "dangling=true" --format "{{.ID}}\t{{.Size}}\t{{.CreatedSince}}"
# For EACH image, verify no container references it
docker ps -a --filter "ancestor=<IMAGE_ID>" --format "{{.Names}}\t{{.Status}}"Cleanup (only after per-image verification):
# Remove specific images by ID
docker rmi a02c40cc28df 555434521374 f471137cd508Containers
What it is: Running or stopped container instances
Safety: 🟡 Stopped containers may be restarted -- verify with user
Analysis:
docker ps -a --format "table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Size}}"Cleanup (only after user confirms each container/project):
# Remove specific containers by name
docker rm container-name-1 container-name-2Volumes
What it is: Persistent data storage for containers
Safety: 🔴 CAUTION - May contain databases, user uploads, and irreplaceable data
Analysis:
# List all volumes
docker volume ls
# Check which container uses each volume
docker ps -a --filter "volume=<VOLUME_NAME>" --format "{{.Names}}\t{{.Status}}"
# CRITICAL: For database volumes (mysql, postgres, redis in name), inspect contents
docker run --rm -v <VOLUME_NAME>:/data alpine ls -la /data
docker run --rm -v <VOLUME_NAME>:/data alpine du -sh /data/*Cleanup (only after per-volume confirmation, database volumes require content inspection):
# Remove specific volumes by name
docker volume rm project-mysql-data project-redis-dataBuild Cache
What it is: Intermediate build layers
Safety: 🟢 Safe to delete (rebuilds just take longer)
Note: docker builder prune is the ONE exception to the prune prohibition -- build cache contains only intermediate layers, never user data.
Cleanup:
docker builder prune -anode_modules
What it is: Installed npm packages for Node.js projects
Safety: 🟢 Safe to delete (can be regenerated)
Impact: Need to run npm install to restore
Finding large node_modules:
find ~ -name "node_modules" -type d -prune -print 2>/dev/null | while read dir; do
du -sh "$dir"
done | sort -hrCleanup:
# For old projects
rm -rf /path/to/old-project/node_modulesPython Virtual Environments
What it is: Isolated Python environments
Location: venv/, .venv/, env/ in project directories
Safety: 🟢 Safe to delete (can be recreated)
Impact: Need to recreate virtualenv and reinstall packages
Finding venvs:
find ~ -type d -name "venv" -o -name ".venv" 2>/dev/nullGit Repositories (.git directories)
What it is: Git version control data
Safety: 🟡 Depends on use case
When SAFE to delete:
- Project is archived and you have remote backup
- You only need final code, not history
When to KEEP:
- Active development
- No remote backup exists
- You might need the history
Cleanup (convert to plain folder, lose history):
rm -rf /path/to/old-project/.gitLarge Files
Downloads Folder
What it is: Files downloaded from internet
Safety: 🟡 User judgment required
Common cleanable items:
- Old installers (.dmg, .pkg)
- Zip archives already extracted
- Temporary downloads
- Duplicate files
Check before deleting: Might contain important downloads
Disk Images (.dmg, .iso)
What it is: Mountable disk images, often installers
Safety: 🟢 Safe to delete after installation
Typical location: ~/Downloads
Cleanup: Delete .dmg files for already-installed apps
Archives (.zip, .tar.gz)
What it is: Compressed archives
Safety: 🟡 Check if extracted
Before deleting: Verify contents are extracted elsewhere
Old iOS Backups
Location: ~/Library/Application Support/MobileSync/Backup/
What it is: iTunes/Finder iPhone/iPad backups
Safety: 🟡 Caution - backup data
Check:
ls -lh ~/Library/Application\ Support/MobileSync/Backup/Cleanup: Delete old backups via Finder preferences, not manually
Old Time Machine Local Snapshots
What it is: Local Time Machine backups
Safety: 🟢 Safe - macOS manages automatically
macOS automatically deletes these when disk space is low
Check:
tmutil listlocalsnapshots /Manual cleanup (rarely needed):
tmutil deletelocalsnapshots <snapshot_date>What to NEVER Delete
User Data Directories
~/Documents~/Desktop~/Pictures~/Movies~/Music
System Files
/System/Library/Apple(unless you know what you're doing)/private/etc
Security & Credentials
~/.ssh(SSH keys)~/Library/Keychains(passwords, certificates)- Any files containing credentials
Active Databases
*.db,*.sqlitefiles for running applications- Docker volumes in active use
Safety Checklist
Before deleting ANY directory:
1. ✅ Do you know what it is? 2. ✅ Is the application truly uninstalled? 3. ✅ Have you checked if it's in use? (lsof, Activity Monitor) 4. ✅ Do you have a Time Machine backup? 5. ✅ Have you confirmed with the user?
When in doubt, DON'T DELETE.
Recovery Options
Trash vs. Permanent Deletion
Use Trash when possible:
# Move to trash (recoverable)
osascript -e 'tell app "Finder" to move POSIX file "/path/to/file" to trash'Permanent deletion:
# Cannot be recovered without Time Machine
rm -rf /path/to/fileTime Machine
If you deleted something important:
1. Open Time Machine 2. Navigate to parent directory 3. Select date before deletion 4. Restore
File Recovery Tools
If no Time Machine backup:
- Disk Drill (commercial)
- PhotoRec (free, for photos)
- TestDisk (free, for files)
Note: Success rate depends on how recently deleted and disk usage since deletion.
Docker Deep Analysis
Detailed Docker analysis workflow referenced from SKILL.md Step 2. Use this when development-environment cleanup involves Docker images, containers, volumes, or build cache.
Step 2A: Docker Deep Analysis
Use agent team to analyze Docker resources in parallel for comprehensive coverage:
Agent 1 — Images:
# List all images sorted by size
docker images --format "table {{.ID}}\t{{.Repository}}:{{.Tag}}\t{{.Size}}\t{{.CreatedSince}}" | sort -k3 -h -r
# Identify dangling images (no tag)
docker images -f "dangling=true" --format "{{.ID}}\t{{.Size}}\t{{.CreatedSince}}"
# For each image, check if any container references it
docker ps -a --filter "ancestor=<IMAGE_ID>" --format "{{.Names}}\t{{.Status}}"Agent 2 — Containers and Volumes:
# All containers with status
docker ps -a --format "table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Size}}"
# All volumes with size
docker system df -v | grep -A 1000 "VOLUME NAME"
# Identify dangling volumes
docker volume ls -f dangling=true
# For each volume, check which container uses it
docker ps -a --filter "volume=<VOLUME_NAME>" --format "{{.Names}}"Agent 3 — System Level:
# Docker disk usage summary
docker system df
# Build cache
docker builder du
# Container logs size
for c in $(docker ps -a --format "{{.Names}}"); do
echo "$c: $(docker inspect --format='{{.LogPath}}' $c | xargs ls -lh 2>/dev/null | awk '{print $5}')"
doneVersion Management Awareness: Identify version-managed images (e.g., Supabase managed by CLI). When newer versions are confirmed running, older versions are safe to remove. Pay attention to Docker Compose naming conventions (dash vs underscore).
Step 2B: OrbStack-Specific Analysis
OrbStack users have additional considerations.
data.img.raw is a Sparse File:
# Logical size (can show 8TB+, meaningless)
ls -lh ~/Library/OrbStack/data/data.img.raw
# Actual disk usage (this is what matters)
du -h ~/Library/OrbStack/data/data.img.rawThe logical vs actual size difference is normal. Only actual usage counts.
Post-Cleanup: Reclaim Disk Space: After cleaning Docker objects inside OrbStack, data.img.raw does NOT shrink automatically. Instruct user: Open OrbStack Settings → "Reclaim disk space" to compact the sparse file.
OrbStack Logs: Typically 1-2 MB total (~/Library/OrbStack/log/). Not worth cleaning.
Step 2C: Double-Check Verification Protocol
Before deleting ANY Docker object, perform independent verification.
For Images:
# Verify no container (running or stopped) references the image
docker ps -a --filter "ancestor=<IMAGE_ID>" --format "{{.Names}}\t{{.Status}}"
# If empty → safe to delete with: docker rmi <IMAGE_ID>For Volumes:
# Verify no container mounts the volume
docker ps -a --filter "volume=<VOLUME_NAME>" --format "{{.Names}}"
# If empty → check if database volume (see below)
# If not database → safe to delete with: docker volume rm <VOLUME_NAME>Database Volume Red Flag Rule: If volume name contains mysql, postgres, redis, mongo, or mariadb, MANDATORY content inspection:
# Inspect database volume contents with temporary container
docker run --rm -v <VOLUME_NAME>:/data alpine ls -la /data
docker run --rm -v <VOLUME_NAME>:/data alpine du -sh /data/*Only delete after user confirms the data is not needed.
Bonus: Dockerfile Optimization Discoveries
During image analysis, if you discover oversized images, suggest multi-stage build optimization:
# Before: 884 MB (full build environment in final image)
FROM node:20
COPY . .
RUN npm ci && npm run build
CMD ["node", "dist/index.js"]
# After: ~150 MB (only runtime in final image)
FROM node:20 AS builder
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-slim
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
CMD ["node", "dist/index.js"]Key techniques: multi-stage builds, slim/alpine base images, .dockerignore, layer ordering.
Mole Integration Guide
How to integrate Mole with the macOS Cleaner skill.
About Mole
Mole is a command-line interface (CLI) tool for macOS disk cleanup. It provides:
- Interactive terminal-based disk usage analysis
- Comprehensive cleanup for caches, logs, and application remnants
- Developer environment cleanup (Docker, npm, pip, Homebrew, etc.)
- Safe deletion with preview (
--dry-run)
Repository: https://github.com/tw93/Mole
Critical: TTY Environment Required
IMPORTANT: Mole requires a TTY (terminal) environment for interactive commands. When running Mole from automated environments (scripts, Claude Code, CI/CD), use tmux to provide a proper TTY.
# Create tmux session for Mole commands
tmux new-session -d -s mole -x 120 -y 40
# Send command to tmux session
tmux send-keys -t mole 'mo analyze' Enter
# Capture output
tmux capture-pane -t mole -p
# Clean up when done
tmux kill-session -t moleInstallation
Check if Mole is Installed
# Check if mole command exists
which mo && mo --versionExpected output:
/opt/homebrew/bin/mo
Mole version X.Y.Z
macOS: XX.X
Architecture: arm64
...Installation via Homebrew (Recommended)
brew install tw93/tap/moleVersion Check and Update
IMPORTANT: Always check if Mole is up-to-date before use. The tool updates frequently with bug fixes and new features.
# Check current vs latest version
brew info tw93/tap/mole | head -5
# If outdated, upgrade
brew upgrade tw93/tap/moleAvailable Commands
CRITICAL: Only use mo --help to view help. Do NOT append --help to other commands as it may cause unexpected behavior.
# View all commands (SAFE - the only help command)
mo --helpAvailable commands from mo --help:
| Command | Description | Safety |
|---|---|---|
mo | Interactive main menu | Requires TTY |
mo clean | Free up disk space | DANGEROUS - deletes files |
mo clean --dry-run | Preview cleanup (no deletion) | Safe |
mo analyze | Explore disk usage | Safe (read-only) |
mo status | Monitor system health | Safe (read-only) |
mo uninstall | Remove apps completely | DANGEROUS |
mo purge | Remove old project artifacts | DANGEROUS |
mo optimize | Check and maintain system | Caution required |
mo installer | Find and remove installer files | Caution required |
mo analyze vs mo clean --dry-run
CRITICAL: These are two different tools with different purposes. Use the right tool for the job.
Comparison Table
| Aspect | mo analyze | mo clean --dry-run |
|---|---|---|
| Primary Purpose | Explore disk usage interactively | Preview cleanup categories |
| Use When | Understanding what consumes space | Ready to see cleanup options |
| Interface | Interactive TUI with tree navigation | Static list output |
| Navigation | Arrow keys to drill into directories | No navigation |
| Detail Level | Full directory breakdown | Only cleanup-eligible items |
| Recommended Order | Use FIRST | Use SECOND (after analyze) |
When to Use Each
Use `mo analyze` when:
- User asks "What's taking up space?" or "Where is my disk space going?"
- Need to understand storage consumption patterns
- Want to explore specific directories interactively
- Investigating unexpected disk usage
Use `mo clean --dry-run` when:
- Already know what's consuming space (after
mo analyze) - User is ready to see cleanup recommendations
- Need a quick preview of what can be cleaned
- Preparing to run
mo cleanfor actual cleanup
Workflow Recommendation
Step 1: mo analyze (understand the problem)
↓
Step 2: Present findings to user
↓
Step 3: mo clean --dry-run (show cleanup options)
↓
Step 4: User confirms cleanup categories
↓
Step 5: User runs mo clean (actual cleanup)Common Mistake
# ❌ WRONG: Jumping straight to cleanup preview
tmux send-keys -t mole 'mo clean --dry-run' Enter
# This only shows cleanup-eligible items, not the full picture
# ✅ CORRECT: Start with disk analysis
tmux send-keys -t mole 'mo analyze' Enter
# This shows where ALL disk space is goingInteractive TUI Navigation (mo analyze)
mo analyze provides an interactive tree view. Navigate using tmux key sequences:
# Start analysis
tmux send-keys -t mole 'mo analyze' Enter
# Wait for scan to complete (5-10 minutes for Home directory!)
sleep 300 # 5 minutes for large directories
# Capture current view
tmux capture-pane -t mole -p
# Navigate down to next item
tmux send-keys -t mole Down
# Expand/enter selected directory
tmux send-keys -t mole Enter
# Go back up
tmux send-keys -t mole Up
# Quit the TUI
tmux send-keys -t mole 'q'Safe Analysis Workflow
Step 1: Check Version First
# Always ensure latest version
brew info tw93/tap/mole | head -3Step 2: Create TTY Environment
# Start tmux session
tmux new-session -d -s mole -x 120 -y 40Step 3: Run Analysis (Safe Commands Only)
# Disk analysis - SAFE, read-only
tmux send-keys -t mole 'mo analyze' Enter
# Wait for scan to complete (be patient!)
sleep 30 # Home directory scanning can take several minutes
# Capture results
tmux capture-pane -t mole -pStep 4: Preview Cleanup (No Actual Deletion)
# Preview what would be cleaned - SAFE
tmux send-keys -t mole 'mo clean --dry-run' Enter
sleep 10
tmux capture-pane -t mole -pStep 5: User Confirmation Required
NEVER execute mo clean without explicit user confirmation. Always: 1. Show the --dry-run preview results to user 2. Wait for user to confirm each category 3. User runs the actual cleanup command themselves
Safety Principles
0. Value Over Vanity (Most Important)
Your goal is NOT to maximize cleaned space. Your goal is to identify truly useless items while preserving valuable caches.
The vanity trap: Showing "Cleaned 50GB!" feels impressive but:
- User spends 2 hours redownloading npm packages
- Next Xcode build takes 30 minutes instead of 30 seconds
- AI project fails because models need redownload
See SKILL.md sections "Anti-Patterns: What NOT to Delete" and "What IS Safe to Delete" for the full tables of items to keep vs items safe to remove.
1. Never Execute Dangerous Commands Automatically
# ❌ NEVER do this automatically
mo clean
mo uninstall
mo purge
docker system prune -a --volumes
docker volume prune -f
rm -rf ~/Library/Caches/*
# ✅ ALWAYS use preview/dry-run first
mo clean --dry-run2. Patience is Critical
mo analyzeon large home directories can take 5-10 minutes- Do NOT interrupt or skip slow scans
- Report progress to user regularly
- Wait for complete results before making decisions
3. User Executes Cleanup
After analysis and confirmation:
Present findings to user, then provide command for them to run:
"Based on the analysis, you can reclaim approximately 30GB.
To proceed, please run this command in your terminal:
mo clean
You will be prompted to confirm each category interactively."Mole Command Details
mo analyze
Interactive disk usage explorer. Scans these locations:
- Home directory (
~) - App Library (
~/Library/Application Support) - Applications (
/Applications) - System Library (
/Library) - Volumes
Usage in tmux:
tmux send-keys -t mole 'mo analyze' Enter
# Navigate with arrow keys (send via tmux)
tmux send-keys -t mole Down # Move to next item
tmux send-keys -t mole Enter # Select/expand item
tmux send-keys -t mole 'q' # Quitmo clean --dry-run
Preview cleanup without deletion. Shows:
- User essentials (caches, logs, trash)
- macOS system caches
- Browser caches
- Developer tool caches (npm, pip, uv, Homebrew, Docker, etc.)
Whitelist: Mole maintains a whitelist of protected patterns. Check with:
mo clean --whitelistmo status
System health monitoring (CPU, memory, disk, network). Requires TTY for real-time display.
mo purge
Cleans old project build artifacts (node_modules, target, venv, etc.) from configured directories.
Check/configure scan paths:
mo purge --pathsIntegration with Claude Code
Recommended Workflow
1. Version check: Ensure Mole is installed and up-to-date 2. TTY setup: Create tmux session for interactive commands 3. Analysis: Run mo analyze or mo clean --dry-run 4. Progress reporting: Inform user of scan progress 5. Present findings: Show structured results to user 6. User confirmation: Wait for explicit approval 7. Provide command: Give user the command to run themselves
Example Session
# 1. Check version
$ brew info tw93/tap/mole | head -3
# Output: tw93/tap/mole: stable 1.20.0
# Installed: 1.13.13 -> needs upgrade
# 2. Upgrade if needed
$ brew upgrade tw93/tap/mole
# 3. Create tmux session
$ tmux new-session -d -s mole -x 120 -y 40
# 4. Run dry-run analysis
$ tmux send-keys -t mole 'mo clean --dry-run' Enter
# 5. Wait and capture output
$ sleep 15 && tmux capture-pane -t mole -p
# 6. Present to user:
"""
📊 Cleanup Preview (dry-run - no files deleted)
User essentials:
- User app cache: 16.67 GB
- User app logs: 102.3 MB
- Trash: 642.9 MB
Developer tools:
- uv cache: 9.96 GB
- npm cache: (pending)
- Docker: (pending)
Total recoverable: ~27 GB
To proceed with cleanup, please run in your terminal:
mo clean
"""Troubleshooting
"device not configured" Error
Cause: Command run without TTY environment.
Solution: Use tmux:
tmux new-session -d -s mole
tmux send-keys -t mole 'mo status' EnterScan Stuck on "pending"
Cause: Large directories take time to scan.
Solution: Be patient. Home directory with many files can take 5-10 minutes. Monitor progress:
# Check if still scanning (spinner animation in output)
tmux capture-pane -t mole -p | tail -10Non-Interactive Mode Auto-Executes
WARNING: Some Mole commands may auto-execute in non-TTY environments without confirmation!
Solution: ALWAYS use tmux for ANY Mole command, even help:
# ❌ DANGEROUS - may auto-execute
mo clean --help # Might run cleanup instead of showing help!
# ✅ SAFE - use mo --help only
mo --help # The ONLY safe help commandVersion Mismatch
Cause: Local version outdated.
Solution:
# Check versions
brew info tw93/tap/mole
# Upgrade
brew upgrade tw93/tap/moleSummary
Key Points: 1. Mole is a CLI tool, not a GUI application 2. Install via brew install tw93/tap/mole 3. Always check version before use 4. Use tmux for all interactive commands 5. mo --help is the ONLY safe help command 6. Never auto-execute cleanup commands 7. Be patient - scans take time 8. User runs cleanup - provide command, don't execute
Multi-Layer Deep Exploration with Mole
For comprehensive analysis, perform multi-layer exploration, not just top-level scans. This section documents the proven workflow for navigating Mole's TUI.
Navigation Commands
# Create session
tmux new-session -d -s mole -x 120 -y 40
# Start analysis
tmux send-keys -t mole 'mo analyze' Enter
# Wait for initial scan
sleep 8 && tmux capture-pane -t mole -p
# Navigation keys (send via tmux)
tmux send-keys -t mole Enter # Enter/expand selected directory
tmux send-keys -t mole Left # Go back to parent directory
tmux send-keys -t mole Down # Move to next item
tmux send-keys -t mole Up # Move to previous item
tmux send-keys -t mole 'q' # Quit TUI
# Capture current view
tmux capture-pane -t mole -pMulti-Layer Exploration Workflow
Step 1: Top-level overview
# Start mo analyze, wait for initial menu
tmux send-keys -t mole 'mo analyze' Enter
sleep 8 && tmux capture-pane -t mole -p
# Example output:
# 1. Home 289.4 GB (58.5%)
# 2. App Library 145.2 GB (29.4%)
# 3. Applications 49.5 GB (10.0%)
# 4. System Library 10.3 GB (2.1%)Step 2: Enter largest directory (Home)
tmux send-keys -t mole Enter
sleep 10 && tmux capture-pane -t mole -p
# Example output:
# 1. Library 144.4 GB (49.9%)
# 2. Workspace 52.0 GB (18.0%)
# 3. .cache 19.3 GB (6.7%)
# 4. Applications 17.0 GB (5.9%)
# ...Step 3: Drill into specific directories
# Go to .cache (3rd item: Down Down Enter)
tmux send-keys -t mole Down Down Enter
sleep 5 && tmux capture-pane -t mole -p
# Example output:
# 1. uv 10.3 GB (55.6%)
# 2. modelscope 5.5 GB (29.5%)
# 3. huggingface 887.8 MB (4.7%)Step 4: Navigate back and explore another branch
# Go back to parent
tmux send-keys -t mole Left
sleep 2
# Navigate to different directory
tmux send-keys -t mole Down Down Down Down Enter # Go to .npm
sleep 5 && tmux capture-pane -t mole -pStep 5: Deep dive into Library
# Back to Home, then into Library
tmux send-keys -t mole Left
tmux send-keys -t mole Up Up Up Up Up Up Enter # Go to Library
sleep 10 && tmux capture-pane -t mole -p
# Example output:
# 1. Application Support 37.1 GB
# 2. Containers 35.4 GB
# 3. Developer 17.8 GB ← Xcode is here
# 4. Caches 8.2 GBRecommended Exploration Path
For comprehensive analysis, follow this exploration tree:
mo analyze
├── Home (Enter)
│ ├── Library (Enter)
│ │ ├── Developer (Enter) → Xcode/DerivedData, iOS DeviceSupport
│ │ ├── Caches (Enter) → Playwright, JetBrains, etc.
│ │ └── Application Support (Enter) → App data
│ ├── .cache (Enter) → uv, modelscope, huggingface
│ ├── .npm (Enter) → _cacache, _npx
│ ├── Downloads (Enter) → Large files to review
│ ├── .Trash (Enter) → Confirm trash contents
│ └── miniconda3/other dev tools (Enter) → Check last used time
├── App Library → Usually overlaps with ~/Library
└── Applications → Installed appsTime Expectations
| Directory | Scan Time | Notes |
|---|---|---|
| Top-level menu | 5-8 seconds | Fast |
| Home directory | 5-10 minutes | Large, be patient |
| ~/Library | 3-5 minutes | Many small files |
| Subdirectories | 2-30 seconds | Varies by size |
Example Complete Session
# 1. Create session
tmux new-session -d -s mole -x 120 -y 40
# 2. Start analysis and get overview
tmux send-keys -t mole 'mo analyze' Enter
sleep 8 && tmux capture-pane -t mole -p
# 3. Enter Home
tmux send-keys -t mole Enter
sleep 10 && tmux capture-pane -t mole -p
# 4. Enter .cache to see dev caches
tmux send-keys -t mole Down Down Enter
sleep 5 && tmux capture-pane -t mole -p
# 5. Back to Home, then to .npm
tmux send-keys -t mole Left
sleep 2
tmux send-keys -t mole Down Down Down Down Enter
sleep 5 && tmux capture-pane -t mole -p
# 6. Back to Home, enter Library
tmux send-keys -t mole Left
sleep 2
tmux send-keys -t mole Up Up Up Up Up Up Enter
sleep 10 && tmux capture-pane -t mole -p
# 7. Enter Developer to see Xcode
tmux send-keys -t mole Down Down Down Enter
sleep 5 && tmux capture-pane -t mole -p
# 8. Enter Xcode
tmux send-keys -t mole Enter
sleep 5 && tmux capture-pane -t mole -p
# 9. Enter DerivedData to see projects
tmux send-keys -t mole Enter
sleep 5 && tmux capture-pane -t mole -p
# 10. Cleanup
tmux kill-session -t moleKey Insights from Exploration
After multi-layer exploration, you will discover:
1. What projects are using DerivedData - specific project names 2. Which caches are actually large - uv vs npm vs others 3. Age of files - Mole shows ">3mo", ">7mo", ">1yr" markers 4. Specific volumes and their purposes - Docker project data 5. Downloads that can be cleaned - old dmgs, duplicate files
Report Templates
Detailed report templates referenced from SKILL.md. The core report format and classification legend live in SKILL.md; this file holds the longer, fill-in-the-blank templates so they load only when needed.
Docker Report: Required Object-Level Detail
Docker reports must list every individual object, not just categories:
#### Dangling Images (no tag, no container references)
| Image ID | Size | Created | Safe? |
|----------|------|---------|-------|
| a02c40cc28df | 884 MB | 2 months ago | ✅ No container uses it |
| 555434521374 | 231 MB | 3 months ago | ✅ No container uses it |
#### Stopped Containers
| Name | Image | Status | Size |
|------|-------|--------|------|
| ragflow-mysql | mysql:8.0 | Exited 2 weeks ago | 1.2 GB |
#### Volumes
| Volume | Size | Mounted By | Contains |
|--------|------|------------|----------|
| ragflow_mysql_data | 1.8 GB | ragflow-mysql | MySQL databases |
| redis_data | 500 MB | (none - dangling) | Redis dump |
#### 🔴 Database Volumes Requiring Inspection
| Volume | Inspected Contents | User Decision |
|--------|--------------------|---------------|
| ragflow_mysql_data | 8 databases, 45 tables | Still need? |High-Quality Report Template (Chinese)
After multi-layer exploration, present findings using this proven template:
## 📊 磁盘空间深度分析报告
**分析日期**: YYYY-MM-DD
**使用工具**: Mole CLI + 多层目录探索
**分析原则**: 安全第一,价值优于虚荣
---
### 总览
| 区域 | 总占用 | 关键发现 |
|------|--------|----------|
| **Home** | XXX GB | Library占一半(XXX GB) |
| **App Library** | XXX GB | 与Home/Library重叠统计 |
| **Applications** | XXX GB | 应用本体 |
---
### 🟢 绝对安全可删除 (约 X.X GB)
| 项目 | 大小 | 位置 | 删除后影响 | 清理命令 |
|------|------|------|-----------|---------|
| **废纸篓** | XXX MB | ~/.Trash | 无 - 你已决定删除的文件 | 清空废纸篓 |
| **npm _npx** | X.X GB | ~/.npm/_npx | 下次 npx 命令重新下载 | `rm -rf ~/.npm/_npx` |
| **Homebrew 旧版本** | XX MB | /opt/homebrew | 无 - 已被新版本替代 | `brew cleanup --prune=0` |
**废纸篓内容预览**:
- [列出主要文件]
---
### 🟡 需要你确认的项目
#### 1. [项目名] (X.X GB) - [状态描述]
| 子目录 | 大小 | 最后使用 |
|--------|------|----------|
| [子目录1] | X.X GB | >X个月 |
| [子目录2] | X.X GB | >X个月 |
**问题**: [需要用户回答的问题]
---
#### 2. Downloads 中的旧文件 (X.X GB)
| 文件/目录 | 大小 | 年龄 | 建议 |
|-----------|------|------|------|
| [文件1] | X.X GB | - | [建议] |
| [文件2] | XXX MB | >X个月 | [建议] |
**建议**: 手动检查 Downloads,删除已不需要的文件。
---
#### 3. 停用的 Docker 项目 Volumes
| 项目前缀 | 可能包含的数据 | 需要你确认 |
|---------|--------------|-----------|
| `project1_*` | MySQL, Redis | 还在用吗? |
| `project2_*` | Postgres | 还在用吗? |
**注意**: 我不会使用 `docker volume prune -f`,只会在你确认后删除特定项目的 volumes。
---
### 🔴 不建议删除的项目 (有价值的缓存)
| 项目 | 大小 | 为什么要保留 |
|------|------|-------------|
| **Xcode DerivedData** | XX GB | [项目名]的编译缓存,删除后下次构建需要X分钟 |
| **npm _cacache** | X.X GB | 所有下载过的 npm 包,删除后需要重新下载 |
| **~/.cache/uv** | XX GB | Python 包缓存,重新下载在中国网络下很慢 |
| [其他有价值的缓存] | X.X GB | [保留原因] |
---
### 📋 其他发现
| 项目 | 大小 | 说明 |
|------|------|------|
| **OrbStack/Docker** | XX GB | 正常的 VM/容器占用 |
| [其他发现] | X.X GB | [说明] |
---
### ✅ 推荐操作
**立即可执行** (无需确认):1. 清空废纸篓 (XXX MB)
手动: Finder → 清空废纸篓
2. npm _npx (X.X GB)
rm -rf ~/.npm/_npx
3. Homebrew 旧版本 (XX MB)
brew cleanup --prune=0
**预计释放**: ~X.X GB
---
**需要你确认后执行**:
1. **[项目1]** - [确认问题]
2. **[项目2]** - [确认问题]
3. **Docker 项目** - 告诉我哪些项目确定不用了Safety Rules for macOS Cleanup
Critical safety guidelines to prevent data loss and system damage.
Golden Rules
Rule 1: Never Delete Without Confirmation
ALWAYS ask user before deleting ANY file or directory.
Bad:
shutil.rmtree(cache_dir) # Immediately deletesGood:
if confirm_delete(cache_dir, size, description):
shutil.rmtree(cache_dir)
else:
print("Skipped")Rule 2: Explain Before Deleting
Users should understand:
- What is being deleted
- Why it's safe (or not safe)
- Impact of deletion
- Recoverability (can it be restored?)
Rule 3: When in Doubt, Don't Delete
If uncertain about safety: DON'T DELETE.
Ask user to verify instead.
Rule 4: High-Risk Paths Are Hard Blocks
safe_delete.py must refuse dangerous system and credential paths before confirmation and inside the delete function. A warning is not enough for:
/,/System,/usr,/bin,/etc~/.ssh,~/.aws,~/.gnupg~/Library/Keychains
These paths and their descendants are blocked even when the user selects all in batch mode.
Rule 5: Suggest Backups for Large Deletions
Before deleting >10 GB, recommend Time Machine backup.
Rule 6: Docker Prune Prohibition
NEVER use any Docker prune command. This includes:
docker image prune/docker image prune -adocker container prunedocker volume prune/docker volume prune -fdocker system prune/docker system prune -a --volumes
Why: Prune commands operate on categories, not specific objects. They can silently destroy database volumes, user uploads, and container state that the user intended to keep. A user who loses their MySQL data because of a prune command will never trust this tool again.
Correct approach: Always specify exact object IDs or names:
# Images: delete by specific ID
docker rmi a02c40cc28df 555434521374
# Containers: delete by specific name
docker rm container-name-1 container-name-2
# Volumes: delete by specific name
docker volume rm project-mysql-data project-redis-dataRule 7: Double-Check Verification Protocol
Before deleting ANY Docker object, perform independent cross-verification. This applies to images, volumes, and containers.
Key requirements:
- For images: verify no container (running or stopped) references the image
- For volumes: verify no container mounts the volume
- For database volumes (name contains mysql, postgres, redis, mongo, mariadb): MANDATORY content inspection with a temporary container
- Even if Docker reports a volume as "dangling", the data inside may be valuable
See SKILL.md Step 4 for the complete verification commands and database volume inspection workflow.
Rule 8: Use Trash When Possible
Prefer moving to Trash over permanent deletion:
# Recoverable
osascript -e 'tell app "Finder" to move POSIX file "/path/to/file" to trash'
# Permanent (use only when confirmed safe)
rm -rf /path/to/fileNever Delete These
System Directories
| Path | Why | Impact if Deleted |
|---|---|---|
/System | macOS core | System unbootable |
/Library/Apple | Apple frameworks | Apps won't launch |
/etc, /private/etc | System config | System unstable |
/private/var/db | System databases | System unstable |
/usr | Unix utilities | Commands won't work |
/bin, /sbin | System binaries | System unusable |
User Data
| Path | Why | Impact if Deleted |
|---|---|---|
~/Documents | User documents | Data loss |
~/Desktop | User files | Data loss |
~/Pictures | Photos | Data loss |
~/Movies | Videos | Data loss |
~/Music | Music library | Data loss |
~/Downloads | May contain important files | Potential data loss |
Security & Credentials
| Path | Why | Impact if Deleted |
|---|---|---|
~/.ssh | SSH keys | Cannot access servers |
~/.aws | Cloud credentials | Cannot access cloud resources |
~/.gnupg | GPG keys | Cannot decrypt or sign data |
~/Library/Keychains | Passwords, certificates | Cannot access accounts/services |
| Any file with "credential", "password", "key" in name | Security data | Cannot authenticate |
Active Databases
| Pattern | Why | Impact if Deleted |
|---|---|---|
*.db, *.sqlite, *.sqlite3 | Application databases | App data loss |
| Any database file for running app | Active data | Data corruption |
Running Applications
| Path | Why | Impact if Deleted |
|---|---|---|
/Applications | Installed apps | Apps won't launch |
~/Applications | User-installed apps | Apps won't launch |
Files in use (check with lsof) | Currently open | App crash, data corruption |
Require Extra Confirmation
Large Deletions
Threshold: >10 GB
Action: Warn user and suggest Time Machine backup
Example:
⚠️ This operation will delete 45 GB of data.
💡 Recommendation:
Create a Time Machine backup first.
Check last backup:
tmutil latestbackup
Create backup now:
tmutil startbackup
Proceed without backup? [y/N]:System-Wide Caches
Paths: /Library/Caches, /var/log
Action: Require manual sudo command (don't execute directly)
Example:
⚠️ This operation requires administrator privileges.
Please run this command manually:
sudo rm -rf /Library/Caches/*
⚠️ You will be asked for your password.Reason:
- Requires elevated privileges
- User should be aware of system-wide impact
- Audit trail (user types password)
Docker Objects (Images, Containers, Volumes)
Action: List every object individually. Use precision deletion only (see Rule 6 and Rule 7).
NEVER use prune commands. Always specify exact IDs/names.
Example for volumes:
Docker volumes found:
postgres_data (1.2 GB) - Contains PostgreSQL database
redis_data (500 MB) - Contains Redis cache data
app_uploads (3 GB) - Contains user-uploaded files
Database volumes inspected with temporary container:
postgres_data: 8 databases, 45 tables, last modified 2 days ago
redis_data: 12 MB dump.rdb
Confirm EACH volume individually:
Delete postgres_data? [y/N]:
Delete redis_data? [y/N]:
Delete app_uploads? [y/N]:
Deletion commands (after confirmation):
docker volume rm postgres_data redis_dataApplication Preferences
Path: ~/Library/Preferences/*.plist
Action: Warn that app will reset to defaults
Example:
⚠️ Deleting preferences will reset the app to defaults.
Impact:
- All settings will be lost
- Custom configurations will be reset
- May need to re-enter license keys
Only delete if:
- App is misbehaving (troubleshooting)
- App is confirmed uninstalled
Proceed? [y/N]:Safety Checks Before Deletion
Check 1: Path Exists
if not os.path.exists(path):
print(f"❌ Path does not exist: {path}")
return FalseCheck 2: Not a Blocked Path
blocked_paths = [
'/System', '/Library/Apple', '/etc', '/private/etc',
'/usr', '/bin', '/sbin', '/private/var/db',
'~/.ssh', '~/.aws', '~/.gnupg', '~/Library/Keychains',
]
expanded_path = os.path.realpath(os.path.expanduser(path))
if expanded_path == '/':
print("❌ Cannot delete root path")
return False
for blocked_path in blocked_paths:
expanded_blocked = os.path.realpath(os.path.expanduser(blocked_path))
if expanded_path == expanded_blocked or expanded_path.startswith(expanded_blocked + os.sep):
print(f"❌ Cannot delete blocked path: {path}")
return FalseCheck 3: Not User Data
user_data_paths = [
'~/Documents', '~/Desktop', '~/Pictures',
'~/Movies', '~/Music'
]
expanded_path = os.path.expanduser(path)
for data_path in user_data_paths:
if expanded_path.startswith(os.path.expanduser(data_path)):
print(f"⚠️ This is a user data directory: {path}")
print(" Are you ABSOLUTELY sure? [type 'DELETE' to confirm]:")
response = input().strip()
if response != 'DELETE':
return FalseCheck 4: Not in Use
def is_in_use(path):
"""Check if file/directory is in use."""
try:
result = subprocess.run(
['lsof', path],
capture_output=True,
text=True
)
# If lsof finds processes using the file, returncode is 0
if result.returncode == 0:
return True
return False
except:
return False # Assume not in use if check fails
if is_in_use(path):
print(f"⚠️ Warning: {path} is currently in use")
print(" Close the application first, then try again.")
return FalseCheck 5: Permissions
def can_delete(path):
"""Check if we have permission to delete."""
try:
# Check parent directory write permission
parent = os.path.dirname(path)
return os.access(parent, os.W_OK)
except:
return False
if not can_delete(path):
print(f"❌ No permission to delete: {path}")
print(" You may need sudo, but be careful!")
return FalseSafe Deletion Workflow
def safe_delete(path, size, description):
"""
Safe deletion workflow with all checks.
Args:
path: Path to delete
size: Size in bytes
description: Human-readable description
Returns:
(success, message)
"""
# Safety checks
if not os.path.exists(path):
return (False, "Path does not exist")
if is_system_path(path):
return (False, "Cannot delete system path")
if is_user_data(path):
if not extra_confirm(path):
return (False, "User cancelled")
if is_in_use(path):
return (False, "Path is in use")
if not can_delete(path):
return (False, "No permission")
# Backup warning for large deletions
if size > 10 * 1024 * 1024 * 1024: # 10 GB
if not confirm_large_deletion(size):
return (False, "User cancelled")
# Final confirmation
if not confirm_delete(path, size, description):
return (False, "User cancelled")
# Execute deletion
try:
if os.path.isfile(path):
os.unlink(path)
else:
shutil.rmtree(path)
return (True, f"Deleted successfully ({format_size(size)} freed)")
except Exception as e:
return (False, f"Deletion failed: {str(e)}")Error Handling
Permission Denied
except PermissionError:
print(f"❌ Permission denied: {path}")
print(" Try running with sudo (use caution!)")Operation Not Permitted (SIP)
# macOS System Integrity Protection blocks some deletions
except OSError as e:
if e.errno == 1: # Operation not permitted
print(f"❌ System Integrity Protection prevents deletion: {path}")
print(" This is a protected system file.")
print(" Do NOT attempt to bypass SIP unless you know what you're doing.")Path Too Long
except OSError as e:
if e.errno == 63: # File name too long
print(f"⚠️ Path too long, trying alternative method...")
# Try using find + rmRecovery Options
If User Accidentally Confirmed
Immediate action: Check Trash first
# Files may be in Trash
ls -lh ~/.TrashNext: Time Machine
# Open Time Machine to date before deletion
tmutil browseLast resort: File recovery tools
- Disk Drill (commercial)
- PhotoRec (free)
- TestDisk (free)
Note: Success rate depends on:
- How recently deleted
- How much disk activity since deletion
- Whether SSD (TRIM) or HDD
Preventing Accidents
1. Use Trash instead of rm when possible 2. Require Time Machine backup for >10 GB deletions 3. Test on small items first before batch operations 4. Show dry-run results before actual deletion
Red Flags to Watch For
User Requests
If user asks to:
- "Delete everything in ~/Library"
- "Clear all caches including system"
- "Delete all .log files on the entire system"
- "Remove all databases"
Response:
⚠️ This request is too broad and risky.
Let me help you with a safer approach:
1. Run analysis to identify specific targets
2. Review each category
3. Delete selectively with confirmation
This prevents accidental data loss.Script Behavior
If script is about to:
- Delete >100 GB at once
- Delete entire directory trees without listing contents
- Run
rm -rf /or similar dangerous commands - Delete from system paths
Action: STOP and ask for confirmation
Testing Guidelines
Before Packaging
Test safety checks:
1. ✅ Attempt to delete system path → Should reject 2. ✅ Attempt to delete user data → Should require extra confirmation 3. ✅ Attempt to delete in-use file → Should warn 4. ✅ Attempt to delete without permission → Should fail gracefully 5. ✅ Large deletion → Should suggest backup
In Production
Always:
- Start with smallest items
- Confirm results after each deletion
- Monitor disk space before/after
- Ask user to verify important apps still work
Summary
Conservative Approach
When implementing cleanup:
1. Assume danger until proven safe 2. Explain everything to user 3. Confirm each step 4. Suggest backups for large operations 5. Use Trash when possible 6. Test thoroughly before packaging
Remember
"It's better to leave 1 GB of unnecessary files than to delete 1 MB of important data."
User trust is fragile. One bad deletion loses it forever.
Final Checklist
Before any deletion:
- [ ] Path is verified to exist
- [ ] Path is not a system path
- [ ] Path is not user data (or extra confirmed)
- [ ] Path is not in use
- [ ] User has been informed of impact
- [ ] User has explicitly confirmed
- [ ] Backup suggested for large deletions
- [ ] Error handling in place
- [ ] Recovery options documented
Only then: proceed with deletion.
#!/usr/bin/env python3
"""
Analyze macOS cache directories and categorize them by size and safety.
Usage:
python3 analyze_caches.py [--user-only] [--min-size SIZE]
Options:
--user-only Only scan user caches (~/Library/Caches), skip system caches
--min-size Minimum size in MB to report (default: 10)
"""
import os
import sys
import subprocess
import argparse
from pathlib import Path
def get_dir_size(path):
"""
Get directory size using du command.
Args:
path: Directory path
Returns:
Size in bytes, or 0 if error
"""
try:
result = subprocess.run(
['du', '-sk', path],
capture_output=True,
text=True,
timeout=30
)
if result.returncode == 0:
# du -sk returns size in KB
size_kb = int(result.stdout.split()[0])
return size_kb * 1024 # Convert to bytes
return 0
except (subprocess.TimeoutExpired, ValueError, IndexError):
return 0
def format_size(bytes_size):
"""Convert bytes to human-readable format."""
for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
if bytes_size < 1024.0:
return f"{bytes_size:.1f} {unit}"
bytes_size /= 1024.0
return f"{bytes_size:.1f} PB"
def analyze_cache_dir(base_path, min_size_bytes):
"""
Analyze a cache directory and list subdirectories by size.
Args:
base_path: Path to cache directory
min_size_bytes: Minimum size to report
Returns:
List of (name, path, size_bytes) tuples
"""
if not os.path.exists(base_path):
return []
results = []
try:
for entry in os.scandir(base_path):
if entry.is_dir():
size = get_dir_size(entry.path)
if size >= min_size_bytes:
results.append((entry.name, entry.path, size))
except PermissionError:
print(f"⚠️ Permission denied: {base_path}", file=sys.stderr)
return []
# Sort by size descending
results.sort(key=lambda x: x[2], reverse=True)
return results
def categorize_safety(name):
"""
Categorize cache safety based on name patterns.
Returns:
('safe'|'check'|'keep', reason)
"""
name_lower = name.lower()
# Known safe to delete
safe_patterns = [
'chrome', 'firefox', 'safari', 'edge', # Browsers
'spotify', 'slack', 'discord', # Communication
'pip', 'npm', 'homebrew', # Package managers
'temp', 'tmp', 'cache' # Generic temp
]
if any(pattern in name_lower for pattern in safe_patterns):
return ('safe', 'Application regenerates cache automatically')
# Check before deleting
check_patterns = [
'xcode', 'android', # IDEs (may slow next launch)
'jetbrains', 'vscode',
'docker' # May contain important build cache
]
if any(pattern in name_lower for pattern in check_patterns):
return ('check', 'May slow down next application launch')
# Default: check first
return ('check', 'Unknown application, verify before deleting')
def main():
parser = argparse.ArgumentParser(
description='Analyze macOS cache directories'
)
parser.add_argument(
'--user-only',
action='store_true',
help='Only scan user caches (skip system caches)'
)
parser.add_argument(
'--min-size',
type=int,
default=10,
help='Minimum size in MB to report (default: 10)'
)
args = parser.parse_args()
min_size_bytes = args.min_size * 1024 * 1024 # Convert MB to bytes
print("🔍 Analyzing macOS Cache Directories")
print("=" * 50)
# User caches
user_cache_path = os.path.expanduser('~/Library/Caches')
print(f"\n📂 User Caches: {user_cache_path}")
print("-" * 50)
user_caches = analyze_cache_dir(user_cache_path, min_size_bytes)
total_user = 0
if user_caches:
print(f"{'Application':<40} {'Size':<12} {'Safety'}")
print("-" * 70)
for name, path, size in user_caches:
safety, reason = categorize_safety(name)
safety_icon = {'safe': '🟢', 'check': '🟡', 'keep': '🔴'}[safety]
print(f"{name:<40} {format_size(size):<12} {safety_icon}")
total_user += size
print("-" * 70)
print(f"{'Total':<40} {format_size(total_user):<12}")
else:
print("No cache directories found above minimum size.")
# User logs
user_log_path = os.path.expanduser('~/Library/Logs')
if os.path.exists(user_log_path):
log_size = get_dir_size(user_log_path)
if log_size >= min_size_bytes:
print(f"\n📝 User Logs: {user_log_path}")
print(f" Size: {format_size(log_size)} 🟢 Safe to delete")
total_user += log_size
# System caches (if not --user-only)
if not args.user_only:
print(f"\n\n📂 System Caches: /Library/Caches")
print("-" * 50)
print("⚠️ Requires administrator privileges to delete")
system_cache_path = '/Library/Caches'
system_caches = analyze_cache_dir(system_cache_path, min_size_bytes)
total_system = 0
if system_caches:
print(f"{'Application':<40} {'Size':<12}")
print("-" * 70)
for name, path, size in system_caches[:10]: # Top 10 only
print(f"{name:<40} {format_size(size):<12}")
total_system += size
if len(system_caches) > 10:
print(f"... and {len(system_caches) - 10} more")
print("-" * 70)
print(f"{'Total':<40} {format_size(total_system):<12}")
else:
print("No cache directories found above minimum size.")
# Summary
print("\n" + "=" * 50)
print("📊 Summary")
print("=" * 50)
print(f"Total User Caches: {format_size(total_user)}")
if not args.user_only:
print(f"Total System Caches: {format_size(total_system)}")
print(f"Combined Total: {format_size(total_user + total_system)}")
print("\n💡 Next Steps:")
print(" 1. Review the list above")
print(" 2. Identify caches marked 🟢 (safe to delete)")
print(" 3. For 🟡 items, verify the application is not running")
print(" 4. Use safe_delete.py for interactive cleanup")
return 0
if __name__ == '__main__':
sys.exit(main())
#!/usr/bin/env python3
"""
Analyze development environment and find cleanable resources.
Checks:
- Docker (images, containers, volumes, build cache)
- Homebrew cache
- npm cache
- pip cache
- Old .git directories in archived projects
Usage:
python3 analyze_dev_env.py
"""
import os
import sys
import subprocess
import json
from pathlib import Path
def format_size(bytes_size):
"""Convert bytes to human-readable format."""
if bytes_size is None:
return "Unknown"
for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
if bytes_size < 1024.0:
return f"{bytes_size:.1f} {unit}"
bytes_size /= 1024.0
return f"{bytes_size:.1f} PB"
def run_command(cmd):
"""Run command and return output, or None if error."""
try:
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=30
)
if result.returncode == 0:
return result.stdout.strip()
return None
except (subprocess.TimeoutExpired, FileNotFoundError):
return None
def get_dir_size(path):
"""Get directory size using du command."""
output = run_command(['du', '-sk', path])
if output:
try:
size_kb = int(output.split()[0])
return size_kb * 1024 # Convert to bytes
except (ValueError, IndexError):
pass
return 0
def check_docker():
"""Check Docker resources."""
print("\n🐳 Docker Resources")
print("=" * 50)
# Check if Docker is installed
if not run_command(['which', 'docker']):
print(" Docker not installed or not in PATH")
return 0
# Check if Docker daemon is running
if not run_command(['docker', 'info']):
print(" Docker daemon not running")
return 0
total_size = 0
# Images
images_output = run_command(['docker', 'images', '-q'])
if images_output:
image_count = len(images_output.split('\n'))
print(f"\n📦 Images: {image_count}")
# Get size estimate
system_output = run_command(['docker', 'system', 'df', '--format', '{{json .}}'])
if system_output:
for line in system_output.split('\n'):
try:
data = json.loads(line)
if data.get('Type') == 'Images':
size_str = data.get('Size', '')
# Parse size (format like "1.2GB")
if 'GB' in size_str:
size = float(size_str.replace('GB', '')) * 1024 * 1024 * 1024
elif 'MB' in size_str:
size = float(size_str.replace('MB', '')) * 1024 * 1024
else:
size = 0
print(f" Total size: {format_size(size)}")
total_size += size
except (json.JSONDecodeError, ValueError):
pass
# Containers
containers_output = run_command(['docker', 'ps', '-a', '-q'])
if containers_output:
container_count = len(containers_output.split('\n'))
stopped = run_command(['docker', 'ps', '-a', '-f', 'status=exited', '-q'])
stopped_count = len(stopped.split('\n')) if stopped else 0
print(f"\n📦 Containers: {container_count} total, {stopped_count} stopped")
# Volumes
volumes_output = run_command(['docker', 'volume', 'ls', '-q'])
if volumes_output:
volume_count = len(volumes_output.split('\n'))
print(f"\n📦 Volumes: {volume_count}")
# List volumes
for volume in volumes_output.split('\n')[:5]: # Show first 5
inspect = run_command(['docker', 'volume', 'inspect', volume])
print(f" - {volume}")
if volume_count > 5:
print(f" ... and {volume_count - 5} more")
# Build cache
buildx_output = run_command(['docker', 'buildx', 'du'])
if buildx_output and 'Total:' in buildx_output:
print(f"\n📦 Build Cache:")
for line in buildx_output.split('\n'):
if 'Total:' in line:
print(f" {line}")
print("\n💡 Cleanup: Remove specific images/volumes by ID/name (see SKILL.md)")
print(" ⚠️ NEVER use 'docker system prune' -- always specify exact objects")
return total_size
def check_homebrew():
"""Check Homebrew cache."""
print("\n🍺 Homebrew")
print("=" * 50)
if not run_command(['which', 'brew']):
print(" Homebrew not installed")
return 0
cache_path = run_command(['brew', '--cache'])
if cache_path and os.path.exists(cache_path):
size = get_dir_size(cache_path)
print(f" Cache location: {cache_path}")
print(f" Cache size: {format_size(size)}")
print(f"\n💡 Cleanup command: brew cleanup -s")
return size
return 0
def check_npm():
"""Check npm cache."""
print("\n📦 npm")
print("=" * 50)
if not run_command(['which', 'npm']):
print(" npm not installed")
return 0
cache_path = run_command(['npm', 'config', 'get', 'cache'])
if cache_path and cache_path != 'undefined' and os.path.exists(cache_path):
size = get_dir_size(cache_path)
print(f" Cache location: {cache_path}")
print(f" Cache size: {format_size(size)}")
print(f"\n💡 Cleanup command: npm cache clean --force")
return size
return 0
def check_pip():
"""Check pip cache."""
print("\n🐍 pip")
print("=" * 50)
# Try pip3 first
pip_cmd = 'pip3' if run_command(['which', 'pip3']) else 'pip'
if not run_command(['which', pip_cmd]):
print(" pip not installed")
return 0
cache_dir = run_command([pip_cmd, 'cache', 'dir'])
if cache_dir and os.path.exists(cache_dir):
size = get_dir_size(cache_dir)
print(f" Cache location: {cache_dir}")
print(f" Cache size: {format_size(size)}")
print(f"\n💡 Cleanup command: {pip_cmd} cache purge")
return size
return 0
def check_old_git_repos():
"""Find large .git directories in archived projects."""
print("\n📁 Old Git Repositories")
print("=" * 50)
home = Path.home()
common_project_dirs = [
home / 'Projects',
home / 'workspace',
home / 'dev',
home / 'src',
home / 'code'
]
git_repos = []
total_size = 0
for project_dir in common_project_dirs:
if not project_dir.exists():
continue
# Find .git directories
try:
result = subprocess.run(
['find', str(project_dir), '-name', '.git', '-type', 'd', '-maxdepth', 3],
capture_output=True,
text=True,
timeout=30
)
if result.returncode == 0:
for git_path in result.stdout.strip().split('\n'):
if git_path:
size = get_dir_size(git_path)
if size > 10 * 1024 * 1024: # > 10 MB
git_repos.append((git_path, size))
total_size += size
except subprocess.TimeoutExpired:
continue
if git_repos:
# Sort by size
git_repos.sort(key=lambda x: x[1], reverse=True)
print(f" Found {len(git_repos)} .git directories > 10 MB")
print(f"\n Top 10 largest:")
for path, size in git_repos[:10]:
# Get parent directory name (project name)
project_name = Path(path).parent.name
print(f" - {project_name:<30} {format_size(size)}")
print(f"\n Total: {format_size(total_size)}")
print(f"\n💡 If these are archived projects, consider:")
print(f" 1. Delete .git history: rm -rf <project>/.git")
print(f" 2. Or compress entire project: tar -czf archive.tar.gz <project>")
else:
print(" No large .git directories found in common project locations")
return total_size
def main():
print("🔍 Development Environment Analysis")
print("=" * 50)
total_savings = 0
# Check each component
docker_size = check_docker()
brew_size = check_homebrew()
npm_size = check_npm()
pip_size = check_pip()
git_size = check_old_git_repos()
# Summary
print("\n\n📊 Summary")
print("=" * 50)
if docker_size:
print(f"Docker: {format_size(docker_size)}")
total_savings += docker_size
if brew_size:
print(f"Homebrew cache: {format_size(brew_size)}")
total_savings += brew_size
if npm_size:
print(f"npm cache: {format_size(npm_size)}")
total_savings += npm_size
if pip_size:
print(f"pip cache: {format_size(pip_size)}")
total_savings += pip_size
if git_size:
print(f"Old .git repos: {format_size(git_size)}")
total_savings += git_size
print("-" * 50)
print(f"Potential savings: {format_size(total_savings)}")
print("\n💡 Next Steps:")
print(" 1. Review Docker volumes before cleanup (may contain data)")
print(" 2. Package manager caches are safe to delete")
print(" 3. For .git directories, ensure project is truly archived")
return 0
if __name__ == '__main__':
sys.exit(main())
#!/usr/bin/env python3
"""
Find large files on macOS and categorize them.
Usage:
python3 analyze_large_files.py [--threshold SIZE] [--path PATH] [--limit N]
Options:
--threshold Minimum file size in MB (default: 100)
--path Path to search (default: ~)
--limit Maximum number of results (default: 50)
"""
import os
import sys
import argparse
import subprocess
from pathlib import Path
def format_size(bytes_size):
"""Convert bytes to human-readable format."""
for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
if bytes_size < 1024.0:
return f"{bytes_size:.1f} {unit}"
bytes_size /= 1024.0
return f"{bytes_size:.1f} PB"
def categorize_file(path):
"""
Categorize file by type and suggest safety.
Returns:
(category, icon, safety_note)
"""
suffix = path.suffix.lower()
# Video files
video_exts = {'.mp4', '.mov', '.avi', '.mkv', '.m4v', '.flv', '.wmv'}
if suffix in video_exts:
return ('Video', '🎬', 'Review and archive to external storage')
# Archive files
archive_exts = {'.zip', '.tar', '.gz', '.bz2', '.7z', '.rar', '.dmg'}
if suffix in archive_exts:
return ('Archive', '📦', 'Extract if needed, then delete archive')
# Disk images
disk_exts = {'.iso', '.img', '.toast'}
if suffix in disk_exts:
return ('Disk Image', '💿', 'Delete after installation/use')
# Database files
db_exts = {'.db', '.sqlite', '.sqlite3', '.sql'}
if suffix in db_exts:
return ('Database', '🗄️', '⚠️ Verify not in use before deleting')
# Data files
data_exts = {'.csv', '.json', '.xml', '.parquet', '.arrow'}
if suffix in data_exts:
return ('Data File', '📊', 'Archive or compress if historical data')
# Log files
if suffix == '.log' or 'log' in path.name.lower():
return ('Log File', '📝', 'Safe to delete old logs')
# Build artifacts
build_patterns = ['.o', '.a', '.so', '.dylib', '.framework']
if suffix in build_patterns:
return ('Build Artifact', '🔨', 'Safe to delete, rebuild will regenerate')
# Virtual machine images
vm_exts = {'.vmdk', '.vdi', '.qcow2', '.vhd'}
if suffix in vm_exts:
return ('VM Image', '💻', '⚠️ Contains VM data, verify before deleting')
# Other
return ('Other', '📄', 'Review before deleting')
def find_large_files(search_path, threshold_bytes, limit):
"""
Find files larger than threshold using find command.
Args:
search_path: Path to search
threshold_bytes: Minimum size in bytes
limit: Maximum results
Returns:
List of (path, size_bytes) tuples
"""
# Convert bytes to 512-byte blocks (find -size uses 512-byte blocks)
threshold_blocks = threshold_bytes // 512
# Exclude common directories to avoid
exclude_dirs = [
'.Trash',
'Library/Caches',
'Library/Application Support/MobileSync', # iOS backups
'.git',
'node_modules',
'__pycache__'
]
# Build find command
cmd = ['find', search_path, '-type', 'f', '-size', f'+{threshold_blocks}']
# Add exclusions
for exclude in exclude_dirs:
cmd.extend(['-not', '-path', f'*/{exclude}/*'])
try:
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=120
)
if result.returncode != 0:
print(f"⚠️ Warning: find command had errors", file=sys.stderr)
files = []
for line in result.stdout.strip().split('\n'):
if not line:
continue
try:
path = Path(line)
if path.exists():
size = path.stat().st_size
files.append((path, size))
except (OSError, PermissionError):
continue
# Sort by size descending
files.sort(key=lambda x: x[1], reverse=True)
return files[:limit]
except subprocess.TimeoutExpired:
print("⚠️ Search timed out, showing partial results", file=sys.stderr)
return []
def main():
parser = argparse.ArgumentParser(
description='Find large files on macOS'
)
parser.add_argument(
'--threshold',
type=int,
default=100,
help='Minimum file size in MB (default: 100)'
)
parser.add_argument(
'--path',
default=os.path.expanduser('~'),
help='Path to search (default: ~)'
)
parser.add_argument(
'--limit',
type=int,
default=50,
help='Maximum number of results (default: 50)'
)
args = parser.parse_args()
threshold_bytes = args.threshold * 1024 * 1024
search_path = os.path.expanduser(args.path)
print(f"🔍 Searching for files larger than {args.threshold} MB")
print(f"📂 Search path: {search_path}")
print("=" * 80)
print("This may take a few minutes...\n")
large_files = find_large_files(search_path, threshold_bytes, args.limit)
if not large_files:
print("✅ No large files found above the threshold.")
return 0
print(f"\n📦 Found {len(large_files)} large files")
print("=" * 80)
print(f"{'#':<4} {'Size':<12} {'Type':<12} {'Location'}")
print("-" * 80)
# Group by category
by_category = {}
total_size = 0
for i, (path, size) in enumerate(large_files, 1):
category, icon, note = categorize_file(path)
# Shorten path for display
try:
rel_path = path.relative_to(Path.home())
display_path = f"~/{rel_path}"
except ValueError:
display_path = str(path)
# Truncate long paths
if len(display_path) > 45:
display_path = display_path[:42] + "..."
print(f"{i:<4} {format_size(size):<12} {icon} {category:<10} {display_path}")
# Track by category
if category not in by_category:
by_category[category] = {'count': 0, 'size': 0, 'note': note}
by_category[category]['count'] += 1
by_category[category]['size'] += size
total_size += size
print("-" * 80)
print(f"{'Total':<4} {format_size(total_size):<12}")
# Category summary
print("\n\n📊 Breakdown by Category")
print("=" * 80)
for category, data in sorted(
by_category.items(),
key=lambda x: x[1]['size'],
reverse=True
):
print(f"\n{category}")
print(f" Files: {data['count']}")
print(f" Total: {format_size(data['size'])}")
print(f" 💡 {data['note']}")
print("\n\n💡 Next Steps:")
print(" 1. Review the list and identify files you no longer need")
print(" 2. For videos/archives: consider moving to external storage")
print(" 3. For databases/VMs: verify they're not in use")
print(" 4. Use safe_delete.py for interactive cleanup")
return 0
if __name__ == '__main__':
sys.exit(main())
#!/usr/bin/env python3
"""
Generate before/after cleanup reports.
Usage:
# Capture before snapshot
python3 cleanup_report.py --snapshot before
# Capture after snapshot and generate report
python3 cleanup_report.py --snapshot after --compare
"""
import os
import sys
import json
import argparse
import subprocess
from datetime import datetime
from pathlib import Path
def format_size(bytes_size):
"""Convert bytes to human-readable format."""
for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
if bytes_size < 1024.0:
return f"{bytes_size:.1f} {unit}"
bytes_size /= 1024.0
return f"{bytes_size:.1f} PB"
def get_disk_usage():
"""
Get current disk usage.
Returns:
dict with total, used, available, percent
"""
try:
result = subprocess.run(
['df', '-k', '/'],
capture_output=True,
text=True
)
if result.returncode == 0:
lines = result.stdout.strip().split('\n')
if len(lines) >= 2:
# Parse df output
parts = lines[1].split()
total_kb = int(parts[1])
used_kb = int(parts[2])
available_kb = int(parts[3])
percent = int(parts[4].rstrip('%'))
return {
'total': total_kb * 1024,
'used': used_kb * 1024,
'available': available_kb * 1024,
'percent': percent,
'timestamp': datetime.now().isoformat()
}
except (OSError, subprocess.SubprocessError, ValueError, IndexError):
pass
return None
def save_snapshot(name):
"""Save disk usage snapshot to file."""
snapshot_dir = Path.home() / '.macos-cleaner'
snapshot_dir.mkdir(exist_ok=True)
snapshot_file = snapshot_dir / f'{name}.json'
usage = get_disk_usage()
if usage:
with snapshot_file.open('w') as f:
json.dump(usage, f, indent=2)
print(f"✅ Snapshot saved: {snapshot_file}")
return True
else:
print("❌ Failed to get disk usage")
return False
def load_snapshot(name):
"""Load disk usage snapshot from file."""
snapshot_dir = Path.home() / '.macos-cleaner'
snapshot_file = snapshot_dir / f'{name}.json'
if not snapshot_file.exists():
print(f"❌ Snapshot not found: {snapshot_file}")
return None
with snapshot_file.open('r') as f:
return json.load(f)
def generate_report(before, after):
"""Generate comparison report."""
print("\n" + "=" * 60)
print("📊 Cleanup Report")
print("=" * 60)
# Time
before_time = datetime.fromisoformat(before['timestamp'])
after_time = datetime.fromisoformat(after['timestamp'])
duration = after_time - before_time
print(f"\nCleanup Duration: {duration}")
print(f"Before: {before_time.strftime('%Y-%m-%d %H:%M:%S')}")
print(f"After: {after_time.strftime('%Y-%m-%d %H:%M:%S')}")
# Disk usage comparison
print("\n" + "-" * 60)
print("Disk Usage")
print("-" * 60)
before_used = before['used']
after_used = after['used']
recovered = before_used - after_used
print(f"Before: {format_size(before_used):>12} ({before['percent']}%)")
print(f"After: {format_size(after_used):>12} ({after['percent']}%)")
print("-" * 60)
if recovered > 0:
print(f"✅ Recovered: {format_size(recovered):>12}")
percent_recovered = (recovered / before_used) * 100
print(f" ({percent_recovered:.1f}% of used space)")
elif recovered < 0:
print(f"⚠️ Space increased: {format_size(abs(recovered)):>12}")
print(" (This may be due to system activity during cleanup)")
else:
print("No change in disk usage")
# Available space
print("\n" + "-" * 60)
print("Available Space")
print("-" * 60)
before_avail = before['available']
after_avail = after['available']
gained = after_avail - before_avail
print(f"Before: {format_size(before_avail):>12}")
print(f"After: {format_size(after_avail):>12}")
print("-" * 60)
if gained > 0:
print(f"✅ Gained: {format_size(gained):>12}")
elif gained < 0:
print(f"⚠️ Lost: {format_size(abs(gained)):>12}")
else:
print("No change")
# Recommendations
print("\n" + "=" * 60)
if after['percent'] > 90:
print("⚠️ Warning: Disk is still >90% full")
print("\n💡 Recommendations:")
print(" - Consider moving large files to external storage")
print(" - Review and delete old projects")
print(" - Check for large application data")
elif after['percent'] > 80:
print("⚠️ Disk usage is still high (>80%)")
print("\n💡 Recommendations:")
print(" - Run cleanup again in 1-2 weeks")
print(" - Monitor large file creation")
else:
print("✅ Disk usage is healthy!")
print("\n💡 Maintenance Tips:")
print(" - Run cleanup monthly")
print(" - Empty Trash regularly")
print(" - Clear browser caches weekly")
print("=" * 60)
def main():
parser = argparse.ArgumentParser(
description='Generate cleanup reports'
)
parser.add_argument(
'--snapshot',
choices=['before', 'after'],
required=True,
help='Snapshot type (before or after cleanup)'
)
parser.add_argument(
'--compare',
action='store_true',
help='Compare with before snapshot (use with --snapshot after)'
)
args = parser.parse_args()
if args.snapshot == 'before':
# Save before snapshot
print("📸 Capturing disk usage before cleanup...")
if save_snapshot('before'):
usage = get_disk_usage()
print(f"\nCurrent Usage: {format_size(usage['used'])} ({usage['percent']}%)")
print(f"Available: {format_size(usage['available'])}")
print("\n💡 Run cleanup operations, then:")
print(" python3 cleanup_report.py --snapshot after --compare")
return 0
elif args.snapshot == 'after':
# Save after snapshot
print("📸 Capturing disk usage after cleanup...")
if not save_snapshot('after'):
return 1
if args.compare:
# Load before snapshot and compare
before = load_snapshot('before')
after = load_snapshot('after')
if before and after:
generate_report(before, after)
else:
print("❌ Cannot compare: missing snapshots")
return 1
else:
usage = get_disk_usage()
print(f"\nCurrent Usage: {format_size(usage['used'])} ({usage['percent']}%)")
print(f"Available: {format_size(usage['available'])}")
return 0
if __name__ == '__main__':
sys.exit(main())
#!/usr/bin/env python3
"""
Find orphaned application support files and preferences.
This script identifies directories in ~/Library that may belong to
uninstalled applications.
Usage:
python3 find_app_remnants.py [--min-size SIZE]
Options:
--min-size Minimum size in MB to report (default: 10)
"""
import os
import sys
import subprocess
import argparse
import plistlib
from pathlib import Path
def format_size(bytes_size):
"""Convert bytes to human-readable format."""
for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
if bytes_size < 1024.0:
return f"{bytes_size:.1f} {unit}"
bytes_size /= 1024.0
return f"{bytes_size:.1f} PB"
def get_dir_size(path):
"""Get directory size using du command."""
try:
result = subprocess.run(
['du', '-sk', path],
capture_output=True,
text=True,
timeout=30
)
if result.returncode == 0:
size_kb = int(result.stdout.split()[0])
return size_kb * 1024
return 0
except (subprocess.TimeoutExpired, ValueError, IndexError):
return 0
def get_bundle_identifier(app_path):
"""Read CFBundleIdentifier from an app bundle when available."""
info_plist = app_path / 'Contents' / 'Info.plist'
try:
with info_plist.open('rb') as f:
info = plistlib.load(f)
except Exception:
return None
bundle_id = info.get('CFBundleIdentifier')
if isinstance(bundle_id, str) and bundle_id.strip():
return bundle_id.strip()
return None
def _empty_installed_apps():
return {
'names': set(),
'bundle_ids': set(),
}
def _coerce_installed_apps(installed_apps):
if isinstance(installed_apps, dict):
return {
'names': set(installed_apps.get('names', set())),
'bundle_ids': set(installed_apps.get('bundle_ids', set())),
}
return {
'names': set(installed_apps),
'bundle_ids': set(),
}
def _add_app_bundle(installed_apps, app_path):
installed_apps['names'].add(app_path.stem)
bundle_id = get_bundle_identifier(app_path)
if bundle_id:
installed_apps['bundle_ids'].add(bundle_id)
def _matches_bundle_identifier(dir_name, bundle_id):
dir_lower = dir_name.lower()
bundle_lower = bundle_id.lower()
if dir_lower == bundle_lower or dir_lower.startswith(bundle_lower + '.'):
return True
return normalize_name(dir_name) == normalize_name(bundle_id)
def get_installed_apps():
"""Get installed application names and bundle identifiers."""
apps = _empty_installed_apps()
# System applications
system_app_dir = Path('/Applications')
if system_app_dir.exists():
for app in system_app_dir.iterdir():
if app.suffix == '.app':
_add_app_bundle(apps, app)
# User applications
user_app_dir = Path.home() / 'Applications'
if user_app_dir.exists():
for app in user_app_dir.iterdir():
if app.suffix == '.app':
_add_app_bundle(apps, app)
return apps
def normalize_name(name):
"""
Normalize app name for matching.
Examples:
'Google Chrome' -> 'googlechrome'
'com.apple.Safari' -> 'safari'
"""
# Remove common prefixes
for prefix in ['com.', 'org.', 'net.', 'io.']:
if name.startswith(prefix):
name = name[len(prefix):]
# Remove non-alphanumeric
name = ''.join(c for c in name if c.isalnum())
return name.lower()
def is_likely_orphaned(dir_name, installed_apps):
"""
Check if directory is likely orphaned.
Returns:
(is_orphaned, confidence, reason)
confidence: 'high' | 'medium' | 'low'
"""
installed = _coerce_installed_apps(installed_apps)
norm_dir = normalize_name(dir_name)
# Bundle identifiers are common in Containers and Saved Application State.
for bundle_id in installed['bundle_ids']:
if _matches_bundle_identifier(dir_name, bundle_id):
return (
False,
None,
f"Matches installed app bundle identifier: {bundle_id}"
)
# Check display-name matches.
for app in installed['names']:
norm_app = normalize_name(app)
if norm_app and (norm_app in norm_dir or norm_dir in norm_app):
return (False, None, f"Matches installed app: {app}")
# System/common directories to always keep
system_dirs = {
'apple', 'safari', 'finder', 'mail', 'messages', 'notes',
'photos', 'music', 'calendar', 'contacts', 'reminders',
'preferences', 'cookies', 'webkit', 'coredata',
'cloudkit', 'icloud', 'appstore', 'systemmigration'
}
if any(sys_dir in norm_dir for sys_dir in system_dirs):
return (False, None, "System/built-in application")
# If we get here, likely orphaned
return (True, 'medium', "No matching application found")
def analyze_library_dir(library_path, min_size_bytes, installed_apps):
"""
Analyze a Library subdirectory for orphaned data.
Args:
library_path: Path to scan (e.g., ~/Library/Application Support)
min_size_bytes: Minimum size to report
installed_apps: Dict with installed app names and bundle identifiers
Returns:
List of (name, path, size, confidence, reason) tuples
"""
if not os.path.exists(library_path):
return []
results = []
try:
for entry in os.scandir(library_path):
if entry.is_dir():
size = get_dir_size(entry.path)
if size >= min_size_bytes:
is_orphaned, confidence, reason = is_likely_orphaned(
entry.name,
installed_apps
)
if is_orphaned:
results.append((
entry.name,
entry.path,
size,
confidence,
reason
))
except PermissionError:
print(f"⚠️ Permission denied: {library_path}", file=sys.stderr)
return []
# Sort by size descending
results.sort(key=lambda x: x[2], reverse=True)
return results
def main():
parser = argparse.ArgumentParser(
description='Find orphaned application data'
)
parser.add_argument(
'--min-size',
type=int,
default=10,
help='Minimum size in MB to report (default: 10)'
)
args = parser.parse_args()
min_size_bytes = args.min_size * 1024 * 1024
print("🔍 Searching for Orphaned Application Data")
print("=" * 70)
# Get installed apps
print("Scanning installed applications...")
installed_apps = get_installed_apps()
print(
f"Found {len(installed_apps['names'])} installed applications "
f"and {len(installed_apps['bundle_ids'])} bundle identifiers\n"
)
# Directories to check
library_dirs = {
'Application Support': Path.home() / 'Library' / 'Application Support',
'Containers': Path.home() / 'Library' / 'Containers',
'Preferences': Path.home() / 'Library' / 'Preferences',
'Saved Application State': Path.home() / 'Library' / 'Saved Application State'
}
all_orphans = []
total_size = 0
for category, path in library_dirs.items():
print(f"\n📂 {category}")
print("-" * 70)
orphans = analyze_library_dir(path, min_size_bytes, installed_apps)
if orphans:
print(f"{'Name':<40} {'Size':<12} {'Confidence'}")
print("-" * 70)
for name, full_path, size, confidence, reason in orphans:
conf_icon = {'high': '🔴', 'medium': '🟡', 'low': '🟢'}[confidence]
# Truncate long names
display_name = name if len(name) <= 37 else name[:34] + "..."
print(f"{display_name:<40} {format_size(size):<12} {conf_icon} {confidence}")
all_orphans.append((category, name, full_path, size, confidence, reason))
total_size += size
else:
print("No orphaned data found above minimum size")
# Summary
print("\n\n📊 Summary")
print("=" * 70)
print(f"Total orphaned data found: {len(all_orphans)} items")
print(f"Total size: {format_size(total_size)}")
if all_orphans:
print("\n\n🗑️ Recommended Deletions (Medium/High Confidence)")
print("=" * 70)
for category, name, path, size, confidence, reason in all_orphans:
if confidence in ['medium', 'high']:
print(f"\n{name}")
print(f" Location: {path}")
print(f" Size: {format_size(size)}")
print(f" Reason: {reason}")
print(f" ⚠️ Verify this app is truly uninstalled before deleting")
print("\n\n💡 Next Steps:")
print(" 1. Double-check each item in /Applications and ~/Applications")
print(" 2. Search Spotlight for the application name")
print(" 3. If truly uninstalled, safe to delete with:")
print(" rm -rf '<path>'")
print(" 4. Or use safe_delete.py for interactive cleanup")
return 0
if __name__ == '__main__':
sys.exit(main())
#!/usr/bin/env python3
"""
Interactive safe file/directory deletion with confirmation.
Usage:
python3 safe_delete.py <path1> [path2] [path3] ...
python3 safe_delete.py --batch <file_with_paths>
Options:
--batch FILE Read paths from a file (one per line)
"""
import os
import sys
import shutil
import argparse
import subprocess
from pathlib import Path
def _canonical_path(path):
"""Return a canonical path for safety comparisons."""
return Path(os.path.realpath(os.path.expanduser(str(path))))
ROOT_PATH = _canonical_path('/')
HIGH_RISK_PATHS = frozenset(
_canonical_path(path)
for path in [
'/System',
'/Library/Apple',
'/usr',
'/bin',
'/sbin',
'/etc',
'/private/var/db',
'~/.ssh',
'~/.aws',
'~/.gnupg',
'~/Library/Keychains',
]
)
def _is_same_or_child(path, parent):
try:
path.relative_to(parent)
return True
except ValueError:
return False
def get_high_risk_match(path):
"""Return the denied ancestor path when path is too dangerous to delete."""
canonical = _canonical_path(path)
if canonical == ROOT_PATH:
return ROOT_PATH
for denied_path in HIGH_RISK_PATHS:
if _is_same_or_child(canonical, denied_path):
return denied_path
return None
def is_high_risk_path(path):
"""Check whether a path is blocked by the forced-delete denylist."""
return get_high_risk_match(path) is not None
def format_blocked_message(path, denied_path):
return (
"BLOCKED: high-risk path refused by safety denylist "
f"({path} matches or is inside {denied_path})"
)
def format_size(bytes_size):
"""Convert bytes to human-readable format."""
for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
if bytes_size < 1024.0:
return f"{bytes_size:.1f} {unit}"
bytes_size /= 1024.0
return f"{bytes_size:.1f} PB"
def get_size(path):
"""Get size of file or directory."""
path_obj = Path(path)
if not path_obj.exists():
return 0
if path_obj.is_file():
return path_obj.stat().st_size
elif path_obj.is_dir():
try:
result = subprocess.run(
['du', '-sk', path],
capture_output=True,
text=True,
timeout=30
)
if result.returncode == 0:
size_kb = int(result.stdout.split()[0])
return size_kb * 1024
except (subprocess.TimeoutExpired, ValueError, IndexError):
pass
return 0
def get_description(path):
"""Get human-readable description of path."""
path_obj = Path(path)
if not path_obj.exists():
return "Path does not exist"
if path_obj.is_file():
suffix = path_obj.suffix or "file"
return f"File ({suffix})"
elif path_obj.is_dir():
try:
# Count items
items = list(path_obj.iterdir())
return f"Directory ({len(items)} items)"
except PermissionError:
return "Directory (permission denied to list)"
return "Unknown"
def confirm_delete(path, size, description):
"""
Ask user to confirm deletion.
Args:
path: File/directory path
size: Size in bytes
description: What this file/directory is
Returns:
True if user confirms, False otherwise
"""
denied_path = get_high_risk_match(path)
if denied_path:
print(f"\n⛔ {format_blocked_message(path, denied_path)}")
print(" Refusing to ask for confirmation for this target.")
return False
print(f"\n🗑️ Confirm Deletion")
print("━" * 50)
print(f"Path: {path}")
print(f"Size: {format_size(size)}")
print(f"Description: {description}")
# Additional safety check for important paths
path_str = str(path).lower()
danger_patterns = [
'documents', 'desktop', 'pictures', 'movies',
'downloads', 'music', '.ssh', 'credentials'
]
if any(pattern in path_str for pattern in danger_patterns):
print("\n⚠️ WARNING: This path may contain important personal data!")
print(" Consider backing up before deletion.")
response = input("\nDelete this item? [y/N]: ").strip().lower()
return response == 'y'
def batch_confirm(items):
"""
Show all items, ask for batch confirmation.
Args:
items: List of (path, size, description) tuples
Returns:
List of items user approved
"""
print("\n📋 Items to Delete:")
print("━" * 70)
print(f"{'#':<4} {'Size':<12} {'Path'}")
print("-" * 70)
for i, (path, size, description) in enumerate(items, 1):
# Truncate long paths
display_path = str(path)
if len(display_path) > 48:
display_path = display_path[:45] + "..."
print(f"{i:<4} {format_size(size):<12} {display_path}")
total_size = sum(item[1] for item in items)
print("-" * 70)
print(f"{'Total':<4} {format_size(total_size):<12}")
print("\nOptions:")
print(" 'all' - Delete all items")
print(" '1,3,5' - Delete specific items by number")
print(" '1-5' - Delete range of items")
print(" 'none' - Cancel (default)")
response = input("\nYour choice: ").strip().lower()
if response == '' or response == 'none':
return []
elif response == 'all':
return items
else:
selected = []
# Parse response
parts = response.replace(' ', '').split(',')
for part in parts:
try:
if '-' in part:
# Range: 1-5
start, end = part.split('-')
start_idx = int(start) - 1
end_idx = int(end) - 1
for i in range(start_idx, end_idx + 1):
if 0 <= i < len(items):
selected.append(items[i])
else:
# Single number
idx = int(part) - 1
if 0 <= idx < len(items):
selected.append(items[idx])
except ValueError:
print(f"⚠️ Ignoring invalid selection: {part}")
continue
return selected
def delete_path(path):
"""
Delete a file or directory.
Returns:
(success, message)
"""
try:
denied_path = get_high_risk_match(path)
if denied_path:
return (False, format_blocked_message(path, denied_path))
path_obj = Path(path)
if not path_obj.exists():
return (False, "Path does not exist")
if path_obj.is_file():
path_obj.unlink()
elif path_obj.is_dir():
shutil.rmtree(path)
else:
return (False, "Unknown path type")
return (True, "Deleted successfully")
except PermissionError:
return (False, "Permission denied")
except Exception as e:
return (False, f"Error: {str(e)}")
def main():
parser = argparse.ArgumentParser(
description='Interactive safe deletion'
)
parser.add_argument(
'paths',
nargs='*',
help='Paths to delete'
)
parser.add_argument(
'--batch',
metavar='FILE',
help='Read paths from file (one per line)'
)
args = parser.parse_args()
# Collect paths
paths = []
if args.batch:
# Read from file
batch_file = Path(args.batch)
if not batch_file.exists():
print(f"❌ Batch file not found: {args.batch}")
return 1
with batch_file.open('r') as f:
for line in f:
line = line.strip()
if line and not line.startswith('#'):
paths.append(line)
else:
paths = args.paths
if not paths:
parser.print_help()
return 1
# Block high-risk paths before sizing or confirmation.
allowed_paths = []
for path in paths:
denied_path = get_high_risk_match(path)
if denied_path:
print(f"⛔ {format_blocked_message(path, denied_path)}")
else:
allowed_paths.append(path)
paths = allowed_paths
# Prepare items
items = []
for path in paths:
size = get_size(path)
description = get_description(path)
items.append((path, size, description))
# Remove non-existent paths
items = [(p, s, d) for p, s, d in items if Path(p).exists()]
if not items:
print("❌ No valid paths to delete")
return 1
# Interactive mode
if len(items) == 1:
# Single item - simple confirmation
path, size, description = items[0]
if not confirm_delete(path, size, description):
print("\n✅ Deletion cancelled")
return 0
success, message = delete_path(path)
if success:
print(f"\n✅ {message}")
print(f" Freed: {format_size(size)}")
return 0
else:
print(f"\n❌ {message}")
return 1
else:
# Multiple items - batch confirmation
selected = batch_confirm(items)
if not selected:
print("\n✅ Deletion cancelled")
return 0
# Delete selected items
print(f"\n🗑️ Deleting {len(selected)} items...")
print("━" * 50)
success_count = 0
total_freed = 0
for path, size, description in selected:
success, message = delete_path(path)
status_icon = "✅" if success else "❌"
print(f"{status_icon} {path}: {message}")
if success:
success_count += 1
total_freed += size
print("━" * 50)
print(f"\n📊 Results:")
print(f" Successfully deleted: {success_count}/{len(selected)}")
print(f" Total freed: {format_size(total_freed)}")
return 0 if success_count == len(selected) else 1
if __name__ == '__main__':
sys.exit(main())
Related skills
How it compares
Pick macos-cleaner for guided, safety-rated developer Mac cache cleanup; pick dedicated uninstaller tools when removing entire applications rather than caches.
FAQ
Is it safe to delete ~/Library/Caches with macos-cleaner?
macos-cleaner marks ~/Library/Caches as safe to delete, covering browser, app, thumbnail, and font caches. Apps may be slower on first launch and websites slower on first visit while caches rebuild.
How much disk space can macos-cleaner reclaim?
macos-cleaner targets reclaiming tens of gigabytes on a development Mac by removing caches, logs, and temporary files, with per-target safety notes so developers avoid breaking installed applications.