Config GC — Claude Code 配置垃圾回收
借鉴运行时垃圾回收的思想:定期扫描不再被引用、冗余、过期或低价值的对象,并回收空间。关键区别在于:此处回收需要人工参与。绝不要自主删除。
何时激活
- 用户要求清理、审计或精简其 Claude Code 配置
- 用户抱怨技能太多、钩子嘈杂或会话启动缓慢
- 月度/周期性配置审查到期
- 安装大型技能包后(例如本仓库),以协调与现有配置的重叠
不要激活于:清理项目源代码(那是重构)、清除聊天历史或卸载 Claude Code 本身。
设计理念
- 仅追加的配置会泄漏。 技能、记忆文件、钩子和权限条目只会增加。没有定期审查,它们会悄然腐烂。
- 定期审计胜过一次性清理。 大约每 30 天扫描一次,每次提出一小批候选。
- 分通道策略。 每种累积类型(技能、钩子、权限……)都有其自身的陈腐信号——不要对所有类型应用同一规则。
- 先软删除。 重命名为
.disabled> 移动到~/.claude/_gc_trash/> 真实删除。始终保留撤销路径。 - 强制人工参与。 每个候选都有其自己的
[y/n/skip]确认。没有“全部同意”快捷方式。 - 保留日志。 每次垃圾回收运行都会追加到
~/.claude/gc_log.md:涉及的内容、原因以及如何撤销。
扫描通道
| # | 通道 | 路径 | 陈腐/冗余信号 |
|---|---|---|---|
| 1 | 技能 | ~/.claude/skills/*/ | 名称高度重叠;近期对话中从未触发;领域与用户实际工作不匹配;SKILL.md 损坏或为空 |
| 2 | 记忆 | ~/.claude/**/memory/*.md 及其索引 | 同一主题有多个索引条目;内容与较新条目冲突;日期已过;索引中缺少孤立文件;应合并的少于 100 字的片段 |
| 3 | 钩子 | ~/.claude/hooks/ + 设置 | 脚本存在于磁盘但未被任何钩子配置引用;被重写取代的旧版本 |
| 4 | 权限 | permissions.allow 在 settings.json / settings.local.json | 重复条目;已被通配符覆盖的特定条目(例如当 Bash(*) 已允许时仍有 Bash(git push));过去实验的一次性授权 |
| 5 | MCP 服务器 | ~/.claude.json 或项目 .mcp.json | 无法连接的服务器;功能重复的服务器;长期未使用的服务器 |
| 6 | 定时提醒/作业 | 用户存放的位置 | 超过 30 天的一次性触发;目标脚本不再存在的作业 |
| 7 | 项目历史 | ~/.claude/projects/*/ | 陈旧的手动快照;被较新状态取代的会话记录 |
| 8 | 运行时缓存 | cache/, file-history/, logs/, shell-snapshots/ | 按大小和修改时间排序;提议超过 30 天且体积大的条目 |
工作流程
- 扫描所有通道(或用户指定的子集)。收集候选对象,包含:路径、通道、标记它的信号、大小、最后修改时间。
- 排序按置信度(损坏/孤立 = 高;仅陈旧 = 低),并以编号表格呈现。每次运行限制大约 20 个候选——垃圾回收是周期性的,不是穷举的。
- 逐一确认。 对每个候选显示证据,然后询问
[y/n/skip]。用户可以随时停止。 - 软删除已确认的条目:优先对技能/钩子进行
.disabled重命名,对文件进行_gc_trash/<date>/移动。权限条目存在于 JSON(无法添加注释):备份设置文件,在gc_log.md中逐字记录每个删除的条目,然后使用jq从allow数组中移除。仅在用户明确要求时才进行硬删除。 - 记录运行到
~/.claude/gc_log.md:时间戳、处理的项目、撤销说明。 - 报告:回收的大小、仍健康的通道、建议的下次审查日期。
示例扫描命令
孤立钩子脚本(通道 3)——存在于磁盘但未被任何钩子配置引用的脚本:
for f in ~/.claude/hooks/*; do
name=$(basename "$f")
grep -rq "$name" ~/.claude/settings.json ~/.claude/settings.local.json 2>/dev/null \
|| echo "孤立: $f"
done
冗余权限条目(通道 4)——重复以及被通配符覆盖的特定授权:
jq -r '.permissions.allow[]' ~/.claude/settings.local.json | sort | uniq -d
if jq -e '.permissions.allow | index("Bash(*)")' ~/.claude/settings.local.json >/dev/null; then
jq -r '.permissions.allow[]' ~/.claude/settings.local.json \
| grep '^Bash(' | grep -vF 'Bash(*)'
fi
最大陈旧缓存(通道 8)——使用 du -k 而非 GNU 专属的 find -printf,以便在 macOS/BSD 上也能工作:
find ~/.claude/file-history ~/.claude/shell-snapshots -type f -mtime +30 \
-exec du -k {} + 2>/dev/null | sort -rn | head -20
带撤销路径的软删除(一次记录日期,使日志与目录保持一致):
gc_date=$(date +%Y-%m-%d)
mkdir -p ~/.claude/_gc_trash/$gc_date
mv ~/.claude/skills/dead-skill ~/.claude/_gc_trash/$gc_date/
echo "$(date -Iseconds) moved skills/dead-skill -> _gc_trash/$gc_date/ (撤销: 移回)" >> ~/.claude/gc_log.md
移除已确认冗余的权限条目(JSON 无注释——备份、记录、然后编辑):
cp ~/.claude/settings.local.json ~/.claude/settings.local.json.bak
echo "$(date -Iseconds) removed permission entry: Bash(git push) (撤销: 从 .bak 恢复或重新添加)" >> ~/.claude/gc_log.md
jq '.permissions.allow -= ["Bash(git push)"]' ~/.claude/settings.local.json.bak \
> ~/.claude/settings.local.json
反模式
- 批量批准。 询问“全部删除这 15 个?[y/n]”违背了设计原则。一次一项,一次一个决定。
- 首次通过就硬删除。 如果没有
_gc_trash/副本或.disabled重命名,你就做错了。 - 把“旧”当成“死”。 一个 60 天未触及的技能可能是季节性的(报税季、季度审查)。年龄是一个信号,而不是判决——正因如此才需要人类确认。
- 通过截断来清理记忆。 合并两个矛盾的记忆文件需要阅读两者并保留较新的真相,而不是删除较长的那个。
- 触碰
~/.claude之外的任何内容(或项目的.claude/)。配置垃圾回收从不进入源代码树。
最佳实践
- 在大规模添加后运行,而不是仅按日历运行:安装 50 个技能包时正是与现有技能重叠出现的时候。
- 当两个技能重叠时,优先禁用触发描述较弱的那一个——因为它可能从未被触发过。
- 按每分钟花费的时间计算,权限清理是价值最高的通道:冗余的允许条目使安全审查更加困难。
- 永久保留
gc_log.md。它很小,而且“我什么时候禁用了那个钩子以及为什么”的出现频率比你想象的要高。
相关技能
skill-stocktake— 审计技能质量;config-gc 审计技能存在与否。在垃圾回收幸存者上运行 stocktake。workspace-surface-audit— 其补充部分:推荐安装什么。config-gc 是同一生命周期的减法部分。configure-ecc— 使用它安装技能后,运行 config-gc 以协调与现有配置的重叠。continuous-learning— 产生本技能稍后审计的记忆文件。security-review— 与权限通道配合良好。