关于 disk-hygiene
disk-hygiene 是面向 macOS 开发者的磁盘审计与清理技能,指导检查缓存、构建产物、遗留大文件和下载目录。它强调识别运行中的依赖服务,并谨慎处理缓存锁和硬链接。
主要功能
- 01检查磁盘空间与主要目录
- 02盘点并清理开发工具缓存
- 03查找大型文件和项目构建产物
- 04整理 Downloads 文件夹
- 05清理前检查服务依赖与缓存锁
适用场景
- 01磁盘空间不足时定位占用来源
- 02清理可重新生成的开发缓存
- 03筛选长期未访问的大文件
- 04整理下载目录并选择删除或移动内容
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ terrylica/ cc-skills --skill disk-hygiene安装后在 Claude Code 中输入/disk-hygiene确认已加载。
附带文件references/evolution-log.mdscripts/cache-audit.sh
详细介绍
磁盘清理
审计磁盘使用情况,清理开发者缓存,查找被遗忘的大文件,并整理 macOS 上的“下载”文件夹。
自我进化技能:此技能会在使用中持续改进。如果说明有误、参数发生变化,或需要采用变通方法,请立即修复此文件,不要拖延。仅针对真实且可复现的问题进行更新。
何时使用此技能
适用于以下情况:
- 用户询问磁盘空间、存储或清理相关问题
- 系统可用空间不足
- 用户想查找旧的或被遗忘的大文件
- 用户想清理开发者缓存(brew、uv、pip、npm、cargo)
- 用户想整理“下载”文件夹
- 用户询问磁盘分析工具(dust、dua、gdu、ncdu)
TodoWrite 任务模板
模板 A - 完整磁盘审计
1. Run disk overview (df -h /System/Volumes/Data && major directories)
2. Audit developer caches (uv, brew, pip, npm, cargo, rustup, Docker)
3. Scan in-repo build artifacts (Rust target/, .venv, node_modules — often the biggest, see Phase 2.5)
4. Scan for forgotten large files (>50MB, not accessed in 180+ days)
5. Present findings with AskUserQuestion for cleanup choices
6. Execute selected cleanups
7. Report space reclaimed
模板 B - 仅清理缓存
1. Measure current cache sizes
2. Run safe cache cleanups (brew, uv, pip, npm)
3. Report space reclaimed
模板 C - 整理“下载”文件夹
1. List Downloads contents with dates and sizes
2. Categorize into groups (media, dev artifacts, personal docs, misc)
3. Present AskUserQuestion multi-select for deletion/move
4. Execute selected actions
模板 D - 查找被遗忘的文件
1. Scan home directory for large files not accessed in 180+ days
2. Group by location and type (media, ISOs, dev artifacts, documents)
3. Present findings sorted by size
4. Offer cleanup options via AskUserQuestion
阶段 1 - 磁盘概览
在深入查看具体内容之前,先了解整体情况。
/usr/bin/env bash << 'OVERVIEW_EOF'
echo "=== Disk Overview ==="
# MUST be /System/Volumes/Data, NOT `/`. On APFS (Catalina+) `/` is the SEALED
# READ-ONLY system volume and reports a fixed ~10GB used — it is not your disk.
# Verified 2026-07-31: `df -h /` said "10Gi used, 185Gi avail" on a machine that
# was actually 707GB used and 80% full. Reading `/` will make you conclude there
# is nothing to clean.
df -h /System/Volumes/Data
echo ""
echo "=== Major Directories ==="
du -sh ~/Library/Caches ~/Library/Logs ~/Library/Application\ Support \
~/.Trash ~/Downloads ~/Documents ~/Desktop ~/Movies ~/Music ~/Pictures \
2>/dev/null | sort -rh
echo ""
echo "=== Developer Tool Caches ==="
du -sh ~/.docker ~/.npm ~/.cargo ~/.rustup ~/.local ~/.cache \
~/.conda ~/.pyenv ~/.proto 2>/dev/null | sort -rh
OVERVIEW_EOF
阶段 2 - 缓存审计与清理
缓存大小参考
| 缓存 | 位置 | 典型大小 | 清理命令 |
|---|---|---|---|
| uv | ~/Library/Caches/uv/ or ~/.cache/uv/ | 5-15 GB | uv cache clean |
| Homebrew | ~/Library/Caches/Homebrew/ | 3-10 GB | brew cleanup --prune=all |
| pip | ~/Library/Caches/pip/ | 0.5-2 GB | pip cache purge |
| npm | ~/.npm/_cacache/ | 0.5-2 GB | npm cache clean --force |
| cargo | ~/.cargo/registry/cache/ | 1-5 GB | cargo cache -a(需要 cargo-cache) |
| rustup | ~/.rustup/toolchains/ | 2-10 GB | rustup toolchain uninstall <name>(使用 rustup toolchain list 列出) |
| proto | ~/.proto/tools/<tool>/<version>/ | 每个 0.2-2 GB | proto uninstall <tool> <version>(使用 proto versions <tool> --installed 列出),或 proto clean tools |
| Docker | Docker.app | 5-30 GB | docker system prune -a |
| Playwright | ~/Library/Caches/ms-playwright/ | 0.5-2 GB | npx playwright uninstall |
| sccache | ~/Library/Caches/Mozilla.sccache/ | 1-3 GB | rm -rf ~/Library/Caches/Mozilla.sccache |
| go-build | ~/Library/Caches/go-build/ | 5-25 GB | go clean -cache(如果 go 不在 PATH 上,则使用 rm -rf) |
| huggingface | ~/.cache/huggingface/ | 1-10 GB | rm -rf ~/.cache/huggingface/hub/<model> |
| Claude Code 插件 | ~/.claude/plugins/cache/<marketplace>/<plugin>/<version>/ | 5-30 GB | 删除不在 ~/.claude/plugins/installed_plugins.json 中的版本,并保留最新的 3 个(使用 sort -V),以供已运行的会话使用 |
安全清理命令(始终可重新下载)
/usr/bin/env bash << 'CACHE_CLEAN_EOF'
set -euo pipefail
echo "=== Measuring current cache sizes ==="
echo "uv: $(du -sh ~/Library/Caches/uv/ 2>/dev/null | cut -f1 || echo 'N/A')"
echo "Homebrew: $(du -sh ~/Library/Caches/Homebrew/ 2>/dev/null | cut -f1 || echo 'N/A')"
echo "pip: $(du -sh ~/Library/Caches/pip/ 2>/dev/null | cut -f1 || echo 'N/A')"
echo "npm: $(du -sh ~/.npm/_cacache/ 2>/dev/null | cut -f1 || echo 'N/A')"
echo ""
echo "=== Cleaning ==="
brew cleanup --prune=all 2>&1 | tail -3
uv cache prune 2>&1 # prune, NOT `clean --force` — see "uv cache lock" below
pip cache purge 2>&1
npm cache clean --force 2>&1
CACHE_CLEAN_EOF
缓存清理故障排查
| 问题 | 原因 | 解决方案 |
|---|---|---|
uv cache 锁被占用 | 先确定占用者 | 见下文“uv cache 锁”——--force 可能带来实际危险 |
brew cleanup 跳过 formula | 已链接,但不是最新版本 | 可以安全忽略,或执行 brew reinstall <pkg> |
pip cache purge 权限被拒绝 | 系统 pip 与用户 pip 的区别 | 使用 python -m pip cache purge |
| Docker 未运行 | Docker Desktop 尚未启动 | 先启动 Docker.app,或跳过 |
⚠️ uv cache 锁——在确认占用者之前,绝不要使用 --force
uv cache clean 会等待 300 s 获取独占锁,然后报错。此技能的早期版本曾建议“使用 --force”。如果锁的占用者是长期运行的服务,这条建议就是错的,而且与 api-gateway 事件属于同一类错误:在运行中的守护进程下方修改缓存。
2026-08-24 测量结果:uv cache clean 超时,占用者为
uv 1934 uv run python ~/code/search-svc/kernel/embed.py serve --model minishlab/potion-retrieval-32M
uv 2652 uv run python ~/code/search-svc/kernel/embed.py rerank --serve --model Xenova/ms-marco-MiniLM-L-6-v2
——两者都是 launchd 作业 com.search-svc.serve 的子进程,已运行 1 h 37 m。这些是持久守护进程,因此锁永远不会释放,clean 不可能自行成功。在决定如何处理之前,务必先确定占用者:
lsof ~/.cache/uv/.lock 2>/dev/null # exact PIDs holding the lock
ps -o pid,ppid,lstart,command -p <PIDS> # how long, and whose child
launchctl list | grep -i <service> # is the parent a launchd job?
然后根据占用者选择操作:
| 占用者 | 操作 |
|---|---|
临时进程(刚运行的 uv sync) | 等待它结束,或使用 --force——这确实安全 |
| 长期运行的 launchd/守护进程 | 不要使用 --force。 要么跳过,要么依次执行 launchctl bootout → 清理 → bootstrap → 验证 |
| 未知 / 无法确定 | 跳过。缓存绝不值得用服务中断来换 |
日常维护时优先使用 uv cache prune,而不是 uv cache clean。 prune 只删除_未使用的_条目,并保留所有运行中的环境所依赖的内容,因此它才是定期维护的正确命令;clean 会清除所有内容,导致必须重新完整下载。
uv archive-v0 会悄悄累积完整的虚拟环境
uv 缓存显示的总量容易误导,因此在判断之前应先分类。文档说明,archive-v0 存储的是解包后的 wheel 文件主体,并通过硬链接放入各个 .venv——这部分确实有用。但它也会累积从不进行垃圾回收的完整虚拟环境(构建环境 / 工具环境),这些纯属冗余。2026-08-24 对 69 GB uv 缓存的测量结果:
| 条目类型 | 数量 | 大小 |
|---|---|---|
完整 venv(包含 pyvenv.cfg) | 210 | 39 GB |
| 实际解包的 wheel | 1,577 | 10 GB |
因此,archive-v0 中有 78 % 是孤立环境,而非去重层。应先分类——这一区分会改变诊断结果和解决方式(用 prune,而非 clean):
du -sk ~/.cache/uv/archive-v0/* 2>/dev/null > /tmp/uv-all.txt
while read -r kb path; do
[ -f "$path/pyvenv.cfg" ] && echo "VENV $((kb/1024))MB $path"
done < /tmp/uv-all.txt | sort -k2 -rn | head
承诺能回收多少空间之前,也要检查硬链接数量。 uv 会将缓存文件硬链接到运行中的 .venv,因此删除 links>1 的缓存条目_不会回收任何空间_:
stat -f 'links=%l size=%z %N' "$(find ~/.cache/uv/archive-v0 -type f -size +20M | head -1)"
# links=1 -> deleting truly reclaims; links>1 -> shared with a live venv, no gain
不要把“Python 太臃肿”当作结论。 在同一次审计中,各仓库的 Rust target/ 目录总计 57 GB,而 Python .venv 约为 7 GB——前者是后者的 8 倍——因为每个仓库各自拥有 target/,彼此不共享,而 uv 的归档由所有项目共享。应报告实测的细分数据,而非未经验证的说法。
第 2.5 阶段 - 项目构建产物(仓库内,可重新生成)
这是最容易漏掉的一类——每次审计都要检查。 编译器和依赖项的输出位于_仓库内部_,而非 ~/Library 下,因此第 1/2 阶段的扫描无法发现它们。单个活跃 Rust 仓库的 target/ 通常就有 10-35 GB;在整个开发目录中,这些产物的总量可能超过所有缓存之和(某次实际审计中,target/ 占 62 GB,.venv 占 20 GB)。这些内容都能在下一次构建时重新生成——唯一的代价是重新编译 / 同步所需的时间。
| 产物 | 目录名 | 典型大小 | 重新生成方式 |
|---|---|---|---|
| Rust 构建 | target/ | 每个 1-35 GB each | cargo build |
| Python 虚拟环境 | .venv/ | 每个 0.1-2 GB | uv sync / uv venv |
| Node 模块 | node_modules/ | 每个 0.1-0.5 GB | npm / bun install |
| Zig 缓存 | .zig-cache/, zig-cache/ | 每个 0.1-1 GB | 下一次 zig build |
查找 + 统计大小(将 ROOTS 指向你的代码目录)
/usr/bin/env bash << 'ARTIFACT_SCAN_EOF'
ROOTS=(~/code ~/projects ~/src)
for n in target .venv node_modules .zig-cache zig-cache; do
echo "=== $n (top 10 by size) ==="
find "${ROOTS[@]}" -maxdepth 5 -type d -name "$n" -prune 2>/dev/null \
-exec du -sh {} \; 2>/dev/null | sort -rh | head -10
done
ARTIFACT_SCAN_EOF
安全删除
Rust target/——防止误匹配。 仅删除同级目录中存在 Cargo.toml 的 target/,这样就不会误删恰好名为 “target” 的无关文件夹:
/usr/bin/env bash << 'TARGET_CLEAN_EOF'
ROOTS=(~/code ~/projects)
find "${ROOTS[@]}" -maxdepth 5 -type d -name target -prune 2>/dev/null | while read -r t; do
[ -f "$(dirname "$t")/Cargo.toml" ] && rm -rf "$t" && echo "cleaned: $t"
done
TARGET_CLEAN_EOF
.venv / node_modules 可以按名称批量安全删除(下次运行 uv sync / install 时会重新生成):
find ~/code ~/projects -maxdepth 5 -type d -name .venv -prune -exec rm -rf {} +
⚠️ 先检查是否有依赖这些目录的服务——这不是可选项
只有对于_当前没有运行_的仓库,node_modules 和 .venv 才能“安全地批量删除”。2026-07-31,一次批量删除移除了 api-gateway/node_modules;随后其 launchd 看门狗连续失败 95 次,并在尝试重启网关时启动了 Chrome,引发 macOS TCC 提示。用户将其报告为一个莫名其妙的权限弹窗,而磁盘清理与该症状之间隔了两步。
删除任何内容之前,先建立排除列表:
/usr/bin/env bash << 'DEPCHECK_EOF'
# Every repo backing a live launchd job — never delete artifacts inside these.
for p in "$HOME"/Library/LaunchAgents/*.plist; do
prog=$(plutil -extract ProgramArguments.0 raw "$p" 2>/dev/null) || continue
case "$prog" in "$HOME"/*) ;; *) continue ;; esac
d=$(dirname "$prog")
for _ in 1 2 3 4 5; do
{ [ -f "$d/package.json" ] || [ -f "$d/pyproject.toml" ]; } && break
d=$(dirname "$d"); [ "$d" = "$HOME" ] && break
done
[ "$d" = "$HOME" ] && continue # walked out; not a real repo match
echo "$d"
done | sort -u
DEPCHECK_EOF
然后,对位于这些根目录下的任何候选路径执行 SKIP,并打印跳过信息,以便操作人员确认保护措施已触发。清理后,重新运行相同的列表,并确认每个仓库仍有与清单匹配的目录(package.json → node_modules,pyproject.toml → .venv)。
⚠️ 检查仓库中的每个清单文件,而不只是根目录中的文件。 上述版本会从 launchd 程序所在位置向上查找第一个
package.json或pyproject.toml,然后停止——因此,对于服务代码位于子目录中的仓库,它验证的是错误的目录。2026-08-03,在~/code/search-svc中进行的检查表明:根目录有pyproject.toml(所以检查报告.venv=ok和node_modules=—,即“无需检查”),但服务实际需要的是**ts/node_modules**,而该目录缺失。保护检查报告仓库状态正常,但其 launchd 任务已崩溃重启 11,593 次。改为逐个枚举:find "$repo" -name package.json -not -path '*/node_modules/*' -maxdepth 3 \ | while read -r m; do d=$(dirname "$m"); [ -d "$d/node_modules" ] || echo "MISSING $d/node_modules"; done find "$repo" -name pyproject.toml -not -path '*/.venv/*' -maxdepth 3 \ | while read -r m; do d=$(dirname "$m"); [ -d "$d/.venv" ] || echo "MISSING $d/.venv"; done另请注意,Python 虚拟环境即使存在,也可能并不完整:
uv sync只会安装默认依赖组。search-svc在[dependency-groups] embed下声明了其嵌入依赖,因此虚拟环境虽然存在,import pymupdf也能正常运行,但在运行import numpy时会失败,直到执行uv sync --group embed。目录存在不等于依赖项已安装——如果仓库文档说明了依赖组 / 附加项,就应将其恢复。 🔴 如果任务执行的 runner shim 位于仓库外,向上遍历将什么也找不到。 上述两个版本都会从ProgramArguments.0开始,沿文件系统向_上_遍历。但 launchd runner-shim 策略(将 shim 签名并命名为独特名称,放在~/.local/libexec/或~/.claude/tools/launchd-runners/libexec/中)会把程序放在一个与仓库毫无祖先关系的目录里——向上遍历会在$HOME处终止,或者更糟,遍历到~/.claude,并将_它_报告为仓库。随后,保护逻辑会生成一份看似可靠、实际完全错误的排除列表,真正的仓库则会被删除。2026-09-13 的测量结果:排除列表将
~/.claude列为 16 个任务的目录,却完全没有提到~/code/terminal-scripts、~/code/trading-tools、~/code/private-monorepo——因此清理操作删除了这三个仓库的.venv/node_modules,导致com.example.terminal-snapshot(崩溃安全快照)、com.example.notify-telemetry(Bun/TS 守护进程)和com.example.keystroke-helper无法运行。除了向上遍历,还要在 shim 中搜索仓库路径:for p in "$HOME"/Library/LaunchAgents/*.plist; do prog=$(plutil -extract ProgramArguments.0 raw "$p" 2>/dev/null) || continue [ -f "$prog" ] || continue # shim 通常是编译后的二进制文件——使用 `strings`,而非 `grep`;也要搜索环境变量默认值 # (例如 SCRIPTS_REPO=/Users/.../code/terminal-scripts) strings "$prog" 2>/dev/null \ | grep -oE '/Users/[^/]+/(code|projects|src)/[A-Za-z0-9._-]+' | sort -u done | sort -u将这些结果与向上遍历的结果合并。还要检查 plist 中的
StandardOutPath/StandardErrorPath和WorkingDirectory——即使ProgramArguments没有指向真正的仓库,这些路径通常也会指向那里。还要保护所有正在被运行中进程使用的仓库,而不只是 launchd 任务使用的仓库。 正在运行的 Claude 会话、开发服务器和手动启动的长运行守护进程不会出现在 plist 中。将运行中命令行和进程工作目录中提到的仓库也合并进来:
{ ps -axo command=; lsof -d cwd -Fn 2>/dev/null | sed -n 's/^n//p'; } \ | grep -oE "$HOME/(code|projects|src)/[A-Za-z0-9._-]+" | sort -u2026-10-15 的测量结果:launchd 扫描列出了 6 个仓库;这又发现了 5 个,否则仅靠 launchd 扫描就会在进程仍在运行时删除它们。
推论——自愈型 runner 可能已永久失效,却看起来一切正常。
terminal-snapshot的 shim 会在 venv 缺失时重建它,但会限制尝试次数,并将计数保存在~/.local/state/<job>/venv-bootstrap-attempts.txt中。自 8 月 21 日起,这个计数一直停在5/5,由于 venv 仍然存在,所以从未检查过。删除 venv 后,任务首次尝试就触发了_过期_的次数上限,拒绝自愈:FATAL: venv bootstrap cap exhausted (5/5)。恢复依赖还不够——重置尝试计数并 kickstart,然后从日志中确认实际工作已恢复(本例中为[auto-snapshot] wrote 17 tabs),而不只是看到exit 0。
注意事项:
- 先运行
pgrep -fl 'cargo build|rustc|zig build'——绝不要删除正在运行构建/测试的仓库中的构建产物。 - 这是一种以可重新生成内容的成本换取空间的权衡:下次构建时需要从头构建(大型 Rust crate 可能需要几分钟)。对于闲置仓库值得这么做;但要跳过即将用于构建的那个仓库。
- 如果更倾向于使用工具原生方式,
cargo clean(按仓库分别运行)等同于rm -rf target。
阶段 3 - 检测遗忘文件
查找超过 180 天未访问的大文件。
/usr/bin/env bash << 'STALE_EOF'
echo "=== Large forgotten files (>50MB, untouched 180+ days) ==="
echo ""
# Scan home directory (excluding Library, node_modules, .git, hidden dirs)
find "$HOME" -maxdepth 4 \
-not -path '*/\.*' \
-not -path '*/Library/*' \
-not -path '*/node_modules/*' \
-not -path '*/.git/*' \
-type f -atime +180 -size +50M 2>/dev/null | \
while read -r f; do
mod_date=$(stat -f '%Sm' -t '%Y-%m-%d' "$f" 2>/dev/null)
size=$(du -sh "$f" 2>/dev/null | cut -f1)
echo "${mod_date} ${size} ${f}"
done | sort
echo ""
echo "=== Documents & Desktop (>10MB, untouched 180+ days) ==="
find "$HOME/Documents" "$HOME/Desktop" \
-type f -atime +180 -size +10M 2>/dev/null | \
while read -r f; do
mod_date=$(stat -f '%Sm' -t '%Y-%m-%d' "$f" 2>/dev/null)
size=$(du -sh "$f" 2>/dev/null | cut -f1)
echo "${mod_date} ${size} ${f}"
done | sort
STALE_EOF
突然空间骤减:按最近写入时间排查,而非按文件年龄
如果几小时内可用空间减少了几十或几百 GB,上面的按年龄扫描就找不到这些文件,通常目录的 du 也找不到(它们往往不在 $HOME 下)。请在整个 Data 卷中查找近期写入的文件,包括 /private/tmp:
find /System/Volumes/Data -xdev -type f -mmin -180 -size +2G -exec ls -la {} + 2>/dev/null | sort -k5 -rn | head
lsof "<file>" # still open? Deleting an open file frees nothing until the writer exits; truncate it instead
实测于 2026-10-16:这台 Mac 的可用空间在几小时内从 293 GB 降至 14 GB(99%)。原因是 /private/tmp 中的一个 262 GB 文件,它由 cat run-*.log > run-all.log 写入;其输出匹配了自身的 glob,因此 cat 不断读回自己的输出,直到磁盘写满。最后一行是 cat: stdout: No space left on device。当时没有进程打开该文件,所以 rm 立即释放了空间。也请先检查该卷的快照(diskutil apfs listSnapshots disk3s1):如果某个快照早于你的删除操作,它会让已删除文件占用的块继续保留。
排查大文件时的两个陷阱
1. 表观大小 ≠ 实际占用大小(稀疏文件)。 ls -l 和 find -size 显示的是文件的_逻辑_范围;du 显示的是磁盘上实际占用的块数。索引损坏或数据库出现异常偏移,会生成稀疏文件,两者大小可能相差几个数量级。2026-08-03 在一个 ChromaDB HNSW 文件上的实测:
ls -l link_lists.bin -> 2831.5 GB (apparent — impossible on a ~1 TB disk)
du -h link_lists.bin -> 174 GB (actual)
请始终用 du 确认候选文件的大小。如果 ls -l 显示的大小超过磁盘容量,你就找到了一个稀疏文件——这通常意味着有值得向上游报告的 Bug,而不只是可以清理的磁盘占用。绝不要 cp 这样的文件(简单复制会填充空洞)。
2. 应用会隔离自己的损坏数据——查找带有自述标签的目录。 运行良好的数据存储会重命名损坏的集合,而不是将其删除;新名称会说明诊断结果。在最大的目录中搜索这些标记:
find "$BIG_DIR" -maxdepth 2 -name '*corrupt*' -o -name '*.drift-*' \
-o -name '*.pre-rebuild-*' -o -name '*.bak-*' -o -name '*.quarantine*'
删除之前,请确认该目录未被引用且已有替代品:
- 没有正在运行的进程持有该目录内的文件描述符——
lsof -p <pid> | grep <dir>返回 0; - 应用自身的索引或清单中不包含其 UUID;
- 已有健康的替代集合,且应用在此之后已完成一次运行。
真实案例:~/.memory-store 已增长至 190 GB,其中 175 GB 是一个名为 <uuid>.corrupt-20260802-160712.drift-20260802-160712 的目录——应用已诊断出持续 3 天的崩溃循环造成的损坏,并将其隔离;一个健康的 882 MB 集合已取而代之。执行一次删除操作后,该卷的已用空间占比从 82 % 降至 60 %。
常见的遗忘文件类型
| 类型 | 常见位置 | 示例 |
|---|---|---|
| Windows/Linux ISO 文件 | Documents、Downloads | VM 设置中的 .iso 文件 |
| CapCut/iMovie 导出文件 | Movies/ | 大型 .mp4 渲染文件 |
| 手机视频传输文件 | Pictures/、DCIM/ | iPhone 拍摄的 .MOV 文件 |
| 旧 Zoom 录制文件 | Documents/ | 会议产生的 .aac、.mp4 文件 |
| 孤立下载文件 | Documents/ | CFNetworkDownload_*.mp4 |
| 屏幕录制文件 | Documents/、Desktop/ | Capto/QuickTime 的 .mov 文件 |
Phase 4 - 下载内容分类
使用 AskUserQuestion 的多选功能,让用户选择要清理的内容。
工作流
- 列出
~/Downloads中所有文件的日期和大小 - 将文件归类为逻辑分组
- 使用 AskUserQuestion 展示分类选项,并启用多选
- 将个人或敏感 PDF 单独列出(保留、移至 Documents 或删除)
- 执行所选操作
分类模式
/usr/bin/env bash << 'DL_LIST_EOF'
echo "=== Downloads by date and size ==="
find "$HOME/Downloads" -maxdepth 1 \( -type f -o -type d \) ! -path "$HOME/Downloads" | \
while read -r f; do
mod_date=$(stat -f '%Sm' -t '%Y-%m-%d' "$f" 2>/dev/null)
size=$(du -sh "$f" 2>/dev/null | cut -f1)
echo "${mod_date} ${size} $(basename "$f")"
done | sort
DL_LIST_EOF
AskUserQuestion 模板
呈现 Downloads 清理选项时,请使用以下模式:
- 问题 1(multiSelect:true)- “你想删除 ~/Downloads 中的哪些项目?”
- 按类型分组:电影文件(附总大小)、旧 PDF/文档、开发产物、应用导出文件
- 问题 2(multiSelect:false)- “个人/敏感 PDF 怎么处理?”
- 选项:全部保留、移至 Documents、删除(已有副本)
- 问题 3(multiSelect:false)- “你偏好使用哪种日常清理工具?”
- 选项:dust + dua-cli、Hazel 自动化、自定义 launchd 脚本
磁盘分析工具参考
对比(在约 632GB 的主目录和 Apple Silicon 上进行基准测试)
| 工具 | 总耗时 | CPU 使用率 | 交互式删除 | 安装 |
|---|---|---|---|---|
| dust | 20.4s | 637%(并行) | 否(仅查看) | brew install dust |
| gdu-go | 28.8s | 845%(高度并行) | 是(TUI) | brew install gdu |
| dua-cli | 37.1s | 237%(中等) | 是(暂存后安全删除) | brew install dua-cli |
| ncdu | 96.6s | 43%(单线程) | 是(TUI) | brew install ncdu |
推荐组合
- 使用
dust快速查看“空间都去哪儿了”——扫描速度最快,输出目录树 - 使用
dua i或gdu-go进行交互式探索和删除
快速用法
# dust - instant tree overview
dust -d 2 ~ # depth 2
dust -r ~/Library # reverse sort (smallest first)
# dua - interactive TUI with safe deletion
dua i ~ # navigate, mark, delete with confirmation
# gdu-go - ncdu-like TUI, fast on SSDs
gdu-go ~ # full TUI with delete support
gdu-go -n ~ # non-interactive (for scripting/benchmarks)
安装所有工具
brew install dust dua-cli gdu
注意:gdu 安装为 gdu-go,以避免与 coreutils 冲突。
快速清理摘要
按通常可释放的空间从多到少排序:
| 操作 | 通常可释放空间 | 风险 | 命令 |
|---|---|---|---|
Rust target/ 目录(阶段 2.5) | 10-60 GB+ | 无(下次运行 cargo build 时会重新完整构建) | find ROOTS -type d -name target + Cargo.toml 同级目录检查 |
Python .venv 目录(阶段 2.5) | 通常约 0 GB | 无(通过 uv sync 重新同步) | uv 会将 venv 文件硬链接到缓存:使用 df 测量前后差异,而不是 du |
go clean -cache | 5-25 GB | 无(会重新下载) | go clean -cache |
uv cache prune | 5-40 GB | 无(仅删除未使用的条目) | uv cache prune(先检查 lsof ~/.cache/uv/.lock) |
brew cleanup --prune=all | 3-10 GB | 无(会重新下载) | brew cleanup --prune=all |
| 删除 Downloads 中的电影文件 | 2-10 GB | 请先检查 | 在 AskUserQuestion 后手动操作 |
| 清理旧的 rustup 工具链 | 2-5 GB | 保留当前版本 | rustup toolchain list,然后运行 rustup toolchain uninstall <name> |
| 清理过期的 proto 工具链 | 0.5-3 GB | 先与 .prototools 中固定的版本交叉检查 | proto versions <tool> --installed,然后运行 proto uninstall <tool> <version> |
npm cache clean --force | 0.5-2 GB | 无(会重新下载) | npm cache clean --force |
pip cache purge | 0.5-2 GB | 无(会重新下载) | pip cache purge |
| Docker system prune | 5-30 GB | 会移除已停止的容器 | docker system prune -a |
| 清空废纸篓 | 不定 | 不可逆 | find ~/.Trash -mindepth 1 -delete(不要使用 rm -rf ~/.Trash/*) |
更改后检查清单
修改此技能后:
- 已在 macOS(Apple Silicon)上测试缓存命令
- 基准测试数据仍为最新(工具更新后重新运行)
- AskUserQuestion 模式与当前工具 API 一致
- 所有 bash 代码块均使用
/usr/bin/env bash << 'EOF'包装 - 没有硬编码的用户路径(使用
$HOME) - 将更改追加到 evolution-log.md
故障排除
| 问题 | 原因 | 解决方案 |
|---|---|---|
uv cache clean 挂起 / 300 s 后超时 | 锁被 uv 进程持有——通常是持久运行的 launchd 守护进程,因此永远不会释放 | 使用 lsof ~/.cache/uv/.lock 找出持有者,然后再决定。切勿盲目使用 --force。优先使用 uv cache prune。参见第 2 阶段中的“uv cache lock” |
rm -rf ~/.Trash/* 因 no matches found 中止并以状态码 1 退出 | Shell 是 zsh,其默认的 nomatch 会将不匹配的 glob 视为致命错误——因此 rm 根本不会运行,非零状态码还可能中止 set -e 脚本,或被误认为删除失败 | 使用 find ~/.Trash -mindepth 1 -delete,它不使用 glob,能处理电影发行版文件名中的空格 / 方括号,并且在废纸篓为空时不会执行任何操作 |
brew cleanup 释放 0 字节 | 已清理干净,或 formulae 已链接 | 运行 brew cleanup --prune=all |
find 报告权限被拒绝 | 系统完整性保护 | 添加 2>/dev/null 以隐藏错误 |
找不到 gdu 命令 | 安装的是 gdu-go | 使用 gdu-go(避免与 coreutils 冲突) |
dust 显示的大小与 df 不同 | 统计方法不同 | 正常——df 包含文件系统开销 |
| 扫描陈旧文件速度很慢 | 目录树很深 | 限制 -maxdepth 或排除更多路径 |
| 无法访问 Docker | 桌面应用未运行 | 启动 Docker.app,或跳过 Docker 清理 |
带有含空格路径的 heredoc 报错 parse error near TASK_ID=$(pueue add ...) | 用户 Shell 钩子(例如 pueue 提交钩子)会重新解析命令字符串,并在 heredoc 中遇到 ${var}/Path With Spaces/* glob 时出错 | 先使用 Write 工具将多行脚本写入 /tmp/<name>.sh,然后运行 bash /tmp/<name>.sh——这样可以完全绕过内联 heredoc → 钩子重新引用的路径 |
| 释放远程 Linux 主机上的“旧操作系统”分区 | 操作系统迁移后,留在数据盘上的旧根分区看起来像无用数据(usr、opt、boot、swap.img) | 删除前,先查阅主机的迁移文档和固件启动项(efibootmgr -v):它可能是文档所述、在观察期内用于回滚的路径。没有打开的文件和引用,并不代表未被使用。测量日期为 2026-10-15:一次清理在观察期结束前 25 天删除了一个备用根分区 |
rsync -a src/ dest/ 放宽了你以 0700 权限创建的目录 | -a 会将源目录自身的权限复制到 dest | 暂存敏感数据时,在 rsync 后运行 chmod 0700(或使用 --chmod=D0700,F0600),并在依赖该权限前使用 stat 检查 |
| 在有 Agent 创建 git worktree 的主机上,磁盘一夜之间被占满 | 每个 worktree 都会检出一份独立的 LFS 跟踪数据 | 统计 worktree 数量并检查各自数据目录的大小;只移除干净、空闲且已完整推送的 worktree,并使用 git worktree remove。长久的解决方案应在仓库中实施:为大体积数据设置 lfs.fetchexclude,并将其符号链接到 worktree 中 |
| 移除 proto 工具链会立即触发自动重新安装 | 项目的 .prototools 固定了刚刚移除的版本;启用 auto-install 时,proto 会在该项目中下次调用时重新安装该版本,禁用时则会导致调用失败 | 运行 proto uninstall <tool> <version> 前,先在所有可访问的 .prototools 文件中搜索该版本。如果有固定版本,就保留它,或先更新固定版本。项目中的 rust-toolchain.toml 文件与 rustup 工具链也适用同样的规则。 |
适用于钩子的多行脚本
如果用户的 Shell 环境中有拦截工具调用的 Bash 钩子(pueue、asciinema 等),且 here-doc 模式因晦涩的解析错误而失败,请将脚本写入临时文件并调用:
# Instead of: bash << 'EOF' ... EOF
# Use: Write tool → /tmp/<task>.sh, then:
bash /tmp/<task>.sh
即使安装了钩子,像 du -sh "$HOME/Library/Application Support"/Google/* 2>/dev/null | sort -rh | head 这样的单行 Bash 调用也能正常运行——只有包含带空格路径通配符的多行 here-doc 会出现问题。
执行后反思
此技能完成后,请在结束任务前进行反思:
- 确认位置。 — 编辑前找到此 SKILL.md 的规范路径。
- 哪里失败了? — 修复导致失败的指令。
- 哪些做法比预期更有效? — 将其提升为推荐实践。
- 哪里出现了偏差? — 修复任何与实际情况不再匹配的脚本、参考资料或依赖项。
- 记录下来。 — 在演进日志中记录触发原因、修复内容和证据。
不要推迟。下一次调用会继承你留下的一切。