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.

  • namememory-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

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/skills/memory-file-architect/SKILL.md

--- name: memory-file-architect description: Writes, restructures, and prunes CLAUDE.md memory files for Claude Code across user, project, nested-directory, and local scopes, deciding… license: MIT ---

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

ScopeLocationLoadedCommit?
OrganizationManaged policy CLAUDE.md (set by admins)Alwaysn/a
User~/.claude/CLAUDE.mdAlways, every projectNo
Project./CLAUDE.md or ./.claude/CLAUDE.mdAt startup, plus parent dirs up to the repo rootYes
Local./CLAUDE.local.mdAt startupNo (gitignore it)
Nested<subdir>/CLAUDE.mdWhen Claude reads or edits files in that subtreeYes
  • Imports: a line with @path/to/file.md pulls 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 with paths frontmatter. Use them to split a large root file by area if the installed version supports it; otherwise use nested CLAUDE.md files.
  • /memory opens the loaded memory files; /context shows how many tokens they cost.
  • /init drafts a CLAUDE.md from the repo. Treat its output as a first draft to prune, never as finished.

Where each kind of content belongs

ContentPut it inNot in CLAUDE.md because
Build, test, lint, run commands with exact flagsCLAUDE.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-explanatoryCLAUDE.md—
Package-specific commands and rules in a monorepoNested packages/x/CLAUDE.mdLoads only when working there
Multi-step procedures (release, migration, incident)SkillBody loads only when needed
Must happen every time (format on save, block .env edits)HookMemory is advice; hooks are enforced
Tool permissions, env vars, modelsettings.jsonConfiguration, not instructions
Personal preferences (tone, editor, “explain less”)~/.claude/CLAUDE.md or local fileShould not apply to teammates
Architecture narratives, onboarding prose, API docsdocs/, linked by pathToo long; Claude can read on demand
Style rules a formatter or linter enforcesThe linter configDuplicates 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”:

  1. Would Claude do this without being told? (“Write clean code”, “use meaningful names”)
  2. Does a tool already enforce it? (Prettier, ESLint, ruff, type checker)
  3. Is it stale? (path gone, command renamed, dependency removed)
  4. Is it only needed for one kind of task? Move it to a skill or docs/.
  5. Is it a must-always rule with no judgment? Move it to a hook.
  6. 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 by pnpm 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

  1. Rerun the audit script: zero errors, root file under the line target.
  2. 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.
  3. Run /context and confirm memory files are a small share of the window (a few thousand tokens at most for a typical repo).
  4. 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 171 lines · 7.0 KB
#!/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.

esc