关于 optimize-claude-config
用于审计和优化仓库的 `.claude/` 配置,让规则只在相关上下文中加载,并保持 `CLAUDE.md` 简洁。它会先报告待确认的更改再应用,也支持仅审计模式。
主要功能
- 01审计规则前置元数据与路径 glob
- 02根据已跟踪文件验证 glob
- 03检查并精简 `CLAUDE.md` 文件
- 04检查规则注入钩子
- 05确认后再应用更改
适用场景
- 01将 Cursor 风格的 `globs:` 规则迁移为 `paths:`
- 02查找匹配不到文件的规则路径模式
- 03识别重复或仅含导入的 `CLAUDE.md`
- 04检查新建匹配文件时的钩子设置
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ somnio-software/ somnio-ai-tools --skill optimize-claude-config安装后在 Claude Code 中输入/optimize-claude-config确认已加载。
详细介绍
optimize-claude-config
优化仓库的 .claude/ 配置,让规则仅在相关时加载,并保持 CLAUDE.md 精简。默认模式会先审查、报告并请求确认,然后应用更改。--audit-only 会在报告后停止。
为什么这很重要(权威事实)
已在 Claude Code 2.1.145 中通过实证验证:
- 作为 YAML 数组的
paths:会延迟加载规则(仅在读取匹配文件时才进入上下文),并通过 IDE 扩展的架构验证。这是目标格式。 - CSV 单行格式的
paths: a, b在运行时会延迟加载,但 IDE 会将其标红("paths must be an array of glob patterns")。请迁移为数组格式。YAML 数组支持在 CC 2.1.84 中引入。 globs: "..."(Cursor 的字段)会在会话开始时立即加载,不考虑作用范围。这是主要需要修复的问题——转换为paths:。alwaysApply:是 Cursor 的字段,Claude Code 会忽略它——这是无害的冗余。保留即可;不要仅仅为了删除它而改动文件。- 没有
paths:且没有globs:的规则实际上会始终加载。请标记它:它要么确实应该放在 CLAUDE.md 中,要么需要设置paths:作用范围。 - 仅读取、不在创建时触发的注意事项(错误 #23478): 路径作用范围规则会在读取文件时触发,而非创建文件时触发。如果创建一个匹配规则的新文件,却没有先读取相邻文件,该规则就不会加载。区域 3 中的钩子会兜底处理。
- CLAUDE.md 应是一个轻量、始终加载的索引:项目概览 + 规则映射(指向
.claude/rules/*)+ 简短的“关键规则”摘要,而不是内联完整规则正文。通过paths:实现规则自行加载后,仅包含纯@import的目录级 CLAUDE.md 文件便是多余的。
步骤 0 — 解析参数
- 存在
--audit-only→ 仅报告,不编辑或写入。 - 末尾的路径参数 → 将其视为要扫描的仓库根目录;否则使用当前工作目录。在该根目录下解析
.claude/。 - 如果
<root>/.claude不存在,则停止并告知用户此处没有需要优化的内容。
步骤 1 — 发现
收集 <root> 下的以下内容:
find .claude/rules -type f -name '*.md' 2>/dev/null
find . -name 'CLAUDE.md' -not -path '*/node_modules/*' 2>/dev/null
ls .claude/settings.json .claude/settings.local.json 2>/dev/null
ls .claude/hooks/inject-rules.py 2>/dev/null
读取每个规则文件的 frontmatter、每个 CLAUDE.md 文件和设置文件。
步骤 1b — 分析项目结构(根据实际情况推导路径)
不要猜测路径 glob,也不要盲目信任已有配置。先构建仓库的事实映射,再据此推导并验证每个 paths:。
枚举实际文件树(仅限已跟踪文件,因此不包含构建产物和依赖):
git ls-files 2>/dev/null || find . -type f -not -path '*/.git/*' -not -path '*/node_modules/*' -not -path '*/build/*'
根据文件列表计算:
- 顶层领域 — 每个文件的第一个路径段(例如
apps/、functions/、packages/)。这些是规则设置作用范围时使用的粗粒度区域。 - Monorepo 包根目录 — 包含清单文件的目录(
pubspec.yaml、package.json、Cargo.toml、go.mod、pyproject.toml)。嵌套包(例如apps/mobile/packages/*/)会影响合适的 glob 深度:优先使用apps/mobile/packages/*/lib/**/*.dart,而不是扁平的**/*.dart。 - 各领域中的扩展名 — 不同位置包含哪些文件类型(例如
apps/下的.dart、functions/下的.ts)。据此确定每个 glob 的文件类型结尾(**/*.dart对比**/*.ts)。 - 生成、供应商提供或冻结的区域 — 规则不应匹配的目录(
*.g.dart、*.freezed.dart、build/、node_modules/、lib/中的构建产物,以及仓库的 CLAUDE.md 标记为冻结或已弃用的任何内容)。从建议的 glob 中排除这些区域。
使用快速汇总查看结构,例如:
# extensions by top-level domain
git ls-files | awk -F/ '{d=$1; n=split($NF,a,"."); print d" *."a[n]}' | sort | uniq -c | sort -rn | head -40
# package roots
git ls-files | grep -E '/(pubspec\.yaml|package\.json|Cargo\.toml|go\.mod|pyproject\.toml)$'
保留此映射;步骤 2 将用它验证并推导 glob。
步骤 2 — 审查规则 frontmatter
对于每个 .claude/rules/**/*.md,对其 frontmatter 进行分类:
| 发现 | 条件 | 建议操作 |
|---|---|---|
| ✅ 正常 | paths: 是 YAML 数组(每行一个 - pattern) | 无 |
🔴 过早加载的 globs: | 存在 globs:(任何形式) | 将键名改为 paths:,并将值转换为 YAML 数组(每行一个条目) |
🟡 CSV 格式的 paths: | paths: 的值是行内格式/逗号分隔的单行内容 | 重新格式化为 YAML 数组 |
| 🟡 未限定范围 | 没有 paths:,也没有 globs: | 根据步骤 1b 和规则自身的领域推导候选范围,然后提交给用户确认。不要自动应用范围。 |
| 🟠 过时的 glob | paths:/globs: 模式未匹配到任何已跟踪文件 | 标记此项 — 目录可能已重命名/移除,或深度/扩展名有误。根据实际目录树提出修正后的模式。 |
| 🟠 范围过宽 | 模式匹配到生成文件/第三方文件/冻结文件(步骤 1b 列表) | 收窄范围(添加正确的包目录深度,或缩小扩展名范围),确保规则只对手写源文件加载。 |
针对实际目录树验证每个模式。 对于每个 paths:/globs:
条目,检查它是否确实匹配文件(复用钩子中的 glob→regex 逻辑,
或将 glob 转换后使用 git ls-files | grep)。匹配到零个文件的模式
是最常见的无声失效 — 规则看似限定了范围,实际上却从未加载。
推导 glob(针对未限定范围或过时的规则)。 使用步骤 1b 的映射:
- 根据规则的
description:/正文识别其领域(例如“Flutter 客户端”、“Cloud Functions”、“TypeScript”),并映射到发现的实际顶层 领域和扩展名。 - 遵循单体仓库的包目录深度 — 存在包时,使用
apps/mobile/packages/*/lib/**/*.dart, 而不是扁平的**/*.dart。 - 排除生成/冻结区域;绝不要将规则限定到
build/、node_modules/、*.g.dart或已弃用/冻结的目录。 - 为用户展示每个推导出的 glob 及其匹配的文件数量,供其检查
(例如
apps/mobile/lib/**/*.dart → 142 files)。
转换规则:
- 模式已正确时,原样保留 — 只更改键名和 YAML 格式。去掉 CSV 格式中 可能存在的外层引号。
- 只有步骤 1b 证实模式已过时或范围过宽时,才重写模式的值,并在 报告中展示修改前后的内容。
description:和任何alwaysApply:行保持不变。- 幂等:文件已处于 ✅ 状态且模式匹配实际文件时,不做任何修改。
目标格式:
---
description: <unchanged>
alwaysApply: false # only if it was already there
paths:
- apps/mobile/lib/**/*.dart
- functions/**/*.ts
---
步骤 3 — 审查 CLAUDE.md 精简情况
对于找到的每个 CLAUDE.md:
- 纯
@import外壳 — 文件正文(去除空行/注释后)只有@import/@path行 → 冗余,因为规则现在会通过paths:自行 加载。建议删除。例外:如果父级CLAUDE.md依赖导入此文件,且此文件 包含独有的文字内容,则保留。 - 臃肿的索引 — 顶层 CLAUDE.md 内联了
.claude/rules/下已有规则的 完整正文。建议将重复部分替换为单行指引((→ .claude/rules/<area>/<file>.md)), 仅保留简短摘要。不要删除独有内容 — 只去重规则文件中逐字存在的内容。 - 健康的索引 — 概览 + 规则映射 + 简短的关键规则摘要 → 保持不变。
谨慎处理:无法确定内容是否独有时,请标记并交由用户处理,而不是删除。
绝不要删除包含规则未涵盖的手写指导内容的 CLAUDE.md。
第 4 步 — 审查只读不创建钩子
检查以下两项:
.claude/settings.json注册了一个PreToolUse钩子,匹配器为Write, 指向.claude/hooks/inject-rules.py。.claude/hooks/inject-rules.py存在。
如果缺少其中任意一项,建议安装。规范文件内嵌如下——此技能是自包含的。始终从本文档中的代码块逐字复现这些文件。绝不要从其他仓库、同级项目或主机上的路径复制 inject-rules.py(或任何配置)——不存在“参考仓库”。无论是谁运行此技能,它的行为都必须相同,包括只有此技能文件的同事。
.claude/settings.json(合并到现有的 hooks 块中;不要覆盖其他钩子):
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write",
"hooks": [
{
"type": "command",
"command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/inject-rules.py\""
}
]
}
]
}
}
.claude/hooks/inject-rules.py——如果文件不存在,就 Write 以下确切内容(逐字符复制下方代码块;不要从其他位置获取):
#!/usr/bin/env python3
"""PreToolUse(Write) hook: inject path-scoped rules when creating a new file.
Works around the read-not-create caveat (#23478): `.claude/rules/*.md` files
with `paths:` frontmatter load on read, not on create. When Claude creates a
NEW file matching a rule's paths, this injects that rule's body as
additionalContext so it is in context before the file is written.
Fires only for: Write to a file that does NOT yet exist AND matches at least
one rule's `paths:` patterns. Otherwise it exits silently.
"""
import json
import sys
import os
import re
import glob
def glob_to_regex(pat):
out, i = ['^'], 0
while i < len(pat):
if pat[i:i + 3] == '**/':
out.append('(?:.*/)?')
i += 3
elif pat[i:i + 2] == '**':
out.append('.*')
i += 2
elif pat[i] == '*':
out.append('[^/]*')
i += 1
elif pat[i] == '?':
out.append('[^/]')
i += 1
else:
out.append(re.escape(pat[i]))
i += 1
out.append('$')
return re.compile(''.join(out))
def parse_paths(frontmatter):
paths, in_paths = [], False
for line in frontmatter.splitlines():
if re.match(r'^paths:\s*$', line):
in_paths = True
continue
if in_paths:
m = re.match(r'^\s*-\s*(.+?)\s*$', line)
if m:
paths.append(m.group(1))
elif line.strip() and not line[0].isspace():
in_paths = False
return paths
def main():
try:
data = json.load(sys.stdin)
except Exception:
return
proj = os.environ.get('CLAUDE_PROJECT_DIR') or data.get('cwd') or os.getcwd()
file_path = (data.get('tool_input') or {}).get('file_path', '')
if not file_path:
return
abs_fp = file_path if os.path.isabs(file_path) else os.path.join(proj, file_path)
# Only the caveat case: a brand-new file.
if os.path.exists(abs_fp):
return
rel = os.path.relpath(abs_fp, proj)
chunks = []
pattern = os.path.join(proj, '.claude', 'rules', '**', '*.md')
for rule_file in glob.glob(pattern, recursive=True):
try:
txt = open(rule_file, encoding='utf-8').read()
except Exception:
continue
if not txt.startswith('---'):
continue
end = txt.find('\n---', 3)
if end == -1:
continue
frontmatter, body = txt[3:end], txt[end + 4:]
if any(glob_to_regex(p).match(rel) for p in parse_paths(frontmatter)):
chunks.append(body.strip())
if not chunks:
return
context = (
'The file you are about to create (`' + rel + '`) matches path-scoped '
'rules that were NOT yet loaded into context (they load on read, not on '
'create). Apply the following rule(s) to the content you write now:\n\n'
+ '\n\n---\n\n'.join(chunks)
)
print(json.dumps({
'hookSpecificOutput': {
'hookEventName': 'PreToolUse',
'additionalContext': context,
}
}))
main()
如果 inject-rules.py 存在,但与内嵌的规范版本不同,请报告差异并在覆盖前征求确认——用户可能对它做了自定义修改。用户确认同步后,Write 上方内嵌的代码块;不要从任何其他位置获取该文件。
第 5 步 — 报告
按区域(Rules / CLAUDE.md / Hook)分组,打印一张汇总表,每行包含 path · finding · proposed action。对于任何发生变化或推导得出的 glob,展示变更前 → 变更后,并附上匹配的文件数量,以便一目了然地核实影响范围(例如:globs:"lib/**" → paths: apps/mobile/lib/**/*.dart (142 files))。最后给出数量汇总,例如:3 条规则待迁移 · 1 个过时 glob 已修复 · 1 个 CLAUDE.md 待删除 · 钩子已安装。
如果无需更改,请明确说明并停止——当前配置已是最优状态。
第 6 步 — 确认并应用
- 如果使用了
--audit-only:在报告后停止。 - 否则,询问用户是否确认(输入
y应用全部更改,也可以让用户取消选择特定行)。然后:- 使用
Edit编辑规则 frontmatter(重命名键并将其重塑为 YAML 数组)。 - 使用
rm删除多余的壳文件;使用Edit删除臃肿CLAUDE.md中的重复内容。 - 仅在对应行已获确认时,安装钩子(
Write脚本,使用Edit/Write修改 settings.json)。
- 使用
- 绝不要自动处理
unscoped规则或含义不明确的 CLAUDE.md 内容——这些仅是提示标记,需由用户手动解决。
第 7 步 — 验证
应用更改后,告知用户如何确认延迟加载有效:
- 运行
/memory(或读取与已迁移规则的paths:匹配的文件),并查找⎿ Loaded …InstructionsLoaded 消息——只有当匹配的文件位于上下文中时,规则才应出现,而不是在会话开始时出现。 - 钩子只会在创建新的匹配文件时触发;additionalContext 会在首次写入后到达,因此模型会通过第二次写入自行修正(先创建,再修复)。最终,新文件会符合要求。
除非用户提出要求,否则不要运行 git commit。