You ask Claude Code to add a feature. The PR works, but it’s architecturally off: it extracted three internal helpers into a “shared service,” added a dependency-injection container nobody asked for, turned plain functions into classes, or proposed splitting the feature into its own microservice when your stack is deliberately a single Astro app.
Fastest fix: put an explicit “we use / we don’t use” architecture block in your project CLAUDE.md, pair each rule with a real file to copy, and name that file in the task prompt. If drift keeps happening on the same pattern, stop relying on prose and add a PreToolUse hook that hard-blocks it — CLAUDE.md is context Claude tries to follow, not an enforced rule, so anything you truly cannot allow belongs in a hook.
Architecture mismatch happens because Claude applies generic “best practices” from training data against the deliberate choices your team made. The win is making your conventions inevitable: specific, file-anchored examples beat generic priors, and a hook beats both when it has to.
Which bucket are you in
| Symptom | Most likely cause | Jump to |
|---|---|---|
| No architecture rules anywhere | CLAUDE.md missing/vague | Cause 1, Step 1 |
| Output mirrors NestJS/Spring/Rails defaults | Training prior overriding you | Cause 2, Step 1 |
| Picks the older of two patterns you’re migrating | Mixed patterns, no canonical | Cause 3, Step 4 |
| Rules exist but Claude ignores them | Abstract rules, no example file | Cause 4, Step 2 |
Drift only in one folder (e.g. src/api/) | No path-scoped rule | Cause 4, Step 2b |
| Your prompt said “service”/“controller”/“repo” | Prompt vocabulary cued it | Cause 5, Step 6 |
| Claude follows the doc but the doc is wrong | CLAUDE.md stale | Cause 6, Step 5 |
| Same violation every run, prose won’t stop it | Needs enforcement, not context | Step 7 (hook) |
Common causes
Ordered by hit rate, highest first.
1. CLAUDE.md missing or vague on architecture
Without explicit rules (“we are monolithic by choice; do not propose splits”), Claude defaults to what looks professional in training data — which often means microservices, DI, repository patterns. Note that CLAUDE.md is delivered as a user message after the system prompt, so Claude reads it and tries to follow it, but there is no strict-compliance guarantee, especially for vague or conflicting wording.
How to spot it: grep -i "architecture\|monolith\|microservice\|pattern" CLAUDE.md returns nothing. Architecture is implicit, not codified. Run /memory in the session to confirm which instruction files actually loaded — if your CLAUDE.md isn’t in that list, Claude never saw it.
2. Training prior stronger than your conventions
Claude saw tens of thousands of “extract this into a class with DI” examples during training. Your project’s “functions only” preference appears in zero examples it has seen. Without an explicit override, the prior wins.
How to spot it: The suggestion mirrors a popular framework pattern (NestJS DI, Spring beans, Rails service objects). You’re getting framework defaults, not your codebase’s defaults.
3. Project has mixed patterns; agent picks the one it knows best
Half your codebase uses Redux, half uses Zustand (migration in progress). Claude sees both, can’t tell which is canonical, and defaults to Redux because it’s more common in training data.
How to spot it: The suggestion uses the older / more popular pattern and ignores the newer / project-canonical one. Migration ambiguity.
4. CLAUDE.md has abstract rules without examples
“Prefer composition over inheritance” is a fine principle but useless to Claude without a concrete file to copy from. Abstract rules don’t transfer; concrete examples do.
How to spot it: Your CLAUDE.md rules are statements without file pointers. Add a canonical example path to each one.
5. The task implicitly asks for a new pattern
“Build a billing service” — the word “service” cues service-oriented design. “Add billing to src/features/” cues following your existing feature structure.
How to spot it: Your prompt echoes framework vocabulary (“service”, “controller”, “repository”) that your project doesn’t actually use.
6. CLAUDE.md is stale
The architecture used to be different. CLAUDE.md still describes the old approach. Claude follows the doc faithfully and produces code that contradicts the new (real) architecture, then you blame Claude.
How to spot it: CLAUDE.md was last touched months ago while the architecture moved on. A stale doc is actively lying to Claude.
Shortest path to fix
Ordered by ROI. Steps 1–3 lock in the right pattern at the durable layer; Step 7 is the escalation when prose alone won’t hold.
Step 1: Write explicit “we use” / “we don’t use” rules
In your project CLAUDE.md (at the repo root, or ./.claude/CLAUDE.md — both are loaded as project memory):
## Architecture
We use:
- Single Astro app (monolith); deploy as one unit
- Functions over classes (no class-based services)
- Direct DB queries via Drizzle (no repository pattern, no ORM abstractions)
- Local component state + URL state (no Redux/Zustand global store)
- Server actions for mutations (no API routes for first-party calls)
- Vitest for tests (no Jest)
We don't use:
- Microservices — single-process Astro is intentional
- Dependency injection containers
- Repository / unit-of-work patterns
- GraphQL — REST + server actions only
- Class-based components — functional only
If you find yourself proposing any of the "don't use" patterns, stop and ask first.
Explicit don’ts matter as much as dos — they’re what actually overrides the training prior. Keep the whole CLAUDE.md under ~200 lines; longer files consume more context and reduce adherence (per Anthropic’s own guidance, as of June 2026). If you don’t have a CLAUDE.md yet, run /init to have Claude generate a first draft from your codebase, then tighten the architecture section by hand. /init also reads an existing AGENTS.md, .cursorrules, or .windsurfrules and folds the relevant parts in.
Step 2: Pair every rule with a canonical example
Abstract rules slide off; concrete file pointers stick:
## Canonical examples
- Feature module: src/features/auth/
- index.ts (public API)
- actions.ts (server actions)
- components/ (UI)
- Database access: src/db/queries/users.ts (direct Drizzle, no repo)
- Component state: src/features/billing/components/CheckoutForm.tsx (useState + URL params)
Claude reads these files and copies their shape, which beats the abstract prior. You can also import a longer convention doc instead of pasting it inline, using the @path syntax anywhere in CLAUDE.md (relative paths resolve against the file, imports nest up to four hops deep, as of June 2026):
See @docs/architecture.md for the full pattern catalog.
Imported files load into context at launch, so they help organization but do not save context budget.
Step 2b: Scope folder-specific rules with .claude/rules/
If drift only happens in one area (say Claude keeps adding controllers under src/api/), don’t bloat the global CLAUDE.md. Put a path-scoped rule in .claude/rules/ that only loads when Claude touches matching files:
---
paths:
- "src/api/**/*.ts"
---
# API conventions
- API endpoints are Astro server actions in `actions.ts`, not REST controllers.
- No class-based handlers. No DI. Copy `src/features/auth/actions.ts`.
Each file in .claude/rules/ covers one topic; the paths glob means the rule enters context only when Claude reads a matching file, keeping your always-on CLAUDE.md lean. Rules without a paths field load every session at the same priority as ./.claude/CLAUDE.md.
Step 3: In the task, name the canonical example explicitly
Add the billing feature.
Architecture: follow `src/features/auth/` exactly — same file structure, same patterns.
DO NOT propose:
- Extracting anything into a separate service
- Adding a dependency-injection layer
- Converting functions to classes
- A global state store
If the task seems to need any of these, stop and ask before changing direction.
The “stop and ask” clause is the safety valve: Claude can flag a genuine architecture need without unilaterally introducing it.
Step 4: Reject architecture-drift PRs at review, and declare a canonical for mixed patterns
When Claude proposes a divergent design, push back instead of accepting it:
Your PR introduces a `BillingService` class with DI.
This contradicts `src/features/auth/` and CLAUDE.md.
Refactor to:
- Functions in `src/features/billing/actions.ts`
- No service class
- No DI
Then resubmit.
Each rejection steers the rest of that session. For the mixed-pattern case (Cause 3), a one-off rejection isn’t enough — write down the winner so Claude can’t guess wrong next time:
## State management
Canonical: Zustand (`src/stores/`). Redux is legacy and being removed; never add to it.
Migrate any Redux you touch; do not extend it.
Step 5: Audit CLAUDE.md against current code on a schedule
A stale doc lies to Claude. Check the doc’s age against recent architecture work:
# When did CLAUDE.md last update vs major architecture changes?
git log -1 --format="%ai" CLAUDE.md
git log --since="6 months ago" --oneline -- src/features/ | head
If a lot of feature work landed since the last CLAUDE.md edit, the doc probably doesn’t reflect current patterns. Also review for internal contradictions: if two rules disagree, Claude picks one arbitrarily. Running /memory shows every loaded CLAUDE.md, CLAUDE.local.md, and .claude/rules/ file — useful for catching a forgotten ancestor file in a monorepo that’s pulling Claude toward the wrong pattern.
Step 6: Avoid framework vocabulary in prompts
Words shape design. Compare:
Bad: "Build a billing service with a controller and repository."
-> Claude builds NestJS-style code.
Good: "Add billing to src/features/billing/.
Follow src/features/auth/ patterns."
-> Claude follows your conventions.
When you must use a framework term (“API endpoint”), immediately bind it to your repo’s shape: “API endpoint = Astro server action in actions.ts”.
Step 7: Hard-block the pattern with a PreToolUse hook (when prose won’t hold)
CLAUDE.md and .claude/rules/ are context — Claude reads them and usually complies, but there’s no guarantee. If a specific violation keeps recurring (e.g. someone keeps letting a *Service.ts class through), enforce it at the system level with a PreToolUse hook. The hook runs before the tool executes and exit code 2 blocks the call, regardless of what the model decided.
In .claude/settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{ "type": "command", "command": ".claude/hooks/no-service-class.sh" }
]
}
]
}
}
A minimal guard that rejects new class ...Service definitions:
#!/usr/bin/env bash
# .claude/hooks/no-service-class.sh
input=$(cat)
content=$(echo "$input" | jq -r '.tool_input.content // .tool_input.new_string // ""')
if echo "$content" | grep -Eq 'class [A-Za-z]+Service'; then
echo "Blocked: this repo uses functions in actions.ts, not service classes. See CLAUDE.md." >&2
exit 2
fi
exit 0
This is the difference between guidance and enforcement: a hook applies no matter what the model “decides.” Use it sparingly, only for the one or two patterns you genuinely cannot allow.
How to confirm it’s fixed
- Run
/memoryand confirm your projectCLAUDE.md(and any relevant.claude/rules/file) appears in the loaded list. - Re-issue the same task that drifted before, naming the canonical example file.
- Check the diff against your forbidden list: no new service classes, no DI container, no microservice split, functions not classes.
- If you added a hook from Step 7, try to deliberately introduce the bad pattern and confirm the tool call is blocked with your message.
Prevention
CLAUDE.mdcarries explicit “we use” + “we don’t use” architecture lists, kept under ~200 lines and audited against the code on a schedule.- Every architectural rule points to a canonical example file Claude can read and copy.
- Folder-specific conventions live in path-scoped
.claude/rules/, not in the always-onCLAUDE.md. - Task prompts name the canonical and explicitly forbid common divergent patterns.
- In-repo pattern inconsistencies get a declared winner (and the loser gets migrated), so Claude can’t pick wrong.
- The one or two patterns you absolutely cannot allow are enforced with a
PreToolUsehook, not just described in prose.
FAQ
Why does Claude ignore a rule that’s clearly in my CLAUDE.md?
Because CLAUDE.md is loaded as a user message after the system prompt, not as enforced configuration — Claude treats it as context and tries to follow it. Vague wording, a conflicting rule in an ancestor CLAUDE.md, or an unloaded file all cause misses. Run /memory to verify it loaded, make the rule concrete (“no class-based services; copy actions.ts”), and for anything non-negotiable use a PreToolUse hook.
Does Claude Code read my AGENTS.md?
No — Claude Code reads CLAUDE.md, not AGENTS.md. If your repo already uses AGENTS.md, create a CLAUDE.md that imports it with @AGENTS.md on the first line and add Claude-specific notes below, or symlink them with ln -s AGENTS.md CLAUDE.md. Running /init on a repo with an existing AGENTS.md also folds it in.
Where should architecture rules go — CLAUDE.md, .claude/rules/, or a hook?
Use CLAUDE.md for project-wide conventions that apply every session. Use .claude/rules/ with a paths: glob for rules that only matter in one folder, so they load on demand instead of bloating context. Use a PreToolUse hook when a rule must be enforced rather than suggested.
My CLAUDE.md got long and Claude follows it less reliably. What now?
Files over ~200 lines consume more context and reduce adherence (Anthropic guidance, June 2026). Split per-folder instructions into path-scoped .claude/rules/, move long reference material into an @path import, and delete anything a linter already enforces. Shorter and more specific beats long and exhaustive.
Instructions disappear after /compact. Why?
Project-root CLAUDE.md survives compaction — Claude re-reads it from disk afterward. Nested CLAUDE.md files in subdirectories are not re-injected automatically; they reload the next time Claude reads a file in that subdirectory. Anything you only said in chat is lost on compaction, so move recurring corrections into CLAUDE.md.
Related
- Claude Code does not understand the whole project
- Claude Code refactor scope too broad
- Claude Code lost project context mid-task
- Claude Code beginner guide
- Claude Code workflow
- Claude Code project setup
External references:
- Claude Code memory docs — CLAUDE.md locations, load order, imports, auto memory
- Claude Code hooks guide — PreToolUse and the full hook lifecycle