Why Claude Code Over-Simplifies Your Code and How to Stop It
I spent two days debugging a Swift build that Claude Code had “simplified” for me. The build succeeded, but the app crashed at runtime. Claude had removed what it thought were “unnecessary” compiler flags - flags that enabled full-text search and thread safety.
Me: "Update the Package.swift dependencies"Claude: "I've simplified the build configuration by removing redundant flags"Me: "Those weren't redundant. The app crashes now."Claude: "I apologize. I was following the 'avoid over-engineering' directive."The fix? I had to prefix critical sections with “THIS IS NOT OVER-ENGINEERING” - which is absurd, but it actually works.
The Problem: Claude’s Hidden Directive
Claude Code operates with built-in directives that prioritize simplicity. Two of them cause the most trouble:
1. "Avoid over-engineering. Only make changes that are directly requested or clearly necessary"
2. "Don't add features, refactor code, or make 'improvements' beyond what was asked"These sound reasonable. But they create a fundamental conflict when working with intentional complexity.
What Claude Sees vs. What Exists
┌─────────────────────────────────────────────────────────────┐│ What Claude sees: ││ - Extra layers in the architecture ││ - "Redundant" validation checks ││ - "Unnecessary" build flags ││ - "Overly complex" patterns │├─────────────────────────────────────────────────────────────┤│ What actually exists: ││ - Cross-platform build requirements ││ - Security layers preventing injection attacks ││ - Compiler flags for runtime features ││ - Patterns enabling testability and maintenance │└─────────────────────────────────────────────────────────────┘Claude lacks context about WHY complexity exists. It only sees the complexity itself.
Real Examples from the Trenches
Example 1: Build Flags Get Stripped
I had a Swift package with conditional compilation flags:
let package = Package( name: "DocumentIndexer", targets: [ .target( name: "DocumentIndexer", swiftSettings: [ .define("SQLITE_ENABLE_FTS5"), // Full-text search .define("SQLITE_THREADSAFE", to: "1") // Thread safety ] ) ])Claude “simplified” it to:
let package = Package( name: "DocumentIndexer", targets: [ .target(name: "DocumentIndexer") ])The build succeeded. The app crashed the moment anyone tried to search documents.
Example 2: Repository Pattern Removed
I had a clean architecture with repositories:
// Repository pattern for testability and multi-tenant supportexport class UserRepository { constructor(private db: Database) {}
async findById(id: string): Promise<User | null> { return this.db.users.find({ id }) }
async create(data: CreateUserDTO): Promise<User> { return this.db.users.create(data) }}Claude “simplified” it to direct database calls everywhere, removing the repository layer entirely. This broke:
- Unit tests (no more mocking)
- Multi-tenant architecture (tenant context lost)
- Audit logging (no central point)
Example 3: Validation Layer Deleted
# Input validation layerdef validate_user_input(data: dict) -> ValidatedInput: if not data.get('email'): raise ValidationError("Email required") if not is_valid_email(data['email']): raise ValidationError("Invalid email format") if contains_sql_injection(data): raise ValidationError("Invalid characters detected") return ValidatedInput(**data)Claude removed this as “redundant” because the database also validates. But the database validation doesn’t check for SQL injection patterns - it just escapes them. The security layer was gone.
The Solution: Explicit Purpose Labels
The Reddit community found a workaround that sounds ridiculous but works:
<!-- THIS IS NOT OVER-ENGINEERING: Required for cross-platform builds --><!-- Context: Windows requires different linking flags, macOS needs .framework -->I now prefix every critical section with this pattern:
# THIS IS NOT OVER-ENGINEERING# Purpose: This validation layer prevents injection attacks# Context: User input flows through 3 different endpoints# Do NOT remove - required by security policy
def validate_user_input(data: dict) -> ValidatedInput: if not data.get('email'): raise ValidationError("Email required") if not is_valid_email(data['email']): raise ValidationError("Invalid email format") if contains_sql_injection(data): raise ValidationError("Invalid characters detected") return ValidatedInput(**data)The key is explaining WHY, not just WHAT.
Solution 2: Strategic CLAUDE.md Documentation
A vague CLAUDE.md gets ignored:
# Coding Style- Keep architecture patterns- Don't simplify build configs- Maintain existing structureA specific CLAUDE.md with rationale works:
# Build Configuration
## Swift Build FlagsCRITICAL: Do NOT simplify these flags. This is not over-engineering.
```swift// These conditional flags are REQUIRED:// - SQLITE_ENABLE_FTS5: Full-text search for document indexing// - SQLITE_THREADSAFE=1: Thread safety for concurrent requests// Without these, the app will CRASH on document search.
let package = Package( name: "MyApp", targets: [ .target( name: "MyApp", swiftSettings: [ .define("SQLITE_ENABLE_FTS5"), .define("SQLITE_THREADSAFE", to: "1") ] ) ])If you remove these thinking they’re “unnecessary complexity,” the build will succeed but the app will crash at runtime.
Repository Pattern
This is NOT over-engineering. We use repositories because:
- Enables unit testing with mock data
- Decouples business logic from database
- Required for multi-tenant architecture
DO NOT simplify to direct database calls. This will break:
- All unit tests (no mocking capability)
- Tenant isolation (context lost)
- Audit logging (no central point)
## The Pattern: Context + Consequence
I've found a formula that works:
```text title="The anti-simplification formula"┌─────────────────────────────────────────────────────────────┐│ 1. Label: "THIS IS NOT OVER-ENGINEERING" ││ 2. Purpose: What this code does ││ 3. Context: Why it exists in this architecture ││ 4. Consequence: What breaks if removed │└─────────────────────────────────────────────────────────────┘Example:
// THIS IS NOT OVER-ENGINEERING// Purpose: Caching layer for API responses// Context: We make 10,000+ API calls per hour// Consequence: Without this, API costs increase 90% and response times triple
export class CacheLayer { private cache = new Map<string, CachedResponse>()
async get<T>(key: string, fetcher: () => Promise<T>): Promise<T> { const cached = this.cache.get(key) if (cached && !this.isExpired(cached)) { return cached.data as T } const data = await fetcher() this.cache.set(key, { data, timestamp: Date.now() }) return data }}Common Mistakes I Made
Mistake 1: Fighting Claude After the Fact
I used to let Claude simplify, then try to re-explain. This is inefficient:
1. Claude simplifies code2. I notice something broke3. I explain why the original was needed4. Claude restores it5. Repeat for next sectionNow I prevent with proactive documentation:
1. Document WHY complexity exists in CLAUDE.md2. Label critical sections with purpose3. Claude reads context before making changes4. Complexity is preservedMistake 2: Vague Rules
“Maintain architecture” means nothing to Claude. Be specific:
# Rules- Maintain existing patterns- Don't over-simplify# Architecture Patterns
## CQRS PatternThis is NOT over-engineering. We use Command Query Responsibility Segregation because:- Read and write workloads have different scaling needs- Enables separate optimization for queries vs commands- Required for eventual consistency with event sourcing
DO NOT merge read and write models. This will break the event replay mechanism.Mistake 3: Assuming Claude Remembers Context
Claude starts fresh each session. I used to assume it remembered previous conversations:
Session 1: "This caching layer is critical for performance"Session 2: Claude remembers and preserves caching layerSession 1: "This caching layer is critical for performance"Session 2: Claude has no memory, sees "extra layer", removes itNow I put critical context in CLAUDE.md, which is read every session.
What I Actually Do Now
My current workflow:
-
Before any Claude session, I ensure CLAUDE.md has:
- Purpose explanations for all architectural decisions
- Consequence descriptions for removing each pattern
- Explicit “NOT OVER-ENGINEERING” labels
-
In code, I label critical sections:
// THIS IS NOT OVER-ENGINEERING// Purpose: Enable full-text search and thread safety// Context: Document indexing runs on background threads// Consequence: App crashes on search without SQLITE_ENABLE_FTS5
let package = Package( name: "DocumentIndexer", targets: [ .target( name: "DocumentIndexer", swiftSettings: [ .define("SQLITE_ENABLE_FTS5"), .define("SQLITE_THREADSAFE", to: "1") ] ) ])- After Claude makes changes, I verify critical sections weren’t touched.
Decision Tree: When to Add Labels
Does this code look like it could be "simplified"?│├─ YES → Would simplification break something?│ ││ ├─ YES → Add "THIS IS NOT OVER-ENGINEERING" label│ │ with purpose, context, and consequence│ ││ └─ NO → No label needed│└─ NO → No label neededSigns that code needs a label:
- Multiple layers of abstraction
- Configuration that seems “redundant”
- Validation that duplicates other checks
- Patterns that add indirection
- Build flags or conditional compilation
Key Takeaways
- Claude lacks context about WHY - It sees complexity, not the reason for it
- “THIS IS NOT OVER-ENGINEERING” works - It’s absurd but effective
- Explain purpose + context + consequence - Give Claude the “why”
- CLAUDE.md needs specifics - Vague rules get deprioritized
- Proactive beats reactive - Document before Claude touches code
- Each session is fresh - Claude doesn’t remember previous conversations
The “avoid over-engineering” directive is well-intentioned. It prevents Claude from adding unnecessary complexity. But it misfires when applied to intentional complexity - the kind that exists for good reasons.
Treat Claude like a junior developer who needs the “why,” not just the “what.” The absurd solution of prefixing sections with “THIS IS NOT OVER-ENGINEERING” actually works because it gives Claude the context it needs to distinguish between accidental and intentional complexity.
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments