From b4690da00a46a73706a14937fa743af5b84bc3de Mon Sep 17 00:00:00 2001 From: Miyamura80 Date: Mon, 11 May 2026 12:24:35 +0100 Subject: [PATCH] Add CLAUDE.md editing guide as path-scoped rule Co-Authored-By: Claude Opus 4.6 --- .claude/rules/claude-md-guide.md | 57 ++++++++++++++++++++++++++++++++ scripts/sync_agent_config.py | 45 ++++++++++++++++++++----- 2 files changed, 93 insertions(+), 9 deletions(-) create mode 100644 .claude/rules/claude-md-guide.md diff --git a/.claude/rules/claude-md-guide.md b/.claude/rules/claude-md-guide.md new file mode 100644 index 0000000..9345575 --- /dev/null +++ b/.claude/rules/claude-md-guide.md @@ -0,0 +1,57 @@ +--- +paths: + - "CLAUDE.md" + - "AGENTS.md" +--- + +# Editing CLAUDE.md + +`CLAUDE.md` is loaded into every Claude Code session. Every line costs context. Treat it like a hot path: dense, accurate, no fat. + +`AGENTS.md` is often a symlink or copy of `CLAUDE.md` - check before editing; if linked, editing one edits both. + +## Validation + +Repos commonly enforce structure (required headings, banned characters, link checks) via Make targets, scripts, or pre-commit hooks. Before committing, run whatever the repo defines locally - don't rely on CI to catch it. If the repo has no validation, the hard requirement is just: don't break the section headings other tools rely on. + +## Where it goes + +Before adding to root `CLAUDE.md`, check whether it belongs somewhere else: + +- **Always relevant in this repo** -> root `CLAUDE.md`. +- **Scoped to a subtree** -> nested `CLAUDE.md` / `AGENTS.md` inside that subtree. +- **Sometimes relevant, situation-triggered** -> a skill (`.claude/skills//SKILL.md`), loaded on demand. +- **Triggered by editing specific files** -> a path-scoped rule (`.claude/rules/.md` with `paths:` frontmatter), like this file. +- **Must happen deterministically** -> a hook (pre-commit, `.claude/settings.json`), not prose. Hooks are deterministic; CLAUDE.md is advisory and Claude can ignore it. + +If root `CLAUDE.md` crosses ~200 lines, that's a smell: push specifics into nested files, skills, or rules. + +## What earns its place + +Include a line only if it's all of: +1. **Non-obvious** - not derivable from a directory name, a standard tool, or sensible defaults. +2. **Non-rot-prone** - won't go stale on the next refactor (avoid file enumerations, "currently we have X, Y, Z" lists). +3. **Non-rediscoverable** - a future Claude can't trivially get it from one `Read` or one `ls`. +4. **Not duplicated elsewhere in the file** - if the same fact appears in multiple sections or in a code block, pick one. + +Categories that typically earn their place: +- The project's headline architecture idea or organizing principle. +- Layering / dependency-direction rules. +- `file:line` anchors for key entrypoints. +- Non-standard infra choices a reader would assume differently by default. +- Project-specific conventions (commit message format, naming patterns, long-running task patterns). +- Gotchas (deprecated APIs, fast-evolving specs, build quirks). +- Disambiguations for terms or concepts that are easy to get wrong. + +## What to cut + +- ASCII architecture diagrams - they're a low-density way to transmit structure to a coding agent (lots of tokens for what a few bullets convey). Prefer compact bullet layering; put the diagram in `README.md` if humans want it. +- Restating directory purpose when the name already says it (`db/` is the database, `tests/` is tests, `.github/workflows/` is CI workflows). +- Listing files that mirror a directory - they rot, and `ls` is one tool call. +- Implementation walkthroughs. A `file:line` anchor + one sentence is enough; details are rediscoverable in one `Read`. +- Style rules already enforced by the linter/formatter. The Code Style section should just point at the linter config. +- Boilerplate intros ("This file provides guidance to Claude Code..."). +- Sections duplicated by a dedicated section below. +- Code-block comments that restate the next line. +- Deterministic must-happens better expressed as a hook (pre-commit, `settings.json`) - prose is advisory, hooks are enforced. +- Self-evident platitudes ("write clean code", "follow best practices"). diff --git a/scripts/sync_agent_config.py b/scripts/sync_agent_config.py index 69375c2..2b54a75 100644 --- a/scripts/sync_agent_config.py +++ b/scripts/sync_agent_config.py @@ -9,6 +9,7 @@ - Regenerates `.codex/agents/.toml` from each `.claude/agents/.md`. - Auto-prunes dangling symlinks and orphaned TOMLs silently. """ + from __future__ import annotations import argparse @@ -27,11 +28,25 @@ CODEX_AGENTS = REPO / ".codex" / "agents" FRONTMATTER_RE = re.compile(r"^---\r?\n(.*?)\r?\n---\r?\n?(.*)$", re.DOTALL) -CLAUDE_ONLY_KEYS = {"tools", "model", "color", "allowed-tools", "disable-model-invocation"} +CLAUDE_ONLY_KEYS = { + "tools", + "model", + "color", + "allowed-tools", + "disable-model-invocation", +} SHARED_SKILL_FORBIDDEN_KEYS = { - "allowed-tools", "disable-model-invocation", "user-invocable", - "context", "agent", "model", "effort", "hooks", "paths", "shell", + "allowed-tools", + "disable-model-invocation", + "user-invocable", + "context", + "agent", + "model", + "effort", + "hooks", + "paths", + "shell", "argument-hint", } SHARED_SKILL_FORBIDDEN_BODY_PATTERNS = [ @@ -77,7 +92,9 @@ def render_toml(meta: dict, body: str, source: Path | None = None) -> str: def _strip_code(text: str) -> str: - text = re.sub(r"^[ ]{0,3}(`{3,}).*?^[ ]{0,3}\1`*", "", text, flags=re.DOTALL | re.MULTILINE) + text = re.sub( + r"^[ ]{0,3}(`{3,}).*?^[ ]{0,3}\1`*", "", text, flags=re.DOTALL | re.MULTILINE + ) out: list[str] = [] i, n = 0, len(text) while i < n: @@ -87,7 +104,9 @@ def _strip_code(text: str) -> str: while i + run < n and text[i + run] == "`": run += 1 close = text.find("`" * run, i + run) - if close == -1 or any(text[i + run + k] == "\n" for k in range(close - i - run)): + if close == -1 or any( + text[i + run + k] == "\n" for k in range(close - i - run) + ): out.append(text[i : i + run]) i += run elif preceded_by_bang and run == 1: @@ -114,18 +133,26 @@ def validate_shared_skill(skill_dir: Path) -> list[str]: errs: list[str] = [] bad_keys = SHARED_SKILL_FORBIDDEN_KEYS & set(meta.keys()) if bad_keys: - errs.append(f"{skill_md.relative_to(REPO)}: Claude-only frontmatter keys in shared skill: {sorted(bad_keys)}") + errs.append( + f"{skill_md.relative_to(REPO)}: Claude-only frontmatter keys in shared skill: {sorted(bad_keys)}" + ) for pat, label in SHARED_SKILL_RAW_BODY_PATTERNS: if pat.search(body): - errs.append(f"{skill_md.relative_to(REPO)}: body uses Claude-only feature: {label}") + errs.append( + f"{skill_md.relative_to(REPO)}: body uses Claude-only feature: {label}" + ) scan_body = _strip_code(body) for pat, label in SHARED_SKILL_FORBIDDEN_BODY_PATTERNS: if pat.search(scan_body): - errs.append(f"{skill_md.relative_to(REPO)}: body uses Claude-only feature: {label}") + errs.append( + f"{skill_md.relative_to(REPO)}: body uses Claude-only feature: {label}" + ) if not meta.get("name"): errs.append(f"{skill_md.relative_to(REPO)}: missing `name` in frontmatter") if not meta.get("description"): - errs.append(f"{skill_md.relative_to(REPO)}: missing `description` in frontmatter") + errs.append( + f"{skill_md.relative_to(REPO)}: missing `description` in frontmatter" + ) return errs