mcpField
D
disk-hygiene

disk-hygiene

github.com/terrylica生产力1270 次安装1 天前更新
代码仓库

关于 disk-hygiene

disk-hygiene 是面向 macOS 开发者的磁盘审计与清理技能,指导检查缓存、构建产物、遗留大文件和下载目录。它强调识别运行中的依赖服务,并谨慎处理缓存锁和硬链接。

主要功能

  1. 01检查磁盘空间与主要目录
  2. 02盘点并清理开发工具缓存
  3. 03查找大型文件和项目构建产物
  4. 04整理 Downloads 文件夹
  5. 05清理前检查服务依赖与缓存锁

适用场景

  1. 01磁盘空间不足时定位占用来源
  2. 02清理可重新生成的开发缓存
  3. 03筛选长期未访问的大文件
  4. 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 GBuv cache clean
Homebrew~/Library/Caches/Homebrew/3-10 GBbrew cleanup --prune=all
pip~/Library/Caches/pip/0.5-2 GBpip cache purge
npm~/.npm/_cacache/0.5-2 GBnpm cache clean --force
cargo~/.cargo/registry/cache/1-5 GBcargo cache -a(需要 cargo-cache)
rustup~/.rustup/toolchains/2-10 GBrustup toolchain uninstall <name>(使用 rustup toolchain list 列出)
proto~/.proto/tools/<tool>/<version>/每个 0.2-2 GBproto uninstall <tool> <version>(使用 proto versions <tool> --installed 列出),或 proto clean tools
DockerDocker.app5-30 GBdocker system prune -a
Playwright~/Library/Caches/ms-playwright/0.5-2 GBnpx playwright uninstall
sccache~/Library/Caches/Mozilla.sccache/1-3 GBrm -rf ~/Library/Caches/Mozilla.sccache
go-build~/Library/Caches/go-build/5-25 GBgo clean -cache(如果 go 不在 PATH 上,则使用 rm -rf)
huggingface~/.cache/huggingface/1-10 GBrm -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)21039 GB
实际解包的 wheel1,57710 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 eachcargo build
Python 虚拟环境.venv/每个 0.1-2 GBuv sync / uv venv
Node 模块node_modules/每个 0.1-0.5 GBnpm / 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 -u

2026-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、DownloadsVM 设置中的 .iso 文件
CapCut/iMovie 导出文件Movies/大型 .mp4 渲染文件
手机视频传输文件Pictures/、DCIM/iPhone 拍摄的 .MOV 文件
旧 Zoom 录制文件Documents/会议产生的 .aac、.mp4 文件
孤立下载文件Documents/CFNetworkDownload_*.mp4
屏幕录制文件Documents/、Desktop/Capto/QuickTime 的 .mov 文件

Phase 4 - 下载内容分类

使用 AskUserQuestion 的多选功能,让用户选择要清理的内容。

工作流
  1. 列出 ~/Downloads 中所有文件的日期和大小
  2. 将文件归类为逻辑分组
  3. 使用 AskUserQuestion 展示分类选项,并启用多选
  4. 将个人或敏感 PDF 单独列出(保留、移至 Documents 或删除)
  5. 执行所选操作
分类模式
/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 使用率交互式删除安装
dust20.4s637%(并行)否(仅查看)brew install dust
gdu-go28.8s845%(高度并行)是(TUI)brew install gdu
dua-cli37.1s237%(中等)是(暂存后安全删除)brew install dua-cli
ncdu96.6s43%(单线程)是(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 -cache5-25 GB无(会重新下载)go clean -cache
uv cache prune5-40 GB无(仅删除未使用的条目)uv cache prune(先检查 lsof ~/.cache/uv/.lock)
brew cleanup --prune=all3-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 --force0.5-2 GB无(会重新下载)npm cache clean --force
pip cache purge0.5-2 GB无(会重新下载)pip cache purge
Docker system prune5-30 GB会移除已停止的容器docker system prune -a
清空废纸篓不定不可逆find ~/.Trash -mindepth 1 -delete(不要使用 rm -rf ~/.Trash/*)

更改后检查清单

修改此技能后:

  1. 已在 macOS(Apple Silicon)上测试缓存命令
  2. 基准测试数据仍为最新(工具更新后重新运行)
  3. AskUserQuestion 模式与当前工具 API 一致
  4. 所有 bash 代码块均使用 /usr/bin/env bash << 'EOF' 包装
  5. 没有硬编码的用户路径(使用 $HOME)
  6. 将更改追加到 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 会出现问题。

执行后反思

此技能完成后,请在结束任务前进行反思:

  1. 确认位置。 — 编辑前找到此 SKILL.md 的规范路径。
  2. 哪里失败了? — 修复导致失败的指令。
  3. 哪些做法比预期更有效? — 将其提升为推荐实践。
  4. 哪里出现了偏差? — 修复任何与实际情况不再匹配的脚本、参考资料或依赖项。
  5. 记录下来。 — 在演进日志中记录触发原因、修复内容和证据。

不要推迟。下一次调用会继承你留下的一切。

评论

支持 Markdown