Config GC — Claude Code 配置垃圾回收
借鉴了运行时垃圾回收的概念:定期扫描那些不再被引用、冗余、过期或低价值的对象,并回收空间。关键区别在于:这里需要人工介入确认。绝不自动删除。
何时激活
- 用户要求清理、审计或精简他们的 Claude Code 配置
- 用户抱怨技能太多、钩子太嘈杂或会话启动缓慢
- 需要执行月度/定期配置审查
- 安装大型技能包后(例如本仓库),协调与现有设置的重叠
不要激活用于:清理项目源代码(那是重构)、清除聊天历史、或卸载 Claude Code 本身。
设计理念
- 仅追加的配置会泄漏。 技能、记忆文件、钩子和权限条目只增不减。没有定期审查,它们就会悄悄腐烂。
- 定期审计胜过一次大清理。 每 30 天左右扫描一次,每次只提出一小批候选。
- 不同渠道不同策略。 每种累积类型(技能、钩子、权限……)都有自己的过期信号——不要对所有应用同一规则。
- 先软删除。 重命名为
.disabled> 移动到~/.claude/_gc_trash/> 真正删除。始终保留撤销路径。 - 强制人工参与。 每个候选对象都有自己的
[y/n/skip]确认。没有“全部同意”的快捷方式。 - 保留日志。 每次 GC 运行都会附加到
~/.claude/gc_log.md:记录了处理了什么、为什么以及如何撤销。
扫描渠道
| # | 渠道 | 路径 | 过期/冗余信号 |
|---|---|---|---|
| 1 | 技能 | ~/.claude/skills/*/ | 名称高度重叠;在最近对话中从未触发;与用户实际工作领域不匹配;SKILL.md 损坏或为空 |
| 2 | 记忆 | ~/.claude/**/memory/*.md + 其索引 | 同一主题有多个索引条目;内容与较新条目矛盾;日期已过;索引中缺少孤立文件;应合并的 100 字以下的片段 |
| 3 | 钩子 | ~/.claude/hooks/ + 设置 | 磁盘上存在脚本但无任何钩子配置引用;旧版本已被重写覆盖 |
| 4 | 权限 | settings.json / settings.local.json 中的 permissions.allow | 重复条目;已被通配符覆盖的特定条目(例如已允许 Bash(*) 时还有 Bash(git push));过去实验的一次性授权 |
| 5 | MCP 服务器 | ~/.claude.json 或项目的 .mcp.json | 无法连接的服务器;功能重复的服务器;长期未使用 |
| 6 | 计划提醒/任务 | 用户存放它们的任何位置 | 已触发的一次性任务超过 30 天;目标脚本已不存在的任务 |
| 7 | 项目历史 | ~/.claude/projects/*/ | 过时的交接快照;被较新状态取代的会话记录 |
| 8 | 运行时缓存 | cache/、file-history/、logs/、shell-snapshots/ | 按大小和修改时间排序;建议超过 30 天且较大的项目 |
工作流
- 扫描 所有渠道(或用户指定的子集)。收集候选信息,包括:路径、渠道、标记它的信号、大小、最后修改时间。
- 排序 按置信度(损坏/孤立 = 高;仅过时 = 低),并以编号表格形式呈现。每次运行限制约 20 个候选——GC 是周期性的,不是穷举的。
- 逐个确认。 对于每个候选,展示证据,然后询问
[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 "ORPHAN: $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/ (undo: mv back)" >> ~/.claude/gc_log.md
移除已确认冗余的权限条目(JSON 无法添加注释——先备份、记录、再编辑):
cp ~/.claude/settings.local.json ~/.claude/settings.local.json.bak
echo "$(date -Iseconds) removed permission entry: Bash(git push) (undo: restore from .bak or re-add)" >> ~/.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/)。Config GC 从不进入源代码树。
最佳实践
- 在大型添加之后运行,而不是仅仅按日历:安装 50 个技能包时,正是与现有技能重叠出现的时候。
- 当两个技能重叠时,优先禁用触发描述较弱的那一个——它可能是那个本来就不怎么触发的。
- 权限清理是每分钟花费中价值最高的渠道:冗余的允许条目会让安全审查变得更困难。
- 永远保留
gc_log.md。它很小,而且“我什么时候禁用了那个钩子以及为什么”这个问题比想象中更常出现。
相关技能
skill-stocktake— 审计技能质量;config-gc 审计技能存在性。在 GC 幸存的技能上运行 stocktake。workspace-surface-audit— 其增加性对应物:推荐要安装什么。config-gc 是同一生命周期的减少性一半。configure-ecc— 在使用它安装技能后,运行 config-gc 来协调与你先前设置的重叠。continuous-learning— 产生本技能稍后审计的记忆文件。security-review— 与权限渠道搭配良好。