Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions .claude/rules/claude-md-guide.md
Original file line number Diff line number Diff line change
@@ -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/<name>/SKILL.md`), loaded on demand.
- **Triggered by editing specific files** -> a path-scoped rule (`.claude/rules/<name>.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").
45 changes: 36 additions & 9 deletions scripts/sync_agent_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
- Regenerates `.codex/agents/<name>.toml` from each `.claude/agents/<name>.md`.
- Auto-prunes dangling symlinks and orphaned TOMLs silently.
"""

from __future__ import annotations

import argparse
Expand All @@ -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 = [
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand All @@ -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


Expand Down
Loading