Skip to content

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.

What happened
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:

Claude's problematic directives
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

The perception gap
┌─────────────────────────────────────────────────────────────┐
│ 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:

Package.swift (before Claude)
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:

Package.swift (after Claude)
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:

user-repository.ts
// Repository pattern for testability and multi-tenant support
export 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

validators.py
# Input validation layer
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)

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:

The absurd solution
<!-- 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:

validators.py (fixed)
# 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:

CLAUDE.md (doesn't work)
# Coding Style
- Keep architecture patterns
- Don't simplify build configs
- Maintain existing structure

A specific CLAUDE.md with rationale works:

CLAUDE.md (works)
# Build Configuration
## Swift Build Flags
CRITICAL: 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:

  1. All unit tests (no mocking capability)
  2. Tenant isolation (context lost)
  3. 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:

cache-layer.ts
// 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:

The wrong approach
1. Claude simplifies code
2. I notice something broke
3. I explain why the original was needed
4. Claude restores it
5. Repeat for next section

Now I prevent with proactive documentation:

The right approach
1. Document WHY complexity exists in CLAUDE.md
2. Label critical sections with purpose
3. Claude reads context before making changes
4. Complexity is preserved

Mistake 2: Vague Rules

“Maintain architecture” means nothing to Claude. Be specific:

Vague (ignored)
# Rules
- Maintain existing patterns
- Don't over-simplify
Specific (followed)
# Architecture Patterns
## CQRS Pattern
This 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:

What I thought
Session 1: "This caching layer is critical for performance"
Session 2: Claude remembers and preserves caching layer
Reality
Session 1: "This caching layer is critical for performance"
Session 2: Claude has no memory, sees "extra layer", removes it

Now I put critical context in CLAUDE.md, which is read every session.

What I Actually Do Now

My current workflow:

  1. 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
  2. In code, I label critical sections:

Package.swift
// 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")
]
)
]
)
  1. After Claude makes changes, I verify critical sections weren’t touched.

Decision Tree: When to Add Labels

When to label code
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 needed

Signs 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

  1. Claude lacks context about WHY - It sees complexity, not the reason for it
  2. “THIS IS NOT OVER-ENGINEERING” works - It’s absurd but effective
  3. Explain purpose + context + consequence - Give Claude the “why”
  4. CLAUDE.md needs specifics - Vague rules get deprioritized
  5. Proactive beats reactive - Document before Claude touches code
  6. 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