Optimize claude config
Skill somnio-software/somnio-ai-tools/skills/optimize-claude-config
Audits and optimizes a repository's Claude Code configuration so path-scoped rules load lazily and CLAUDE.md stays a lightweight always-loaded index. First maps the real project tree (domains, monorepo package roots, extensions, generated/frozen areas) so every path glob is derived from and validated against actual files — stale globs that match zero files and over-broad ones are caught. Three areas: (1) migrate .claude/rules/**/*.md frontmatter to the native lazy-load `paths:` YAML-array form (away from eager `globs:` or red-flagged CSV `paths:`) with accurate, tree-validated patterns; (2) slim CLAUDE.md files to an index + rule map and delete redundant @import-only shells; (3) audit/install the PreToolUse(Write) hook that works around the read-not-create caveat. Audits first, shows a report, asks for confirmation, then applies. Pass --audit-only to skip applying.From its SKILL.md
npx -y skills add somnio-software/somnio-ai-tools --skill optimize-claude-configAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 4 stars4 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
14.9 KB, as published. Nobody here has run it
optimize-claude-config
Optimize the .claude/ config of a repo so rules load only when relevant and
CLAUDE.md stays small. Default mode audits, reports, asks to confirm, then
applies. --audit-only stops after the report.
Why this matters (canonical facts)
Verified empirically in Claude Code 2.1.145:
paths:as a YAML array lazy-loads a rule (it enters context only when a matching file is read) AND passes the IDE extension's schema validation. This is the target format.- CSV single-line
paths: a, blazy-loads at runtime but the IDE flags it red ("paths must be an array of glob patterns"). Migrate to the array form. YAML-array support landed in CC 2.1.84. globs: "..."(Cursor's field) loads eager at session start, ignoring scope. This is the main thing to fix — convert topaths:.alwaysApply:is a Cursor field, ignored by Claude Code — harmless noise. Leave it; do not churn files just to remove it.- A rule with no
paths:and noglobs:is effectively always-loaded. Flag it: either it genuinely belongs in CLAUDE.md, or it needs apaths:scope. - Read-not-create caveat (bug #23478): path-scoped rules trigger on read, not create. Creating a brand-new file that matches a rule, without first reading a neighbor, does NOT load that rule. The hook in area 3 backstops this.
- CLAUDE.md should be a lightweight, always-loaded index: project
overview + a rule map (pointers into
.claude/rules/*) + a short "Critical Rules" digest — NOT full rule bodies inlined. Per-directory CLAUDE.md files that are pure@importshells are redundant once rules self-load viapaths:.
Step 0 — Parse arguments
--audit-onlypresent → report only, never edit or write.- A trailing path arg → treat it as the repo root to scan; otherwise use the
current working directory. Resolve
.claude/under that root. - If
<root>/.claudedoes not exist, stop and tell the user there is nothing to optimize here.
Step 1 — Discover
Collect, under <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
Read each rule file's frontmatter, each CLAUDE.md, and the settings file(s).
Step 1b — Analyze project structure (derive paths from reality)
Do NOT guess path globs or trust existing ones blindly. Build a factual map of
the repo first, then derive/validate every paths: against it.
Enumerate the real tree (tracked files only, so build output and deps are excluded):
git ls-files 2>/dev/null || find . -type f -not -path '*/.git/*' -not -path '*/node_modules/*' -not -path '*/build/*'
From that file list, compute:
- Top-level domains — the first path segment of each file (e.g.
apps/,functions/,packages/). These are the coarse areas rules will scope to. - Monorepo package roots — directories containing a manifest
(
pubspec.yaml,package.json,Cargo.toml,go.mod,pyproject.toml). Nested packages (e.g.apps/mobile/packages/*/) change the right glob depth: preferapps/mobile/packages/*/lib/**/*.dartover a flat**/*.dart. - Extensions per domain — which file types live where (e.g.
.dartunderapps/,.tsunderfunctions/). This tells you the file-type tail of each glob (**/*.dartvs**/*.ts). - Generated / vendored / frozen areas — directories a rule should NOT match
(
*.g.dart,*.freezed.dart,build/,node_modules/,lib/build output, and anything the repo's CLAUDE.md marks frozen/deprecated). Exclude these from proposed globs.
Use a quick aggregation to see the shape, e.g.:
# 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)$'
Hold this map; Step 2 uses it to validate and derive globs.
Step 2 — Audit rules frontmatter
For every .claude/rules/**/*.md, classify its frontmatter:
| Finding | Condition | Proposed action |
|---|---|---|
| ✅ OK | paths: is a YAML array (one - pattern per line) | none |
🔴 eager globs: | has globs: (any form) | rename key to paths:, convert value to a YAML array (one item per line) |
🟡 CSV paths: | paths: value is inline / comma-separated on one line | reformat to a YAML array |
| 🟡 unscoped | no paths: and no globs: | derive a candidate scope from Step 1b and the rule's own domain, then propose it for the user to confirm. Do NOT auto-apply a scope. |
| 🟠 stale glob | a paths:/globs: pattern matches zero tracked files | flag it — the directory was renamed/removed, or the depth/extension is wrong. Propose the corrected pattern from the real tree. |
| 🟠 over-broad | a pattern matches generated/vendored/frozen files (Step 1b list) | tighten it (add the right package depth, or narrow the extension) so the rule only loads for hand-written source. |
Validate every pattern against the real tree. For each paths:/globs:
entry, check it actually matches files (reuse the glob→regex logic from the hook,
or git ls-files | grep with the glob translated). A pattern matching zero files
is the most common silent failure — the rule looks scoped but never loads.
Deriving a glob (for unscoped or stale rules). Use the Step 1b map:
- Identify the rule's domain from its
description:/body (e.g. "Flutter client", "Cloud Functions", "TypeScript") and map it to the real top-level domain(s) and extension(s) you found. - Respect monorepo package depth — emit
apps/mobile/packages/*/lib/**/*.dartwhen packages exist, not a flat**/*.dart. - Exclude generated/frozen areas; never scope a rule to
build/,node_modules/,*.g.dart, or a deprecated/frozen dir. - Present each derived glob with the count of files it matches so the user
can sanity-check (e.g.
apps/mobile/lib/**/*.dart → 142 files).
Conversion rules:
- When a pattern is already correct, preserve it verbatim — only change the key name and the YAML shape. Strip surrounding quotes the CSV form may have.
- Only rewrite a pattern's value when Step 1b proves it stale or over-broad, and show the before/after in the report.
- Keep
description:and anyalwaysApply:line unchanged. - Idempotent: a file already in the ✅ state with patterns that match real files gets no edit.
Target shape:
---
description: <unchanged>
alwaysApply: false # only if it was already there
paths:
- apps/mobile/lib/**/*.dart
- functions/**/*.ts
---
Step 3 — Audit CLAUDE.md slimming
For each CLAUDE.md found:
- Pure
@importshell — file body (after stripping blank lines/comments) is nothing but@import/@pathlines → redundant, because rules now self-load viapaths:. Propose deletion. Exception: keep it if a parentCLAUDE.mdrelies on importing it AND it carries unique prose. - Bloated index — a top-level CLAUDE.md that inlines full rule bodies that
already exist under
.claude/rules/. Propose replacing the duplicated section with a one-line pointer ((→ .claude/rules/<area>/<file>.md)), keeping only a short digest. Do NOT delete unique content — only de-duplicate what is verbatim-present in a rule file. - Healthy index — overview + rule map + short Critical Rules digest → leave it.
Be conservative: when unsure whether content is unique, flag it for the user rather than deleting. Never delete a CLAUDE.md that has hand-written guidance not covered by a rule.
Step 4 — Audit the read-not-create hook
Check two things:
.claude/settings.jsonregisters aPreToolUsehook with matcherWritepointing at.claude/hooks/inject-rules.py..claude/hooks/inject-rules.pyexists.
If either is missing, propose installing it. The canonical files are embedded
below — this skill is self-contained. Always reproduce them verbatim from
the blocks in this document. NEVER copy inject-rules.py (or any config) from
another repository, a sibling project, or a path on the host machine — there is
no "reference repo". The skill must behave identically for anyone who runs it,
including a colleague who only has this skill file.
.claude/settings.json (merge into existing hooks block; don't clobber
other hooks):
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write",
"hooks": [
{
"type": "command",
"command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/inject-rules.py\""
}
]
}
]
}
}
.claude/hooks/inject-rules.py — when absent, Write this exact content
(copy the block below character-for-character; do not fetch it from elsewhere):
#!/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()
If inject-rules.py exists but differs from the embedded canonical, report the
diff and ask before overwriting — the user may have customized it. When the user
confirms a sync, Write the embedded block above; do not pull the file from any
other location.
Step 5 — Report
Print one consolidated table grouped by area (Rules / CLAUDE.md / Hook), each row
being path · finding · proposed action. For any glob that changes or is
derived, show before → after with the file-match count so the scope is
verifiable at a glance (e.g. globs:"lib/**" → paths: apps/mobile/lib/**/*.dart (142 files)). End with a count summary, e.g. 3 rules to migrate · 1 stale glob fixed · 1 CLAUDE.md to delete · hook already installed.
If nothing needs changing, say so plainly and stop — the config is already optimal.
Step 6 — Confirm & apply
- If
--audit-only: stop after the report. - Otherwise ask the user to confirm (
yto apply all, or let them deselect specific rows). Then:- Edit rule frontmatter with
Edit(key rename + YAML-array reshape). - Delete redundant shells with
rm; de-duplicate bloated CLAUDE.md withEdit. - Install the hook (
Writethe script,Edit/Writesettings.json) only if that row was confirmed.
- Edit rule frontmatter with
- Never touch
unscopedrules or ambiguous CLAUDE.md content automatically — those are advisory flags for the user to resolve by hand.
Step 7 — Verify
After applying, tell the user how to confirm lazy-loading works:
- Run
/memory(or read a file matching a migrated rule'spaths:) and look for the⎿ Loaded …InstructionsLoaded message — the rule should appear only when a matching file is in context, not at session start. - The hook fires only on creating a new matching file; the additionalContext arrives after the first write, so the model self-corrects with a second write (create-then-fix). Net result: the new file ends up compliant.
Do not run git commit unless the user asks.