mac-declutter 是做什么的?
一个久经实战考验的开发者 Mac 清理指南:涵盖 AI 智能体泛滥、死掉的后台服务、应用残留、代理陷阱和性能审计。
核心原则:
- 删除前先盘点。 绝不盲目卸载——先测量大小、最后使用日期和运行状态,然后让用户选择。
- 除非明确要求,否则绝不触碰会话记录或用户数据。 缓存可以重新生成;会话不能。
- 每次变更后都进行验证——进程消失、端口关闭、登录项移除。
- 系统完整性保护 (SIP) 是一道坚硬的墙。 根拥有的应用、沙盒容器和 DriverKit 扩展都有各自合法的移除路径;不要与操作系统对抗。
阶段 0 —— 网络排查(先做这个)
症状“所有连接都断开了”几乎总是由死掉的本机代理引起的,而不是应用本身的问题:
scutil --proxy # 系统代理指向什么
nc -z -w 2 127.0.0.1 <port> # 端口上是否真的有进程在监听?
lsof -nP -iTCP:<port> -sTCP:LISTEN # 谁占用了这个端口
经典的陷阱:Clash/V2Ray 这类工具将 macOS 系统代理设置为 127.0.0.1:PORT,然后内核进程崩溃了——无论什么规则模式,所有流量都在门口被截断。遵守系统代理的应用(大多数 GUI 应用和一些 CLI)都会失败;而 curl 仍然可以工作,因为它绕过了系统代理,这会误导诊断。修复方法:重启代理核心,或关闭系统代理(networksetup -setwebproxystate Wi-Fi off + -setsecurewebproxystate + -setsocksfirewallproxystate + WPAD 关闭),或将应用的代理设置指向空。记住:curl 会忽略系统代理——使用 curl -x http://127.0.0.1:PORT 来测试应用风格的连接性。
阶段 1 —— 盘点(只读)
运行 scripts/inventory.sh,或手动覆盖以下维度:
- 智能体配置目录:
~/.claude、~/.codex、~/.pi、~/.jcode、~/.config/opencode等。对每个目录:大小(du -sh)、最后活动时间(最新文件的 mtime)、CLI 二进制文件是否仍然存在(which)。 - 登录/认证状态:
auth.json、openai-auth.json、凭据文件——列出键名,绝不打印密钥。 - 后台服务:
launchctl list | grep -v com.apple和~/Library/LaunchAgents/*.plist——读取每个 plist 的ProgramArguments以了解它实际运行什么。 - 应用:对于
/Applications和~/Applications中每一个非苹果的.app,收集大小、通过mdls获取kMDItemLastUsedDate、以及_MASReceipt是否存在(App Store 应用)。“从未打开”+ 体积大 = 优先考虑的对象。 - 缓存:
~/Library/Caches、~/.npm、pip/bun 缓存——体积大且始终可以安全清理。
将发现结果整理为保留/移除表格,让用户选择。标记付费/订阅应用(例如 _MASReceipt、已知的订阅产品),以便用户在删除之前取消订阅。
阶段 2 —— 卸载指南
针对每个目标,按顺序执行:
- 停止服务:对每个相关的 LaunchAgent/Daemon 执行
launchctl remove <label>。 - 删除 plist:
~/Library/LaunchAgents/<label>.plist。 - 删除应用/CLI:
rm -rf删除.app/npm uninstall -g <pkg>/ 删除二进制文件。Permission denied→ 根拥有(由 pkg 安装)。使用osascript -e 'do shell script "rm -rf ..." with administrator privileges'(一次 GUI 密码提示)。绝不盲目使用sudo。
- 库残留:检查并删除
~/Library/{Application Support,Caches,Containers,HTTPStorages,Preferences}/<app-or-bundle-id>。- 沙盒
Containers/*即使使用 root 也可能无法删除(受容器管理器保护)。如果很小,就留着它们——操作系统会回收孤儿容器;禁用 SIP 永远不值得。
- 沙盒
- 系统扩展:
systemextensionsctl list。SIP 会阻止直接卸载——唯一的干净方法是重新安装供应商的应用并运行其官方卸载程序(在/Library/Application Support/<vendor>/下查找uninstall.sh)。DriverKit.dext驱动只在下次重启后才会卸载——告知用户重启后它才会消失。 - 残留的登录项:删除应用后,其“开机自启”条目通常会作为僵尸项目保留。使用
osascript -e 'tell application "System Events" to delete login item "<name>"'移除。 - 残留的正在运行进程:已删除的应用可能留有从更新程序临时目录(例如 Squirrel
ShipIt暂存区)启动的进程。ps aux | grep -i <name>,杀死它们,并检查$HOME(~/.<appname>) 下是否有数据目录——更新程序生成的守护进程通常在那里隐藏了数百 MB 数据。
阶段 3 —— 缓存清理(零风险)
npm cache clean --force
rm -rf ~/Library/Caches/pip ~/.bun/install/cache
rm -rf ~/Library/Caches/<deleted-app>/*
缓存可以重新生成。会话历史记录(sessions/、projects/、*.jsonl 对话日志)不会——除非用户明确要求,否则保留它们。
阶段 4 —— 性能审计
memory_pressure | tail -2 # 空闲百分比和压力级别
sysctl -n vm.swapusage # 在健康机器上,使用的交换空间应该约为 0
sysctl -n vm.loadavg
ps -axm -o rss,comm | awk 'NR>1 {rss=$1; $1=""; sub(/^ /,""); a[$0]+=rss} END {for (k in a) printf "%d\t%s\n", a[k]/1024, k}' | sort -rn | head -15
- 活动监视器中的“已用内存”包含文件缓存——根据内存压力颜色和交换空间来判断,而不是 GB 数字。
- 在大量删除后,Spotlight(
mds_stores、corespotlightd)会重新索引几个小时——这是暂时现象,不要尝试“修复”它。 - 按应用家族对 RSS 进行分组(浏览器和聊天应用会产生许多辅助进程);报告每个家族的总和,而不是每个进程的噪音。
- 不要优化那些本不是问题的地方:在一台零交换空间且压力为绿色的机器上,用日常使用体验(例如一个智能输入法)来换取几百 MB 内存是不划算的——明确说明这一点。
阶段 5 —— 最终验证
- 已删除的标签在
launchctl list中不再出现;plist 文件已删除;没有匹配已删除名称的进程(除了需要重启才能卸载的驱动)。 - 代理端口监听状态与
scutil --proxy一致;关键的 API 端点可以连通(401表示可连通但需要认证,这算通过)。 - 每个幸存的智能体 CLI 都能通过一个真正的端到端测试(发送一行提示,期待回复)。
df -h /操作前后的对比作为最终评分。