Claude Code Creates a Pile of Unused Helpers

Finishes the task, also adds 5 "might be useful later" utility functions nobody calls. Bound scope in the prompt and in CLAUDE.md, delete on review.

You asked Claude Code to add a formatPrice function. The PR adds formatPrice — and also formatCurrency, formatPercent, formatCompact, parseAmount, and validatePrice. None of those five are called anywhere. The PR description says “complete formatting utilities suite.” You wanted one function; you got a small framework.

This is over-engineering on autopilot. Claude defaults to “complete solutions” because that’s what gets praised in its training data. On a real codebase, every unused export becomes maintenance burden, confusion for future devs, and surface area for bugs. The fix: bound scope at the prompt + CLAUDE.md level, and ruthlessly delete on review.

Common causes

Ordered by hit rate, highest first.

1. Prompt didn’t bound the scope

“Add a price formatter” doesn’t say “only the one I need.” Claude assumes “any reasonable utility related to prices” is in scope and ships the full set.

How to spot it: Your prompt is generic (“add X helpers”). Re-prompt with “add EXACTLY formatPrice(cents): string. Nothing else.”

2. Claude defaults to “comprehensive solutions”

Training data is full of “complete X library” examples. When asked for a function, Claude tends to provide a function + its 4 obvious siblings, because that’s what looks “professional.”

How to spot it: The unused helpers form a natural family with the requested one. That’s the autopilot mode.

3. No existing utils file to anchor against

Greenfield area means Claude has no reference for “how big a utility module should be.” It invents a complete-looking module by default.

How to spot it: The new file lives in a directory with no peers. Claude designed a module shape from scratch.

4. Task implicitly suggested a “library” framing

“Build a date utility module” reads as “build a library of date utilities.” Claude builds the library. The actual ask might have been “I need formatDateForInvoice.”

How to spot it: Words like “module,” “library,” “suite,” “set of utilities” in the prompt. These authorize plurality.

5. CLAUDE.md silent on YAGNI

Without an explicit “don’t design for future use” rule, Claude’s default is “design for plausible future.” YAGNI (You Aren’t Gonna Need It) has to be made an explicit policy, not a culture.

How to spot it: cat CLAUDE.md | grep -i "yagni\|unused\|only what's needed" returns nothing.

6. Claude saw similar libraries in training and replicated their shape

date-fns has hundreds of exports. Claude saw it and thinks “date utilities means lots of exports.” Your project doesn’t need all that — but Claude can’t tell from context.

How to spot it: Helpers mirror a popular library’s API. Claude pattern-matched, didn’t design for your need.

Shortest path to fix

Ordered by ROI. Step 1 + 2 prevent over-engineering before it happens.

Step 1: Use scope-bounded prompts

Add EXACTLY this:
- Function: formatPrice(cents: number, currency: "USD"): string
- File: src/lib/formatPrice.ts
- Test: src/lib/formatPrice.test.ts
- 3 test cases: zero, normal, large amounts

DO NOT add:
- Other formatters (formatCurrency, etc.) — even if they seem useful
- A "utilities barrel" or index file
- Helper functions only used internally — inline them
- Documentation beyond JSDoc on the export

The “DO NOT” list is the lever — without it, Claude fills with plausible additions.

Step 2: Add YAGNI rule to CLAUDE.md

## YAGNI policy

- Write only the code required for the current task.
- Do not design for future requirements unless they're scheduled.
- A new helper requires ≥2 callers to ship. One-caller code stays inlined.
- A new abstraction requires 3 concrete uses. Premature abstraction is rejected at review.
- "Might be useful later" is not a reason to add code. Delete and re-add later when needed.

This makes the default behavior aligned, not just per-task.

Step 3: On review, delete every unused export

After the PR lands, run:

# Find unused exports (using ts-unused-exports or similar)
pnpm dlx ts-unused-exports tsconfig.json

# Or check call graph
pnpm dlx knip

For each unused export, delete it. Don’t merge with unused code “just in case.”

Step 4: Reject the broader file shape, re-prompt narrower

If Claude returned a “complete utilities” file:

Your file has 6 exports; the task needed 1. Delete:
- formatCurrency
- formatPercent
- formatCompact
- parseAmount
- validatePrice

Keep only `formatPrice`. Re-do the test file to test only `formatPrice`.

This trains the session to stay minimal for the rest of the run.

Step 5: Inline helpers used by exactly one caller

If Claude created an internal helper used in one place, inline it:

// Bad — single-use helper
function calculateTax(amount: number): number {
  return amount * 0.08;
}

function checkout(cart: Cart): Receipt {
  const tax = calculateTax(cart.subtotal);
  // ...
}

// Good — inlined, no helper
function checkout(cart: Cart): Receipt {
  const tax = cart.subtotal * 0.08;
  // ...
}

Helpers are for repeated use; one-shot logic stays inline.

Step 6: Audit existing helpers quarterly

Helpers accumulate silently. Run:

# How many exports does each utils file have?
find src/lib src/utils -name "*.ts" -exec sh -c \
  'echo "$(grep -c "^export" "$1") $1"' _ {} \; | sort -nr | head -20

Files with many exports get reviewed: which exports are actually called? Drop the unused ones.

Prevention

  • CLAUDE.md YAGNI policy: a new helper needs ≥2 callers, a new abstraction needs ≥3 uses
  • Every code prompt has an explicit “DO NOT add” list to block plausible additions
  • Use ts-unused-exports or knip in CI; fail PRs that introduce unused exports
  • Reject “might be useful later” reasoning in code review — re-add when actually needed
  • Avoid framing prompts as “build a library / module / utility suite” — those authorize plurality
  • One-shot logic stays inlined; helpers are for repeated use only

Tags: #Claude Code #Debug #Troubleshooting #YAGNI #Over-engineering