Macos clean uninstall
Skill VdustR/vp-claude-code-marketplace/plugins/vp-macos-clean-uninstall/skills/macos-clean-uninstall
Deprecated archive. Use VdustR/skills with npx skills.
npx -y skills add VdustR/vp-claude-code-marketplace --skill macos-clean-uninstallAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Cleanly uninstall applications on macOS with thorough research and cleanup. Use when the user asks to "uninstall", "remove", "delete", or "clean up" an application, program, CLI tool, or package on macOS. Also trigger when the user wants to check what residual data an app has left behind, asks to "check leftover files", or mentions cleaning up after an app removal. Boundary: macOS only. Not for Linux/Windows, removing SIP-protected system apps, or clearing browser data.
SKILL.md
16.6 KB, as published. Nobody here has run it
Clean Uninstall (macOS)
Research-driven workflow for completely removing applications and all associated data from macOS.
Quick Start
Uninstall Docker from my Mac
Remove Slack and all its data
What files did Zoom leave behind?
Clean uninstall 1Password
When to Use
- User asks to uninstall, remove, or delete a macOS application
- User wants to check for residual data left by an app
- User wants to free disk space by cleaning up after a removed app
When NOT to use:
- Linux or Windows uninstalls — this skill is macOS-only
- Removing system-bundled apps (Safari, Mail, Finder) — these are SIP-protected
- Clearing browser data or cookies only — not an app uninstall
- Removing macOS system updates
Workflow
Execute these phases in order. Never skip the research and review phases.
Prerequisite: If the app name cannot be determined unambiguously, ask the user to clarify. Never substitute an empty or whitespace-only string into any command.
Define these variables once and use throughout:
APP_NAME— CLI/short name (e.g.,docker,slack)APP_DISPLAY— display name for.appbundle (e.g.,Docker,Slack)BUNDLE_ID— bundle identifier (e.g.,com.docker.docker)
Phase 1: Identify Installation Method
Determine how the app was installed — this dictates the correct removal procedure.
Run detection as a single consolidated script — not as parallel sub-calls. One shell call with labeled sections prevents (a) short/empty outputs being misattributed across sections, and (b) a single failure (e.g., zsh NOMATCH glob) cancelling the rest of the batch.
# Phase 1 consolidated detection — run as a single Bash tool call
set +e # never abort; every section must print
A="${APP_NAME:?APP_NAME required}"
D="${APP_DISPLAY:-$A}"
echo "=== Homebrew formula ==="
brew list --formula 2>/dev/null | grep -i "$A" || echo "(none)"
echo "=== Homebrew cask ==="
brew list --cask 2>/dev/null | grep -i "$A" || echo "(none)"
echo "=== Caskroom (direct, fallback) ==="
found=$(find /opt/homebrew/Caskroom /usr/local/Caskroom -maxdepth 1 -iname "*${A}*" 2>/dev/null)
[ -n "$found" ] && echo "$found" || echo "(none)"
echo "=== /Applications bundle ==="
[ -d "/Applications/${D}.app" ] && echo "/Applications/${D}.app" || echo "(none at /Applications/${D}.app)"
echo "=== ~/Applications bundle (fallback) ==="
found=$(find ~/Applications -maxdepth 2 -iname "*${A}*.app" 2>/dev/null)
[ -n "$found" ] && echo "$found" || echo "(none)"
echo "=== Bundle ID (mdls, with defaults fallback) ==="
# Spotlight may be disabled or the app un-indexed, making mdls return empty
# or "(null)". Fall back to reading Info.plist directly so downstream phases
# never see an empty BUNDLE_ID (which would cause `find -iname "*${BUNDLE_ID}*"`
# to match every path).
emit_bid() {
app="$1"
raw=$(mdls -raw -name kMDItemCFBundleIdentifier "$app" 2>/dev/null)
if [ -z "$raw" ] || [ "$raw" = "(null)" ]; then
raw=$(defaults read "${app%/}/Contents/Info" CFBundleIdentifier 2>/dev/null)
fi
if [ -n "$raw" ]; then
echo "$app: $raw"
else
echo "$app: (bundle ID unavailable — Spotlight off or Info.plist unreadable; do NOT proceed with empty BUNDLE_ID to Phase 3)"
fi
}
bid_found=0
if [ -d "/Applications/${D}.app" ]; then
emit_bid "/Applications/${D}.app"
bid_found=1
fi
while IFS= read -r app; do
[ -z "$app" ] && continue
emit_bid "$app"
bid_found=1
done < <(find ~/Applications -maxdepth 2 -iname "*${A}*.app" 2>/dev/null)
[ $bid_found -eq 0 ] && echo "(no .app found)"
echo "=== PKG receipts ==="
pkgutil --pkgs 2>/dev/null | grep -i "$A" || echo "(none)"
echo "=== Mac App Store receipt ==="
[ -e "/Applications/${D}.app/Contents/_MASReceipt" ] && echo "MAS receipt present" || echo "(not MAS)"
echo "=== Bundled uninstaller (inside .app) ==="
# Scan Contents of every candidate .app (both /Applications and ~/Applications)
contents_list=""
[ -d "/Applications/${D}.app/Contents" ] && contents_list="/Applications/${D}.app/Contents"
while IFS= read -r app; do
[ -z "$app" ] && continue
[ -d "$app/Contents" ] && contents_list="${contents_list:+$contents_list
}$app/Contents"
done < <(find ~/Applications -maxdepth 2 -iname "*${A}*.app" 2>/dev/null)
if [ -z "$contents_list" ]; then
echo "(no .app)"
else
hits=""
while IFS= read -r c; do
[ -z "$c" ] && continue
more=$(find "$c" -maxdepth 3 \( -iname "*uninstall*" -o -iname "*remove*" \) 2>/dev/null | head -20)
[ -n "$more" ] && hits="${hits:+$hits
}$more"
done <<< "$contents_list"
[ -n "$hits" ] && echo "$hits" || echo "(none)"
fi
echo "=== Sibling uninstaller apps (/Applications and ~/Applications) ==="
# Symmetrical with the bundled-uninstaller scan: check both system and user app dirs.
# find tolerates a missing ~/Applications via 2>/dev/null.
found=$(find /Applications ~/Applications -maxdepth 1 \( -iname "*${A}*uninstall*" -o -iname "*${A}*remove*" \) 2>/dev/null)
[ -n "$found" ] && echo "$found" || echo "(none)"
echo "=== CLI in PATH ==="
# Use `-- "$A"` so names starting with `-` are not parsed as options
CMD=$(command -v -- "$A" 2>/dev/null || true)
if [ -n "$CMD" ] && [ -x "$CMD" ]; then
echo "path: $CMD"
if [ -L "$CMD" ]; then
echo "symlink -> $(readlink "$CMD")"
fi
else
echo "(not in PATH)"
fi
exit 0 # explicit clean exit so the consolidated script returns 0 regardless of individual section find/grep misses
If every primary section above prints (none)/(not ...), also check CLI package managers:
echo "=== command path ==="; command -v -- "${APP_NAME}" 2>/dev/null || echo "(none)"
echo "=== npm global ==="; npm list -g "${APP_NAME}" 2>/dev/null | grep -i "${APP_NAME}" || echo "(none)"
echo "=== pip ==="; pip3 show "${APP_NAME}" 2>/dev/null || echo "(none)"
echo "=== cargo ==="; command -v -- cargo >/dev/null && cargo install --list 2>/dev/null | grep -i "${APP_NAME}" || echo "(none)"
Gate before Phase 2 — explicitly declare in your response:
Installation method: <homebrew-cask | homebrew-formula | pkg | mas | direct-download | cli-pkgmgr | not-found>
Evidence: <the exact labeled section output line(s) that support this>
Do not state a negative ("not Homebrew", "no bundle ID") without quoting the (none) line from the labeled output. If evidence is ambiguous or empty, rerun the script — never proceed on assumption.
Symlink handling: If the CLI in PATH section reports a symlink, determine the relationship and ask the user:
| Scenario | Action |
|---|---|
Symlink to a package manager binary (e.g., npx → npm) | Only remove the symlink |
Symlink to another app (e.g., code → VS Code) | Ask: remove alias only, or uninstall parent app + all aliases? |
| Multiple symlinks to same app | List all; if uninstalling, remove all |
Bundled uninstaller: If found, it takes priority over manual removal in Phase 6. Only use uninstallers from within the installed app bundle or the vendor's verified domain.
Phase 2: Research Official Uninstall Method
Mandatory: Understand the correct uninstall procedure before building a plan.
Shortcut for Homebrew casks: if Phase 1 identified a cask, brew info --cask <token> reveals the zap stanza (which lists the paths --zap will clean). Reviewing this output satisfies Phase 2 for standard casks. Web search is only additionally required when the app:
- installs kernel extensions, system extensions, or launch daemons (e.g.,
docker,karabiner-elements,fuse, VPN clients) - modifies system configuration (
/etc/hosts,/etc/shells, PATH, shell integrations) - manages credentials or keychains at the system level (e.g.,
1password)
For non-Homebrew apps, or when the above conditions apply:
- First search:
"<app name>" official uninstall macOS site:<vendor-domain> - Second search:
"<app name>" uninstall macOS - Evaluate sources — prioritize: official vendor docs > vendor GitHub > Apple Support > community forums
- Reject blog spam, SEO-farm "cleaner" app promotions, and unverified guides
Critical: Some apps have dedicated uninstallers or CLI commands. Missing these can leave kernel extensions, daemons, or system modifications behind.
Phase 3: Scan Associated Data
Safety preamble (always prepend to any Phase 3 script) — an empty BUNDLE_ID would make find -iname "*${BUNDLE_ID}*" expand to ** and match every file on disk. Guard against it:
# Guard: block empty BUNDLE_ID from cascading into a match-everything scan
: "${BUNDLE_ID:?BUNDLE_ID required for Phase 3. If Phase 1 could not resolve it (e.g., CLI-only install, no .app bundle, Spotlight disabled), either resolve it manually (defaults read <app>/Contents/Info CFBundleIdentifier) or remove BUNDLE_ID branches from the find expressions below and rely on APP_NAME + extra manual verification.}"
If the app name is ambiguous (shorter than 4 characters or a common word like go, pro, mail, code, sync, file, app), use bundle ID only:
find ~/Library /Library -maxdepth 3 -iname "*${BUNDLE_ID}*" 2>/dev/null
find ~/.config ~/.local -maxdepth 2 -iname "*${BUNDLE_ID}*" 2>/dev/null
Otherwise, scan with both app name and bundle ID:
echo "=== User Library ==="
find ~/Library -maxdepth 3 \( -iname "*${APP_NAME}*" -o -iname "*${BUNDLE_ID}*" \) 2>/dev/null
echo "=== System Library ==="
find /Library -maxdepth 3 \( -iname "*${APP_NAME}*" -o -iname "*${BUNDLE_ID}*" \) 2>/dev/null
echo "=== XDG Config ==="
find ~/.config ~/.local -maxdepth 2 \( -iname "*${APP_NAME}*" -o -iname "*${BUNDLE_ID}*" \) 2>/dev/null
echo "=== Dotfiles ==="
ls -d ~/."${APP_NAME}" ~/."${APP_NAME}"rc 2>/dev/null
In both cases, require manual verification of every name-based match before including in the removal plan.
Phase 4: Subagent Review of Removal Plan
Mandatory: Before presenting the plan to the user, launch a subagent to review the entire removal plan.
Red flags that mean you are rationalizing skipping this phase — if you catch yourself thinking any of these, stop and invoke the subagent:
- "This is a simple cask uninstall, review is overkill"
- "All paths look safe, nothing under
/Systemor/usr" - "
--zaphandles everything, there is nothing to review" - "I already ran Phase 1 myself, a second read adds nothing"
The subagent's primary job is not catching dangerous paths — those are easy to spot. Its primary job is catching misread evidence from Phase 1 (e.g., declaring "not Homebrew" when brew list actually matched the name, or missing a bundled uninstaller that was buried in a multi-section output).
Subagent prompt must include: app name, bundle ID, installation method, full file list, uninstall steps in order, the raw Phase 1 detection output, and research sources.
Subagent review checklist:
- Uninstall steps match official documentation
- No vendor-provided uninstaller is being skipped
- PKG apps:
pkgutil --files <pkg-id>output reviewed for system-level files - No paths under
/System/,/usr/bin/,/usr/lib/,/bin/,/sbin/,/etc/,/var/,/tmp/,/private/, or~/alone. Paths under/usr/local/lib/,/usr/local/share/,/opt/homebrew/lib/,/opt/homebrew/share/require explicit user confirmation - No overly broad glob patterns; ambiguous names (short or common words) use bundle ID matching only
- Launch agents/daemons identified and will be unloaded before deletion
- Kernel extensions or system extensions identified if applicable
- PKG receipt files list reviewed — no shared components being removed
- Homebrew apps:
brew uses --installed <name>checked for reverse dependencies - Execution order is safe (check/stop processes → unload services → remove app → remove data → forget receipts)
- All paths are absolute and explicitly listed
- Each scan match cross-referenced against bundle ID, not just app name
If the subagent raises any concern, resolve it before proceeding.
Phase 5: Present Removal Plan
Present a categorized table to the user:
| Category | Path | Size | Action |
|---|---|---|---|
| App binary | /Applications/Foo.app | 150 MB | Remove |
| Preferences | ~/Library/Preferences/com.foo.plist | 4 KB | Trash |
| Cache | ~/Library/Caches/com.foo | 23 MB | Remove |
Default recommendation: remove everything (clean uninstall). Flag items containing potentially irreplaceable user data (configuration, databases, project files) and ask explicitly.
Recovery approach: Move user data directories (Application Support, Preferences) to Trash instead of rm -rf. Use rm -rf only for caches and temporary files. To avoid name collisions in Trash, append a timestamp: mv "<path>" ~/.Trash/"$(basename "<path>")_$(date +%s)".
Warn about: login items, browser extensions, privacy permissions, kernel extensions requiring reboot.
Phase 6: Execute with Confirmation
-
Ask for confirmation before any deletion
-
Check for running processes before removal:
pgrep -il "${APP_NAME}"If processes are found, present options to the user:
Option Action Quit gracefully osascript -e "tell application \"${APP_DISPLAY}\" to quit"then recheck after 5s (max 3 retries, then offer force kill)Force kill killall "${APP_DISPLAY}"(warn: may lose unsaved data)Remove auto-launch first, reboot later Unload launch agents/daemons (step 3) + remove login items, then ask user to reboot and re-run removal Note: If a launch agent has
KeepAliveenabled, the process will respawn after quit/kill. In that case, fall back to the "Remove auto-launch first" option. -
Unload launch agents/daemons:
LABEL=$(/usr/libexec/PlistBuddy -c "Print :Label" "<plist-path>") # User agent (~/Library/LaunchAgents/) launchctl bootout "gui/$(id -u)/${LABEL}" launchctl print "gui/$(id -u)/${LABEL}" 2>&1 | grep -q "Could not find" && echo "User agent unloaded" # System daemon (/Library/LaunchDaemons/) — requires sudo sudo launchctl bootout "system/${LABEL}" sudo launchctl print "system/${LABEL}" 2>&1 | grep -q "Could not find" && echo "System daemon unloaded" -
Use Homebrew if applicable — use the exact cask/formula token from Phase 1
brew listoutput (not the user-provided name):- Cask:
brew uninstall --zap --cask "<exact-token>"(--zapremoves all associated files) - Formula:
brew uninstall "<exact-token>"If multiple tokens matchedgrep -iin Phase 1, list all matches and ask the user to select the correct one
- Cask:
-
Use vendor uninstaller if one was found in Phase 1
-
Remove associated data — Trash for user data,
rm -rffor caches. Explicit paths only -
Forget PKG receipts — ALWAYS after removing files (once forgotten, file list is unrecoverable):
sudo pkgutil --forget <pkg-id>
Phase 7: Post-Removal Verification
Targeted verification — check only the specific paths from the removal plan:
# Check each removed path still exists
ls -d <path1> <path2> ... 2>/dev/null
# Check for residual processes
pgrep -il "${APP_NAME}"
# Check for residual login items
osascript -e 'tell application "System Events" to get the name of every login item'
Follow-up reminder checklist — inform the user about any applicable items:
| Condition | Reminder |
|---|---|
| Kernel extension, system extension, or system daemon removed | Reboot required/recommended |
| App had privacy permissions (Accessibility, Full Disk Access, etc.) | Remove in System Settings → Privacy & Security |
| App had Login Items entries | Remove in System Settings → General → Login Items |
| App installed browser extensions | Remove from browser(s) |
| App used network configuration (VPN, proxy, DNS) | Verify System Settings → Network |
| App installed shell integrations (PATH, completions, aliases) | Check ~/.zshrc, ~/.bashrc, ~/.zprofile, /etc/paths.d/ |
| Homebrew dependencies no longer needed | Suggest brew autoremove |
| App stored data in iCloud / cloud sync | Data may still exist in cloud |
App modified /etc/hosts, /etc/shells, or similar | Verify restored |
Always present applicable reminders — err on the side of informing.