CLAUDE.md Architect skill for Claude Code
Short memory files that Claude actually follows. Rebuilds CLAUDE.md around one rule: every line is loaded on every task, so it must be a command, a non-obvious convention, or a trap. Claude audits all memory scopes, pulls commands from package scripts and CI instead of guessing, prunes generic advice and linter-enforced style, and moves procedures into skills and must-always rules into hooks. An audit script catches broken imports, stale paths and leaked secrets.
- name
memory-file-architect - size179 lines · ≈2,050 tokens
- files2
- licenseMIT
- updated
What Claude sees before loading it
This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.
description: Writes, restructures, and prunes CLAUDE.md memory files for Claude Code across user, project, nested-directory, and local scopes, deciding what belongs in memory versus skills, hooks, settings, or docs, and keeping every file short. Use when creating or cleaning up a CLAUDE.md, after running /init, when Claude keeps ignoring project conventions, when memory files have grown long or stale, or when setting up a monorepo with per-package instructions. Try asking
- Our CLAUDE.md is 400 lines and Claude still ignores half of it — fix it
- Clean up what /init generated so it only has what matters
- Set up CLAUDE.md files for this pnpm monorepo with per-package rules
Use it when
- Starting a CLAUDE.md for a repo or refining /init output
- Memory files have grown long, stale, or contradictory
- Claude keeps missing a project convention or command
- A monorepo needs package-specific instructions
Skip it when
- The rule must be enforced with no exceptions — use hooks-automation instead
- You need a reusable multi-step procedure — use skill-authoring instead
- Tracking progress on one long task — use session-handoff-notes instead
How to check the result
A fresh session answers how to run a single test and which areas are off limits straight from memory, and the audit script reports no broken imports or stale paths.
The common mistake it prevents
Appending a new line after every mistake until the file is hundreds of lines long and the rules that matter are lost in generic advice.
CLAUDE.md is loaded into every session, so every line is paid for on every task. A good one is short (root file under ~150 lines, hard ceiling 300), contains only what Claude cannot infer from the code, and every line is a command, a non-obvious convention, or a trap. If Claude would do it anyway, delete the line.
Memory locations
| Scope | Location | Loaded | Commit? |
|---|---|---|---|
| Organization | Managed policy CLAUDE.md (set by admins) | Always | n/a |
| User | ~/.claude/CLAUDE.md | Always, every project | No |
| Project | ./CLAUDE.md or ./.claude/CLAUDE.md | At startup, plus parent dirs up to the repo root | Yes |
| Local | ./CLAUDE.local.md | At startup | No (gitignore it) |
| Nested | <subdir>/CLAUDE.md | When Claude reads or edits files in that subtree | Yes |
- Imports: a line with
@path/to/file.mdpulls that file in (relative to the importing file;~/works; chains are capped at a few hops). Imports load eagerly, so importing a 600-line doc costs 600 lines every session. Import small, stable files only. - Recent versions also read modular rule files from
.claude/rules/*.md, optionally scoped withpathsfrontmatter. Use them to split a large root file by area if the installed version supports it; otherwise use nested CLAUDE.md files. /memoryopens the loaded memory files;/contextshows how many tokens they cost./initdrafts a CLAUDE.md from the repo. Treat its output as a first draft to prune, never as finished.
Where each kind of content belongs
| Content | Put it in | Not in CLAUDE.md because |
|---|---|---|
| Build, test, lint, run commands with exact flags | CLAUDE.md | — |
Non-obvious conventions (“money is integer cents”, “never import from legacy/”) | CLAUDE.md | — |
| Repo map: only the 5-10 directories that are not self-explanatory | CLAUDE.md | — |
| Package-specific commands and rules in a monorepo | Nested packages/x/CLAUDE.md | Loads only when working there |
| Multi-step procedures (release, migration, incident) | Skill | Body loads only when needed |
Must happen every time (format on save, block .env edits) | Hook | Memory is advice; hooks are enforced |
| Tool permissions, env vars, model | settings.json | Configuration, not instructions |
| Personal preferences (tone, editor, “explain less”) | ~/.claude/CLAUDE.md or local file | Should not apply to teammates |
| Architecture narratives, onboarding prose, API docs | docs/, linked by path | Too long; Claude can read on demand |
| Style rules a formatter or linter enforces | The linter config | Duplicates the tool and drifts |
Workflow
- [ ] 1. Audit what exists (all scopes, sizes, imports, stale paths)
- [ ] 2. Collect facts from the repo, not from memory
- [ ] 3. Draft or restructure using the template
- [ ] 4. Prune line by line
- [ ] 5. Split by scope (nested files, skills, hooks)
- [ ] 6. Verify in a fresh session
1. Audit
python3 ${CLAUDE_SKILL_DIR}/scripts/audit_claude_md.py . [--max-lines 150] [--no-user]
It finds every CLAUDE.md, CLAUDE.local.md, and .claude/rules/*.md under the repo (plus
the user file unless --no-user), reports lines and approximate tokens including
imports, flags broken @imports, backticked paths that no longer exist, generic filler
phrases, lines duplicated across files, secret-looking strings, and an un-ignored
CLAUDE.local.md. Exit code 1 means errors (broken imports, secrets, files far over the
limit).
2. Collect facts from the repo
Read the sources of truth instead of guessing:
cat package.json | jq .scripts # or Makefile, justfile, pyproject.toml [tool.*], Taskfile.yml
ls .github/workflows/ && sed -n '1,80p' .github/workflows/ci.yml # what CI actually runs
git log --oneline -20 # commit message conventions in use
ls -a # config files hinting at tools (ruff, biome, turbo)
Ask the user for the things the repo cannot tell you: forbidden areas, deploy rules, review expectations, recurring mistakes Claude has made.
3. Template
# <Project name>
<One sentence: what this is and the main stack.>
## Commands
- Install: `pnpm install`
- Dev server: `pnpm dev` (port 3000)
- Test one file: `pnpm vitest run path/to/file.test.ts`
- Full check before committing: `pnpm lint && pnpm typecheck && pnpm test`
## Layout
- `apps/web/` Next.js app; `packages/db/` Drizzle schema and migrations
- `legacy/` is frozen: read it, never edit it
## Conventions
- Money is integer cents (`amountCents`); never floats
- New API routes validate input with the Zod schemas in `packages/schemas/`
- Migrations: generate with `pnpm db:generate`; never hand-edit applied migrations
## Gotchas
- Tests share one Postgres; run `pnpm db:reset:test` if fixtures look stale
- `NEXT_PUBLIC_*` vars are inlined at build time; changing them needs a rebuild
## Workflow
- Branch from `main`; conventional commit messages (`feat:`, `fix:`)
- Ask before running anything that touches `infra/` or production data
4. Prune line by line
For each line, ask in order and delete on the first “yes”:
- Would Claude do this without being told? (“Write clean code”, “use meaningful names”)
- Does a tool already enforce it? (Prettier, ESLint, ruff, type checker)
- Is it stale? (path gone, command renamed, dependency removed)
- Is it only needed for one kind of task? Move it to a skill or
docs/. - Is it a must-always rule with no judgment? Move it to a hook.
- Is it a personal preference? Move it to the user or local file.
Then tighten what remains:
- Commands over prose: “
pnpm test -- --run” beats “run the tests in non-watch mode”. - One fact per bullet. No paragraphs.
- Use emphasis (
IMPORTANT,NEVER) on at most two or three lines. When everything is emphasized, nothing is. - Say the reason when a rule looks arbitrary (“never edit
generated/; it is overwritten bypnpm codegen”). Claude generalizes better from reasons.
5. Split by scope
- Monorepo: root file holds cross-cutting commands and rules; each package gets a nested CLAUDE.md with its own commands and traps. Do not repeat root content in nested files.
- Long procedures move into project skills under
.claude/skills/. - Repeated “after editing X always run Y” lines become PostToolUse hooks (see
hooks-automation). - Keep CLAUDE.local.md for machine-specific facts (local ports, sandbox URLs) and make sure it is gitignored.
Gotchas
- Listing every file or directory. It rots within weeks and Claude can run
ls. - Copying the README. Link it by path if needed; do not import it.
- Contradictions between root and nested files. The nested file usually wins in practice, which surprises whoever edits the root.
- Secrets, tokens, internal hostnames with credentials. Memory files are committed and sent to the model every session.
- Treating CLAUDE.md as enforcement. If a violation is costly, add a hook or a permission deny rule as well.
- Growing the file after every mistake. Add a line only for mistakes that recur; review and prune monthly.
Verification
- Rerun the audit script: zero errors, root file under the line target.
- Start a fresh session and ask: “How do I run a single test here, and what must I never edit?” The answer must come from the file, correctly, without exploration.
- Run
/contextand confirm memory files are a small share of the window (a few thousand tokens at most for a typical repo). - Give Claude one small real task in the area a rule covers and check the rule is followed.
Output
**Memory files:** ./CLAUDE.md 212 -> 96 lines; packages/api/CLAUDE.md (new, 31 lines)
**Moved out:** release procedure -> .claude/skills/release/; "always run prettier" -> PostToolUse hook
**Deleted:** 41 lines (generic advice, linter-enforced style, 3 stale paths)
**Fixed:** broken @docs/setup.md import; removed an API token from CLAUDE.local.md (rotate it)
**Check:** fresh session answered test/forbidden-dir questions correctly from memory Bundled files
Loaded only when a step in the skill needs them. Review them here before installing.
scripts/audit_claude_md.py
#!/usr/bin/env python3
"""Audit Claude Code memory files (CLAUDE.md, CLAUDE.local.md, .claude/rules/*.md).
Usage:
python3 audit_claude_md.py [repo_root] [--max-lines N] [--no-user]
Reports per file: lines, approximate tokens (own + imports), broken @imports,
backticked paths that do not exist, generic filler phrases, secret-looking strings,
lines duplicated across files, and whether CLAUDE.local.md is gitignored.
Exit code 1 on errors (broken imports, secret-like strings, a file over twice the line
limit), else 0. Standard library only.
"""
from __future__ import annotations
import argparse
import re
import subprocess
import sys
from pathlib import Path
SKIP_DIRS = {".git", "node_modules", "vendor", ".venv", "venv", "dist", "build", ".next", "target", "__pycache__"}
GENERIC = [
"clean code", "best practices", "meaningful names", "well-documented", "readable code",
"follow conventions", "write tests for", "be careful", "high quality", "maintainable",
"keep it simple", "dry principle", "solid principles",
]
SECRET_RES = [
re.compile(r"sk-[A-Za-z0-9_-]{20,}"), re.compile(r"AKIA[0-9A-Z]{16}"),
re.compile(r"gh[pousr]_[A-Za-z0-9]{30,}"), re.compile(r"xox[baprs]-[A-Za-z0-9-]{10,}"),
re.compile(r"-----BEGIN [A-Z ]*PRIVATE KEY-----"),
re.compile(r"(?i)(password|secret|api[_-]?key|token)\s*[:=]\s*['\"]?[A-Za-z0-9/+_-]{12,}"),
]
IMPORT_RE = re.compile(r"(?:^|\s)@((?:~/|\.{0,2}/)?[\w.-]+(?:/[\w.-]+)*\.\w+)")
CODE_SPAN_RE = re.compile(r"`([^`\n]+)`")
def strip_code(text: str) -> str:
text = re.sub(r"```.*?```", "", text, flags=re.S)
return CODE_SPAN_RE.sub("", text)
def find_files(root: Path, include_user: bool) -> list[Path]:
found: list[Path] = []
for p in root.rglob("*"):
if any(part in SKIP_DIRS for part in p.relative_to(root).parts[:-1]):
continue
if not p.is_file():
continue
rel = p.relative_to(root).as_posix()
if p.name in ("CLAUDE.md", "CLAUDE.local.md") or re.search(r"(^|/)\.claude/rules/.+\.md$", rel):
found.append(p)
user = Path.home() / ".claude" / "CLAUDE.md"
if include_user and user.is_file():
found.append(user)
return sorted(set(found))
def resolve_import(src: Path, target: str) -> Path:
if target.startswith("~/"):
return Path.home() / target[2:]
return (src.parent / target).resolve()
def import_tokens(path: Path, seen: set[Path], depth: int = 0) -> tuple[int, list[str]]:
"""Return (tokens of imported files, list of broken import descriptions)."""
if depth > 5:
return 0, [f"{path}: import chain deeper than 5"]
total, broken = 0, []
for target in IMPORT_RE.findall(strip_code(path.read_text(encoding="utf-8", errors="replace"))):
dest = resolve_import(path, target)
if not dest.is_file():
broken.append(f"@{target}")
continue
if dest in seen:
continue
seen.add(dest)
total += len(dest.read_text(encoding="utf-8", errors="replace")) // 4
sub_total, sub_broken = import_tokens(dest, seen, depth + 1)
total += sub_total
broken += sub_broken
return total, broken
def looks_like_path(s: str) -> bool:
if " " in s or s.startswith(("http", "$", "-", "@")) or any(c in s for c in "*<>{}|=()"):
return False
return "/" in s and (re.search(r"\.\w{1,6}$", s) is not None or s.endswith("/"))
def is_ignored(root: Path, path: Path) -> bool | None:
try:
r = subprocess.run(["git", "-C", str(root), "check-ignore", "-q", str(path)], capture_output=True)
return r.returncode == 0 if r.returncode in (0, 1) else None
except FileNotFoundError:
return None
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("root", nargs="?", default=".")
ap.add_argument("--max-lines", type=int, default=150)
ap.add_argument("--no-user", action="store_true")
args = ap.parse_args()
root = Path(args.root).resolve()
files = find_files(root, not args.no_user)
if not files:
print(f"No memory files found under {root}. Start from /init, then prune.")
return 0
errors: list[str] = []
warnings: list[str] = []
line_owner: dict[str, Path] = {}
grand = 0
print(f"{'lines':>5} {'tokens':>7} {'+import':>7} file")
for f in files:
text = f.read_text(encoding="utf-8", errors="replace")
lines = text.splitlines()
own = len(text) // 4
imp, broken = import_tokens(f, {f})
grand += own + imp
label = f.relative_to(root).as_posix() if root in f.parents else str(f)
print(f"{len(lines):>5} {own:>7} {imp:>7} {label}")
for b in broken:
errors.append(f"{label}: broken import {b}")
limit = args.max_lines if f.name == "CLAUDE.md" and f.parent in (root, root / ".claude") else args.max_lines * 2 // 3
if len(lines) > 2 * args.max_lines:
errors.append(f"{label}: {len(lines)} lines (more than twice the {args.max_lines}-line target)")
elif len(lines) > limit:
warnings.append(f"{label}: {len(lines)} lines (target <= {limit})")
if imp > 2000:
warnings.append(f"{label}: imports add ~{imp} tokens to every session")
lower = text.lower()
for phrase in GENERIC:
if phrase in lower:
warnings.append(f"{label}: generic phrase '{phrase}' (Claude does this anyway; delete or make concrete)")
for rx in SECRET_RES:
if rx.search(text):
errors.append(f"{label}: secret-like string matching {rx.pattern[:30]}... (remove and rotate)")
for span in CODE_SPAN_RE.findall(text):
if looks_like_path(span):
candidates = [root / span.lstrip("./"), f.parent / span]
if not any(c.exists() for c in candidates):
warnings.append(f"{label}: referenced path `{span}` not found (stale?)")
emph = len(re.findall(r"\b(IMPORTANT|MUST|NEVER|ALWAYS|CRITICAL)\b", text))
if emph > 5:
warnings.append(f"{label}: {emph} emphasis words; keep emphasis for 2-3 rules")
for ln in lines:
norm = re.sub(r"\s+", " ", ln.strip(" -*#>").lower())
if len(norm) < 25:
continue
if norm in line_owner and line_owner[norm] != f:
warnings.append(f"{label}: duplicates a line in {line_owner[norm].name}: '{norm[:60]}'")
line_owner.setdefault(norm, f)
if f.name == "CLAUDE.local.md" and is_ignored(root, f) is False:
errors.append(f"{label}: CLAUDE.local.md is not gitignored")
print(f"\nTotal: ~{grand} tokens across {len(files)} files (own text + imports)")
print("(nested CLAUDE.md files load only when Claude works in that directory)")
for e in errors:
print(f"ERROR {e}")
for w in warnings:
print(f"WARNING {w}")
print(f"Result: {'FAIL' if errors else 'PASS'} ({len(errors)} errors, {len(warnings)} warnings)")
return 1 if errors else 0
if __name__ == "__main__":
sys.exit(main())
About this skill
Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.