4. Modular Architecture
The Problem with Monolithic Config
A 600-line CLAUDE.md with no structure is the most common failure mode in production contexts. Symptoms:
- Rules from different domains mix together: a React component convention sits next to a database migration rule
- Claude reads all 600 lines but the attention budget means rules on page 5 get less weight than rules on page 1
- New team members can't find relevant rules quickly
- Updates require scanning the entire file to find related rules before editing
- Adherence degrades progressively as the file grows
The fix is architectural: decompose the monolith into focused modules, then use path-scoping to load each module only when relevant.
A CLAUDE.md file that keeps growing hurts the model's performance directly, not just the token bill. The common reflex when Claude gets something wrong is to add one more rule to the system prompt, on the assumption that more context can only help. Two independent talks from the same 2026 meetup reached the opposite conclusion: past a certain size, splitting rules into separate modular files outperforms piling them into one growing document, because the file itself becomes the bottleneck the model has to work around. (Florian Allainmat, Dev With AI Meetup, 2026. Gallet and Dahan, same corpus and year, report the same independent finding.)
A production data engineering team reached the same conclusion from a completely different starting point. Gorgias built an internal SQL-generating agent and found that a single large prompt describing their ~100 BigQuery tables was unmaintainable and noisy. Their fix was a three-tier context layer, structured much like the pattern above: per-table metadata with when_to_use and how_to_use fields (the productive-altitude pattern from Section 3, with 10-15 example question-query pairs per table), hierarchical topic instructions loaded per department, and step-by-step skill playbooks for multi-step tasks. Their own framing: progressive disclosure, loading only the relevant context on demand, was "the single biggest improvement" to reliability. Three unrelated teams (a CLAUDE.md meetup corpus, this guide's own SKILL.md progressive-disclosure pattern, and a data team solving SQL generation) converged on the same shape without coordinating. (Gorgias Engineering, "Building a Context Layer From the Ground Up," Medium, 2026.)
Path-Scoping Pattern
Mechanism: Claude Code supports @path/to/file.md imports in CLAUDE.md. When a path-scoped import is active, rules from that module are added to context only when files under the specified path are in scope.
File structure:
project/
├── CLAUDE.md # Root config, shared rules + @imports
├── src/
│ ├── api/
│ │ └── CLAUDE-api.md # API-specific rules
│ ├── components/
│ │ └── CLAUDE-components.md # React/UI-specific rules
│ └── lib/
│ └── CLAUDE-lib.md # Utility/shared library rules
├── prisma/
│ └── CLAUDE-db.md # Database and migration rules
└── tests/
└── CLAUDE-tests.md # Testing conventionsRoot CLAUDE.md with imports:
# Project Config
## Shared Rules
[...shared rules here...]
## Subsystem Modules
@src/api/CLAUDE-api.md
@src/components/CLAUDE-components.md
@src/lib/CLAUDE-lib.md
@prisma/CLAUDE-db.md
@tests/CLAUDE-tests.mdExample path-scoped module (src/api/CLAUDE-api.md):
# API Rules
- Route handlers in /app/api only; no business logic inline
- All endpoints must validate input with Zod before processing
- Error responses use the standard format: { error: string, code: string }
- Never log request bodies that may contain PII; log IDs only
- Rate limiting headers must be present on all public endpoints
- Authentication: verify JWT in middleware, not in individual handlersThis module's 6 rules are in context only when working in src/api/. They do not consume context budget when working in src/components/.
Skills vs. Rules
This distinction is underused and matters:
| Dimension | Rules | Skills |
|---|---|---|
| Nature | Constraints, standards, conventions | Capabilities, procedures, workflows |
| When active | Always enforced | Invoked on demand |
| Example | "Never use any in TypeScript" | "How to add a new API endpoint" |
| Location | CLAUDE.md | .claude/skills/ |
| Token cost | Always-on | Loaded only when invoked |
Rules define what Claude should and should not do by default. They set the boundaries of acceptable output.
Skills define how to do complex multi-step tasks that require specific knowledge of your project's patterns. They are loaded when Claude needs to perform a specific type of task, not always.
Practical example: A rule says "API endpoints must have Zod validation." A skill says "Here is the step-by-step pattern for creating a new API endpoint in this project, including the Zod schema pattern, the error handling wrapper, the auth middleware hook, and the test file structure."
Putting the endpoint creation procedure in a rule would mean loading 40 lines of procedural instructions for every session, even when you're not creating endpoints. Putting it in a skill means loading those 40 lines only when creating an endpoint.
Rule: Never expose raw database IDs in API responses.Skill: How to generate and use UUID-based public identifiers for entities.
Community skill libraries
Pre-built skill collections reduce the upfront investment in modular context engineering:
anthropics/claude-code-skills(official): Anthropic-maintained skill templates covering common development workflowsibelick/ui-skills: UI component and design system skills for frontend projects
These can be cloned, inspected, and adapted to your project conventions rather than built from scratch. Treat them as starting points: fork and modify to match your stack and naming conventions rather than using them verbatim.
Progressive Disclosure
The principle: don't load everything upfront. Load what is needed for the task at hand.
Core config (always-on):
- Architecture decisions and their rationale
- Coding standards and naming conventions
- Security constraints
- Tool preferences
Contextual modules (loaded per task):
- Deployment procedures (load when deploying)
- API patterns (load when working in API layer)
- Test templates (load when writing tests)
- Database migration procedures (load when touching schema)
Implementation pattern using skills:
.claude/
├── skills/
│ ├── deploy-production.md # Loaded when: "deploy this"
│ ├── add-api-endpoint.md # Loaded when: "add endpoint for X"
│ ├── write-migration.md # Loaded when: "add DB column"
│ └── create-component.md # Loaded when: "create component for X"Each skill file contains the step-by-step procedure with project-specific patterns. Claude loads it when the task type is detected, not proactively.
MCP tool count and context budget
MCP servers inject tool definitions into the system prompt. Each server adds its tool schemas, which consume context budget before any user content appears. Anthropic's engineering guidance recommends:
- Fewer than 10 MCP servers active per project
- Fewer than 80 total tools across all active servers
Beyond these thresholds, tool definition overhead measurably reduces the tokens available for actual task content. At 80+ tools, you are burning 15-20K tokens on tool schemas alone, budget that would otherwise go to code context, conversation history, and file contents.
The progressive disclosure principle applies to MCP servers as much as to rules. Load MCP servers contextually rather than activating all available servers for every project:
{
"mcpServers": {
"database": { },
"github": { }
}
}Resist the pattern of adding every available MCP server to a project's settings "just in case." Each inactive-but-loaded server is pure overhead. If a server is used in fewer than 20% of sessions in a project, it should not be in the default project config.
Anti-Pattern: The Monolithic CLAUDE.md
What it looks like:
# CLAUDE.md (600 lines)
## Rules
1. Use TypeScript
2. No any types
3. Run tests before committing
4. API endpoints need auth
5. Use Prisma for DB queries
6. React components in PascalCase
7. Deploy with ./scripts/deploy.sh
8. Check OWASP Top 10 before shipping
[...492 more rules...]Why it fails:
- Rules 1-20 get ~95% attention weight; rules 500+ get ~30%
- Frontend dev reads backend DB rules they don't need and vice versa
- No logical grouping means finding relevant rules requires reading everything
- Adding a new rule requires checking the entire file for conflicts
- Adherence degrades continuously as the file grows
The fix:
- Extract rules by domain into path-scoped modules
- Keep the root CLAUDE.md to shared rules + import declarations
- Move procedural knowledge to skills
- Target root CLAUDE.md at under 150 lines after extraction
Structural Metadata Files
Rules and structure are two different types of context. Conflating them produces files that are too large to load always-on but too important to skip.
Rules context answers: how should I work in this project? It lives in CLAUDE.md and path-scoped modules. It is relatively stable and almost always relevant.
Structural context answers: what is the shape of this project? How many API routes exist, which domains have components, where do the nested CLAUDE.md files live, how many Prisma models are there. This information is only needed for implementation tasks (creating a new file, adding a route, navigating an unfamiliar domain) and is irrelevant for debugging, documentation, or code review sessions.
Loading structural context always wastes tokens. Not having it at all means Claude browses the filesystem manually at the start of every implementation task, consuming turns and generating noise.
The pattern: a small, auto-generated YAML file (~1K tokens) that captures the structural shape of the codebase, registered in CLAUDE.md as a pointer rather than auto-imported.
What to include (five sections, nothing more):
| Section | Contents | Example |
|---|---|---|
layers | Architecture tiers with root paths and file counts | routers: \{ root: "src/api", count: 33 \} |
component_domains | Feature domains with paths and component counts | \{ name: "chat", count: 66 \} |
nested_contexts | All CLAUDE.md / AI_INSTRUCTIONS.md under src/, with line count and focus | \{ path: "src/server/CLAUDE.md", lines: 45 \} |
stats | Aggregate numbers: total files, test counts, schema model count | total_ts_files: 543 |
key_paths | Canonical paths Claude frequently gets wrong | prisma_schema: "src/server/db/prisma/schema.prisma" |
Keep the file below 1K tokens. Beyond that, you are adding detail that belongs in the actual source files.
The pointer registration pattern
Do not auto-load this file with @machine-readable/code-map.yaml in CLAUDE.md. Instead, register it in a reference table that tells Claude what the file contains and when to reach for it:
## Context Indexes (load on demand)
| File | Contents | When to load |
|------|----------|--------------|
| machine-readable/code-map.yaml | Architecture layers (counts + roots), component domains, nested context files, project stats | Before any implementation task: new file, new route, new component |
| machine-readable/ai-config.yaml | Full AI tooling config: rules, skills, commands, agents, hooks | When auditing or modifying AI configuration |
| PROJECT_INDEX.md | Detailed architecture narrative, ADRs, domain glossary | Deep architectural work only |This pattern scales: Claude reads the table at session start, knows what reference files exist and why, and loads them only when the current task warrants it. A debugging session never touches the code map. An implementation task loads it in one tool call.
What "auto-generated" means in practice
The generation script should do only three things: call readdirSync on each layer root to count files, walk the src tree to total .ts/.tsx files, and glob for nested CLAUDE.md files to populate nested_contexts. No AST parsing, no database queries, no network calls. The whole script runs in under a second. Add it to your pnpm ai:sync (or equivalent) task.
The key design constraint: never add hand-curated content to this file. The moment you do, you have a file that can drift. Auto-generated files cannot lie about the current state of the codebase; files with manual content can and will.
Production example (Méthode Aristote EdTech platform, ~1,300 source files):
version: "1.0.0"
architecture: "Client → tRPC → Router → Service → Repository → Prisma"
layers:
routers:
root: "src/server/api/routers"
description: "Tier 1 — Zod validation, delegate to service"
count: 33
services:
root: "src/server/api/services"
description: "Tier 2 — business logic, enforcePermission()"
count: 61
repositories:
root: "src/server/api/repositories"
description: "Tier 3 — CRUD Prisma only"
count: 38
stats:
total_ts_files: 543
total_tsx_files: 798
prisma_models: 48
unit_tests: 268With this file registered as a pointer, Claude answers "how many tRPC routers exist?" in a single lookup rather than walking src/server/api/routers/ manually. For the implementation task "add a payment router", it immediately knows the correct root, the count, and the architectural constraint, before reading a single source file.
A ready-to-use template is available at examples/context-engineering/code-map-template.yaml.