Use when uninstalling a macOS app or cleaning leftover app files and startup hooks after uninstall.
Use this skill to check whether a macOS app was fully removed, delete confirmed residue, and make sure startup hooks are gone.
Work in three passes: inventory first, delete second, verify last.
Rules:
sudo for the initial scan unless a specific read operation is blocked.find, launchctl print-disabled system, osascript, and receipt scans first.sfltool unless the user explicitly asks for it.root_required, check whether its delete parent is actually writable by the current user. /Applications and /Users/Shared are not automatically root-only on every machine.python3 scripts/scan_residue.py ..., use cleanup_plan.user_delete_paths for non-root deletes.cleanup_plan.root_batch_remove_paths into one privileged action.cleanup_plan.root_batch_remove_paths is empty, do not prompt for administrator authentication.python3 scripts/root_cleanup_prompt.py ... invocation. Let that single prompt handle unloads, disables, deletes, and receipt forgets.scripts/root_cleanup_prompt.py requires a local macOS GUI session because it relies on an AppleScript administrator dialog. Do not recommend it for SSH-only, CI, or other headless shells.scripts/root_cleanup_batch.py from an explicit root context instead.python3 scripts/root_cleanup_batch.py --dry-run ... before prompting.sudo for verification, reporting, or receipt listing.~/Library/Containers, ~/Library/Group Containers, or ~/Library/Mobile Documents, classify it before deleting.Run the bundled scanner with product and vendor names:
python3 scripts/scan_residue.py "Battle.net" Blizzard
python3 scripts/scan_residue.py Riot "League of Legends" LeagueClient RiotClient
python3 scripts/scan_residue.py Grammarly
The scanner inventories:
/Applications and ~/Applications~/Library, /Library, and /Users/Sharedlaunchctl labels, running processes, and login itemsroot_required, container_managed, may_need_full_disk_access, and icloud_syncedcleanup_plan that separates user deletes from the privileged batchStart with a read-only pass.
Useful commands:
python3 scripts/scan_residue.py "App Name" Vendor
du -sh "/Users/Shared/Vendor" "$HOME/Library/Application Support/App Name" 2>/dev/null
pkgutil --pkgs | rg -i 'vendor|app'
Login items and launchctl:
osascript -e 'tell application "System Events" to get the properties of every login item'
launchctl list | rg -i 'vendor|app'
If cleanup_plan.root_batch_remove_paths is empty, do not escalate.
If the scanner flags a path as container_managed, may_need_full_disk_access, or icloud_synced, treat that as preflight feedback and adjust the plan before deleting anything.
Before deleting, classify findings into these buckets:
/Applications/*.app link~/Library/Library/Users/Shared~/Library/Mobile Documentsrm even for root without the right privacy contextShared vendor folders may belong to multiple products.
Protected-path notes:
~/Library/Containers/* and some ~/Library/Group Containers/* items may be container-managed~/Library/Mobile Documents/* may reappear from iCloudUse one combined privileged plan, not several small privileged commands.
Example flow:
python3 scripts/scan_residue.py "App Name" Vendor
uid=$(id -u)
launchctl bootout gui/$uid "$HOME/Library/LaunchAgents/com.vendor.agent.plist" 2>/dev/null || true
launchctl disable gui/$uid/com.vendor.agent 2>/dev/null || true
osascript -e 'tell application "System Events" to delete login item "App Name"' 2>/dev/null || true
rm -rf "$HOME/Library/Application Support/App Name"
rm -f "$HOME/Library/Preferences/com.vendor.app.plist"
python3 scripts/root_cleanup_batch.py --dry-run \
--bootout-system /Library/LaunchDaemons/com.vendor.helper.plist \
--disable-system com.vendor.helper \
--remove "/Applications/App Name.app" \
--remove /Library/LaunchDaemons/com.vendor.helper.plist \
--remove /Library/PrivilegedHelperTools/com.vendor.helper \
--remove "/Library/Application Support/Vendor" \
--remove "/Users/Shared/App Name" \
--forget-pkg com.vendor.package
python3 scripts/root_cleanup_prompt.py \
--bootout-system /Library/LaunchDaemons/com.vendor.helper.plist \
--disable-system com.vendor.helper \
--remove "/Applications/App Name.app" \
--remove /Library/LaunchDaemons/com.vendor.helper.plist \
--remove /Library/PrivilegedHelperTools/com.vendor.helper \
--remove "/Library/Application Support/Vendor" \
--remove "/Users/Shared/App Name" \
--forget-pkg com.vendor.package
python3 scripts/scan_residue.py "App Name" Vendor
Do not split the root-only part above into separate privileged prompts unless the helper script truly cannot express the required operation.
Do not delete active launch files before unloading them.
User domain:
uid=$(id -u)
launchctl bootout gui/$uid /path/to/agent.plist 2>/dev/null || true
launchctl disable gui/$uid/com.vendor.agent 2>/dev/null || true
System domain:
# Add these flags to the single root helper invocation from section 2a:
--bootout-system /Library/LaunchDaemons/com.vendor.helper.plist
--disable-system com.vendor.helper
--bootout-system is only for /Library/LaunchDaemons. Do not pass /Library/LaunchAgents/* to it.
For login items:
osascript -e 'tell application "System Events" to delete login item "App Name"'
Keep multiple system items in the same helper invocation.
Delete only after the scope is confirmed.
Typical user paths:
rm -rf "$HOME/Library/Application Support/App Name"
rm -rf "$HOME/Library/Caches/com.vendor.app"
rm -rf "$HOME/Library/Logs/App Name"
rm -f "$HOME/Library/Preferences/com.vendor.app.plist"
rm -rf "$HOME/Library/Saved Application State/com.vendor.app.savedState"
Typical system paths:
# Add these flags to the single root helper invocation from section 2a:
--remove /Library/LaunchAgents/com.vendor.agent.plist
--remove /Library/LaunchDaemons/com.vendor.helper.plist
--remove /Library/PrivilegedHelperTools/com.vendor.helper
--remove "/Library/Application Support/Vendor"
Typical shared paths:
# Add these flags to the single root helper invocation from section 2a:
--remove "/Users/Shared/App Name"
--remove "/Users/Shared/Vendor"
If /Applications/App Name.app is a symlink whose target is gone, remove the link:
# Add this flag to the single root helper invocation from section 2a:
--remove "/Applications/App Name.app"
If package receipts remain and the user wants a deeper cleanup:
pkgutil --pkgs | rg -i 'vendor|app'
# Add this flag to the single root helper invocation from section 2a:
--forget-pkg com.vendor.package
Do not blindly retry protected deletes. Preferred handling:
container_managed: report that Full Disk Access or a manual Finder cleanup may be requiredicloud_synced: delete locally once, then remove the same container from Finder iCloud Drive or iCloud.com if it reappearsroot_required: batch all matching paths into one helper invocation, not several separate privileged promptsRepeat the same scan after deletion.
Minimum verification:
python3 scripts/scan_residue.py "App Name" Vendor
launchctl list | rg -i 'vendor|app'
ps aux | rg -i 'vendor|app'
Interpret results carefully:
launchctl labels is the strongest signal that cleanup succeededApp-specific iCloud containers under ~/Library/Mobile Documents may require administrator authentication to remove. Some empty containers can reappear if iCloud sync restores them.
If a container keeps returning:
If a container delete fails with Operation not permitted, do not keep looping on the same command. Report that the remaining item is protected by macOS privacy or container controls and switch to a manual or Full Disk Access path.
Do not assume every vendor folder is safe to wipe. Audit contents first when the vendor may own multiple apps or games.
When you finish, summarize:
Keep the summary concrete. Prefer exact paths over generic statements.