I asked Claude to fix a function. It rewrote three.
I asked for a null check. It added five guard clauses, two of which protected against states the type system already prevents.
I asked for one comment. It wrote a JSDoc block longer than the function it described.
Every AI coding tool I've used does the same thing. The default move is to add. Most of the time, the right move is to delete.
TLDR
Five things AI keeps adding that I delete on sight:
- Comments that restate what the code does
- Defensive guards for states the type system already prevents
- Premature abstractions for one caller
- Try/catch around code that can't throw
- Verbose error messages that leak internals
Five ways to make Claude Code stop adding them:
- Run
/simplifyafter any AI write - A CLAUDE.md anti-bloat rules block
- A "trimmer" subagent that proposes deletes
- Plan mode + an Architect agent that locks scope before code starts
- A PostToolUse hook that flags bloat metrics
Code and configs below.
The 5 things to delete
1. Comments that restate what the code does
// Increment the counter by 1
counter++
// Set the user object to null
user = null
// Loop through each item in the array and add it to the total
for (const item of items) {
total += item
}
Well-named identifiers do this job already. The comment adds reading load and rots first when the code changes.
Comments earn their place by explaining why: a hidden constraint, a subtle invariant, a workaround for a specific bug, behavior that would surprise the next reader. Restating the line above? Delete it.
2. Defensive guards for states the type system already prevents
function greet(name: string) {
if (!name) return ''
if (typeof name !== 'string') return ''
return `Hello, ${name}`
}
The signature already says name: string. The typeof check protects against nothing. The !name covers empty string, which is a valid input until the spec says otherwise.
If untrusted data enters the system, validate at that boundary. Inside trusted code, trust the signatures.
3. Premature abstractions for one caller
function getUserDisplayName(
user: User,
options: { useFullName?: boolean; fallback?: string } = {}
) {
const { useFullName = false, fallback = 'Unknown' } = options
return useFullName
? `${user.firstName} ${user.lastName}`
: (user.username || fallback)
}
// Only caller in the codebase:
getUserDisplayName(currentUser)
The options bag is hypothetical optionality. One caller, one shape, one outcome. Inline it.
The wrong abstraction costs more than three repeated lines. If a second caller appears with different needs, refactor then. Not now.
4. Try/catch around code that can't throw
try {
const result = a + b
return result
} catch (err) {
console.error('addition failed', err)
return 0
}
Addition does not throw. JSON parsing throws. Network calls throw. File reads throw. Add try/catch where the failure is real.
The pattern shows up in subtler places too: try/catch around pure helpers, around getters on already-loaded data, around JSON.stringify of objects that can't be circular. Every dead catch hides the next live one.
5. Verbose error messages that leak internals
throw new Error(
`Failed to fetch user data for userId ${userId} from table users at ` +
`${new Date().toISOString()}. Database connection used pool size 10 with ` +
`timeout 5000ms. SQL was: SELECT * FROM users WHERE id = $1`
)
Two errors fighting each other. Too much detail to throw at the user (table name, SQL, internal pool stats). Too little signal to debug from (no trace, no upstream caller, no retry context).
Throw the minimum the caller needs to recover. Log the diagnostic detail separately, structured, where the operator looks for it.
The 5 ways to stop AI from adding them
Deleting is the cheap move. Preventing is the systemic one. Each layer below catches what the previous layer missed.
1. Run /simplify after any AI write
Claude Code ships with a built-in simplify skill:
Review changed code for reuse, quality, and efficiency, then fix any issues found.
After Claude writes or edits a file, type /simplify. The skill reads the diff and proposes deletions and consolidations. One slash command, one round of trimming.
This is reactive. It catches what already landed on disk. Useful as a tax, not as a strategy. The next four layers prevent the work from happening in the first place.
2. Drop an anti-bloat block into your CLAUDE.md
Claude Code reads CLAUDE.md at the start of every session. Put the rules in writing and most of the bloat never lands:
# Writing code
- Don't add features, refactor, or introduce abstractions beyond what the task requires. A bug fix doesn't need surrounding cleanup. Three similar lines is better than a premature abstraction.
- Don't add error handling, fallbacks, or validation for scenarios that can't happen. Trust internal code and framework guarantees. Only validate at system boundaries (user input, external APIs).
- Default to writing no comments. Only add one when the WHY is non-obvious: a hidden constraint, a subtle invariant, a workaround for a specific bug, behavior that would surprise a reader.
- Don't explain WHAT the code does. Well-named identifiers already do that. Don't reference the current task, fix, or callers; those belong in the PR description.
These are paraphrased from Anthropic's own internal policy for Claude. The fact that Anthropic writes this down for its own model tells you what the default is.
Drop the block into the project's CLAUDE.md (or your user-scope one at ~/.claude/CLAUDE.md). Most of the patterns from the first half stop happening before they reach disk.
3. Build a "trimmer" subagent
CLAUDE.md tells Claude what to do. A subagent with fresh context and the right scope makes sure it does it.
Create ~/.claude/agents/trimmer.md:
---
name: trimmer
description: Use after any code is written to find what should be deleted. Reviews the diff and proposes minimal cuts. Read-only, never edits. Invoke when the user says "trim this" or after Engineer finishes any non-trivial implementation.
model: haiku
tools: Read, Grep, Glob, Bash
---
You find what to delete. You do not refactor, rewrite, or improve. You delete.
For each file changed in the current diff:
1. Read the full file, not just the diff.
2. List every line you would cut, with file:line and a one-sentence reason.
3. Group by reason: dead comment, redundant guard, premature abstraction, unused branch, leaky error.
4. End with a verdict: total lines proposed for deletion, and whether the diff still works after.
You may not propose rewrites. You may only propose deletions. If you find code that should change rather than disappear, name it but do not touch it.
Run it with Use the trimmer agent on the current diff. It returns a delete-only patch you approve in seconds.
Haiku is the right tier here. The task is pattern matching, not deep reasoning, and Haiku writes terser output than the bigger models. Pick a heavier model and the trimmer starts explaining itself instead of cutting.
4. Plan mode + an Architect agent
The cheapest bloat is the bloat that never gets written. Plan mode forces a written plan before any code happens. Pair it with a strict Architect subagent and you stop the problem at the source.
The Architect:
---
name: architect
description: Use before any non-trivial implementation. Designs the plan before code is written. Read-only.
model: opus
tools: Read, Grep, Glob, WebSearch, WebFetch
---
You reject vague scope. "Add validation" is not a plan. "Reject empty strings in parseInput() at src/handlers.ts:42 and return HTTP 400 with body { error: 'name required' }" is a plan.
Every step in the plan names:
- The file and approximate location
- The exact change (signature, return shape, error message)
- How it's verified (test name, manual check, type signature)
If the plan says "fix function X," the Engineer is not allowed to refactor function Y "while in there." Scope is fixed at planning time.
Plan mode plus a scope-strict Architect cuts the most expensive form of bloat: code Claude writes because nobody told it where to stop.
5. A PostToolUse hook for bloat metrics
The first post in this series argued that hooks turn instructions into guarantees. Same idea applies here.
A short hook that runs after every Write or Edit and flags bloat-adjacent metrics:
#!/usr/bin/env bash
# ~/.claude/hooks/bloat-watch.sh
# PostToolUse hook: warn Claude if the file looks bloated after writing.
# Exits 2 with stderr when warnings fire so Claude sees them on the next turn.
set -euo pipefail
input="$(cat)"
file="$(printf '%s' "$input" | jq -r '.tool_input.file_path // ""')"
[[ -n "$file" && -f "$file" ]] || exit 0
# Only check source files
case "$file" in
*.ts|*.tsx|*.js|*.jsx|*.go|*.py|*.rs) ;;
*) exit 0 ;;
esac
total=$(wc -l < "$file" | tr -d ' ')
# grep -c always prints the count even when zero; the `|| true` swallows the exit 1 it returns on no-match.
comments=$(grep -cE '^[[:space:]]*(//|#)' "$file" || true)
trycatch=$(grep -cE '^[[:space:]]*(try|catch|except)' "$file" || true)
warned=0
# Comment density over 25% in files with more than 30 lines.
if (( total > 30 )) && (( comments * 4 > total )); then
echo "Warning: ${file} has ${comments}/${total} lines as comments. Consider trimming the ones that restate the code." >&2
warned=1
fi
# Many try/catch blocks in a single file.
if (( trycatch > 6 )); then
echo "Warning: ${file} has ${trycatch} try/catch blocks. Audit which failures are real and remove the rest." >&2
warned=1
fi
(( warned )) && exit 2
exit 0
Register it under PostToolUse: { matcher: "Write|Edit" } in ~/.claude/settings.json. The hook never blocks. It writes to stderr, which Claude reads on the next turn and adjusts.
Tune the thresholds. Add more checks: new files created when the task didn't mention adding files, lines added in a single tool call, ratio of new exports to existing ones. The shape is the same: measure, surface, let Claude correct.
How the layers fit together
A normal task with all five layers in play:
- You ask Claude to "add rate limiting to the /signals endpoint."
- Plan mode + Architect produce a written plan that names the file, the exact middleware, the test to update.
- CLAUDE.md anti-bloat rules keep Engineer from adding a Redis abstraction "in case we need to swap stores later."
- Engineer writes the code.
- PostToolUse bloat hook logs that the new middleware file has a comment density of 30%, Claude trims on the next turn.
-
/simplifycatches one redundant guard the hook missed. - Trimmer subagent finds three more lines to drop.
- Final diff is half the size it would have been.
Most days you use one or two layers, not all five. The point is that the discipline lives outside your head. You don't have to remember to delete. The setup reminds you.
What this changes
AI-written code is a first draft. Treat it like one.
Your bills come down. Your reviews get faster. The codebase stops growing for reasons no one can name a quarter later.
Delete more. Ship less.
Configs and scripts in this post are MIT-licensed and copy-pasteable. The first post in this series, on Claude Code hooks, is here.
Top comments (0)