ADR-062: CLAUDE.md hierarchy replaces .claude/rules/
- Status: Accepted
- Date: 2026-04-15
- Deciders: Christophe, Claude Code
- Supersedes: implicit "rules dir" pattern (never formally recorded)
Contextβ
The .claude/rules/ directory had grown to 31 files totaling ~3886 lines, all auto-loaded into every Claude Code session via the globs: frontmatter mechanism. Combined with the root CLAUDE.md (206 lines) and user-global ~/.claude/CLAUDE.md (51 lines), every session started with ~4247 lines of auto-loaded context before the user even typed a prompt.
Observed pain:
- Token cost: ~12-15K tokens burned at session start regardless of task relevance. A docs-only fix paid the same tax as a cross-component MEGA.
- Signal dilution: critical GO/NOGO rules ("CI red = P0", "Council β₯ 8/10") drowned in protocol detail (full crash-recovery state machine, detailed Arena scoring formulas, gateway adapter method tables).
- Drift risk: rules frontmatter globs were aspirational β in practice nothing enforced which rules loaded when. Claude read them all.
- Audit gap: no clear boundary between "must always know" and "read when touching X".
Decisionβ
Replace .claude/rules/ with a two-tier model:
CLAUDE.mdhierarchy (root + per-service) β auto-loaded. Contains only binary GO/NOGO decisions, in imperative one-liners. Hard cap: 250 lines per file, enforced byclaude-md-budget-lint.shin CI..claude/docs/<topic>.mdβ never auto-loaded. Contains protocols, tables, examples, troubleshooting. Loaded on-demand when a skill/command references it or when Claude explicitly pulls it for a relevant task.
Per-service CLAUDE.md files live in:
/CLAUDE.md(transversal rules)stoa-gateway/CLAUDE.md(Rust, gateway-specific)control-plane-api/CLAUDE.md(Python, adapters)control-plane-ui/CLAUDE.md,portal/CLAUDE.md(TS, tests)charts/stoa-platform/CLAUDE.md(K8s, Helm)e2e/CLAUDE.md(Playwright, audit protocol)cli/CLAUDE.md(stoactl)
A pre-commit hook (.claude/hooks/block-rules-dir.sh) blocks recreation of .claude/rules/.
Rationaleβ
- 94% context reduction at session start (4247 β 257 lines).
- Locality: a Rust change loads Rust rules; it doesn't pay for SEO content rules.
- Enforceability: 250-line cap is a machine check, not a convention. Drift is caught in CI.
- Clarity: "GO/NOGO β CLAUDE.md, details β docs/" is a one-sentence rule anyone can apply.
Consequencesβ
Positiveβ
- Fresh sessions start lean. Delegation to subagents more efficient (they inherit less noise).
- Per-service
CLAUDE.mdis a natural home for team ownership (gateway team ownsstoa-gateway/CLAUDE.md). .claude/docs/is browseable as reference documentation, independent of session loading.
Negativeβ
- Duplication risk: decisions in
CLAUDE.mdand their full protocol indocs/β if both drift, docs lie. Mitigated by: decisions are one-liners (hard to drift); docs are reference-only (rarely edited). - Discoverability: Claude must know to read
.claude/docs/<topic>.mdwhen relevant. Mitigated by explicit pointers fromCLAUDE.mdsections and by skills referencing them. - Onboarding: contributors used to browsing
.claude/rules/must learn the new layout. Mitigated by the backup dir being preserved during transition and by this ADR.
Neutralβ
- Per-service CLAUDE.md count can grow indefinitely β but 250-line cap per file prevents any single one from becoming the next
rules/.
Enforcementβ
| Layer | Mechanism |
|---|---|
| Local | .git/hooks/pre-commit β .claude/hooks/block-rules-dir.sh (blocks .claude/rules/ with any .md file) |
| CI | .github/workflows/claude-md-budget.yml β scripts/ai-ops/claude-md-budget-lint.sh (hard-fails on any CLAUDE.md > 250 lines) |
| Kill-switch | DISABLE_CLAUDE_MD_BUDGET=1 env var (emergency only) |
Referencesβ
- Migration commit:
a2c47bd7(refactor: nuke .claude/rules/ β CLAUDE.md per service + .claude/docs/) - Backup:
.claude/rules-backup-20260415/(removed after validation) - Related: ADR-061 (Council multi-stage review) β the
council-s3.mdrule was migrated to.claude/docs/per this ADR