"""context-files-claude-md-agents-md-cursor-rules.py — audit the agent context files in a repository. What it does: walks a directory tree and reports every file a coding agent would read as standing instructions: CLAUDE.md, CLAUDE.local.md, .claude/rules/*.md, AGENTS.md, AGENTS.override.md, .cursorrules, .cursor/rules/*.mdc, GEMINI.md. For each file it prints lines, bytes, the @imports it contains, and a flag when the file is over 200 lines (the Claude Code docs' per-file guidance) or when the combined AGENTS.md bytes along a path exceed 32 KiB (Codex's default project_doc_max_bytes). Input: a directory (default: current directory). Skips .git, node_modules, venvs, build output. Run: python context-files-claude-md-agents-md-cursor-rules.py [path] Exit: 0 when nothing is flagged, 1 when at least one file is flagged. Python 3.13, standard library only. """ from __future__ import annotations import os import re import sys NAMES = {"CLAUDE.md", "CLAUDE.local.md", "AGENTS.md", "AGENTS.override.md", ".cursorrules", "GEMINI.md"} SKIP_DIRS = {".git", "node_modules", ".venv", "venv", "dist", "build", "prod_v1", "__pycache__"} LINE_LIMIT = 200 # Claude Code memory docs: "target under 200 lines per CLAUDE.md file" CODEX_BYTES = 32 * 1024 # Codex docs: project_doc_max_bytes default 32 KiB IMPORT_RE = re.compile(r"(? str | None: name = os.path.basename(path) parent = os.path.basename(os.path.dirname(path)) grand = os.path.basename(os.path.dirname(os.path.dirname(path))) if name in NAMES: return name if name.endswith(".mdc") and parent == "rules" and grand == ".cursor": return ".cursor/rules/*.mdc" if name.endswith(".md") and parent == "rules" and grand == ".claude": return ".claude/rules/*.md" return None def imports_in(text: str) -> list[str]: """@path imports the way Claude Code parses them: skipped inside code spans and fences.""" text = re.sub(r"```.*?```", " ", text, flags=re.S) text = re.sub(r"`[^`]*`", " ", text) return IMPORT_RE.findall(text) def audit(root: str) -> int: root = os.path.abspath(root) found = [] for dirpath, dirnames, filenames in os.walk(root): dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS] for fn in filenames: path = os.path.join(dirpath, fn) kind = kind_of(path) if kind: with open(path, encoding="utf-8", errors="replace") as f: text = f.read() found.append((os.path.relpath(path, root), kind, text)) if not found: print(f"{root}: no agent context files found") return 0 flagged = 0 print(f"{root}: {len(found)} context file(s)") print(f"{'file':44} {'kind':22} {'lines':>5} {'bytes':>7} notes") for rel, kind, text in sorted(found): lines = text.count("\n") + (0 if text.endswith("\n") or not text else 1) size = len(text.encode("utf-8")) notes = [] imps = imports_in(text) if imps: notes.append("imports: " + ", ".join(imps)) if lines > LINE_LIMIT: notes.append(f"OVER {LINE_LIMIT} LINES") flagged += 1 if size > CODEX_BYTES and kind.startswith("AGENTS"): notes.append("OVER 32 KiB (Codex would truncate)") flagged += 1 print(f"{rel:44} {kind:22} {lines:>5} {size:>7} {'; '.join(notes)}") # Codex concatenates AGENTS.md files root-down along the path to the working directory, # stopping once the combined size reaches project_doc_max_bytes. agents = [(rel, text) for rel, kind, text in found if kind in ("AGENTS.md", "AGENTS.override.md")] if agents: total = sum(len(t.encode("utf-8")) for _, t in agents) state = "over" if total > CODEX_BYTES else "under" print(f"AGENTS.md total across the tree: {total} bytes ({state} the 32 KiB Codex default)") if total > CODEX_BYTES: flagged += 1 print(f"flagged: {flagged}") return 1 if flagged else 0 if __name__ == "__main__": sys.stdout.reconfigure(encoding="utf-8") sys.exit(audit(sys.argv[1] if len(sys.argv) > 1 else "."))