Inspired by Claude Code Simplifier plugin. Here it is adapted into a model-agnostic, process-driven skill for any AI coding agent to use.
Overview
Simplify code by reducing complexity while precisely preserving behavior. The goal is not to reduce line count, but to make the code easier to read, understand, modify, and debug. Every simplification must pass a simple test: "Would a new team member understand it faster than they would understand the original version?"
When to use
- The functionality already works and tests pass, but the implementation feels heavier than it needs to be
- Readability or complexity issues are found during code review
- You encounter deeply nested logic, long functions, or unclear naming
- Refactor code written under time pressure
- Merge related logic scattered across multiple files
- After merging changes that introduced duplication or inconsistency
When not to use:
- The code is already clean and readable; do not simplify for the sake of simplifying
- You do not yet understand what the code is doing; understand first, then simplify
- The code is on a performance-critical path, and the "simpler" version would be noticeably slower
- You are about to completely rewrite the module; simplifying code that is about to be discarded is a waste of effort
Five principles
1. Precisely preserve behavior
Do not change what the code does, only how it is expressed. All inputs, outputs, side effects, error behavior, and edge cases must remain consistent. If you are unsure whether a simplification preserves behavior, do not make it.
ASK BEFORE EVERY CHANGE:
→ Does this produce the same output for every input?
→ Does this maintain the same error behavior?
→ Does this preserve the same side effects and ordering?
→ Do all existing tests still pass without modification?
2. Follow project conventions
Simplifying means making code more consistent with the codebase, not imposing external preferences. Before simplifying:
1. Read CLAUDE.md / project conventions
2. Study how neighboring code handles similar patterns
3. Match the project's style for:
- Import ordering and module system
- Function declaration style
- Naming conventions
- Error handling patterns
- Type annotation depth
A simplification that breaks project consistency is not a simplification, but churn.
3. Clarity over cleverness
If a compact form requires stopping to parse with effort, explicit code is better than compact code.
// UNCLEAR: Dense ternary chain
const label = isNew ? 'New' : isUpdated ? 'Updated' : isArchived ? 'Archived' : 'Active';
// CLEAR: Readable mapping
function getStatusLabel(item: Item): string {
if (item.isNew) return 'New';
if (item.isUpdated) return 'Updated';
if (item.isArchived) return 'Archived';
return 'Active';
}
// UNCLEAR: Chained reduces with inline logic
const result = items.reduce((acc, item) => ({
...acc,
[item.id]: { ...acc[item.id], count: (acc[item.id]?.count ?? 0) + 1 }
}), {});
// CLEAR: Named intermediate step
const countById = new Map<string, number>();
for (const item of items) {
countById.set(item.id, (countById.get(item.id) ?? 0) + 1);
}
4. Keep balance
Simplification also has a failure mode: oversimplification. Watch out for these pitfalls:
- Over-inlining — removing helpers that give concepts names makes call sites harder to read
- Merging unrelated logic — combining two simple functions into one complex function is not simpler
- Removing "unnecessary" abstractions — some abstractions exist for extensibility or testability, not for complexity
- Optimizing for line count — fewer lines is not the goal; easier understanding is the goal
5. Stay within the scope of changes
By default, simplify only recently modified code. Unless explicitly asked to expand the scope, avoid refactoring unrelated code along the way. Unbounded simplification creates diff noise and brings unexpected regression risk.
Simplification process
Step 1: Understand before acting (Chesterton's Fence)
Before modifying or removing anything, understand why it exists. This is Chesterton's Fence: if you see a fence across a road but do not know why it is there, do not tear it down. Understand the reason first, then judge whether that reason still holds.
BEFORE SIMPLIFYING, ANSWER:
- What is this code's responsibility?
- What calls it? What does it call?
- What are the edge cases and error paths?
- Are there tests that define the expected behavior?
- Why might it have been written this way? (Performance? Platform constraint? Historical reason?)
- Check git blame: what was the original context for this code?
If you can't answer these questions, you're not ready to simplify. Read more context first.
Step 2: Identify simplification opportunities
Scan for these patterns. Each one is a concrete signal, not a vague smell:
Structural complexity:
| Pattern | Signal | Simplification approach |
|---|---|---|
| Deep nesting (3+ levels) | Control flow is hard to follow | Extract conditions into guard clauses or helper functions |
| Long functions (50+ lines) | Multiple responsibilities | Split into clearly named, focused functions |
| Nested ternary expressions | Requires a mental stack to parse | Replace with if/else chains, switch, or lookup objects |
| Boolean parameter flags | doThing(true, false, true) |
Replace with options objects or standalone functions |
| Repeated condition checks | The same if check appears in multiple places |
Extract into a well-named predicate function |
Naming and readability:
| Pattern | Signal | Simplification approach |
|---|---|---|
| Generic naming | data, result, temp, val, item |
Rename to describe the content:userProfile, validationErrors |
| Abbreviated naming | usr, cfg, btn, evt |
Unless the abbreviation is common (id, url, api), use the full word |
| Misleading naming | A function named get also modifies state |
Rename to reflect the actual behavior |
| Comments explaining "what" | // increment counter written above count++ written above |
Delete the comment; the code itself is already clear enough |
| Comments explaining "why" | // Retry because the API is flaky under load |
Keep them; they carry intent that the code cannot express |
Redundancy:
| Pattern | Signal | Simplification approach |
|---|---|---|
| Duplicated logic | The same 5+ lines appear in multiple places | Extract into a shared function |
| Dead code | Unreachable branches, unused variables, commented-out code blocks | Remove it (after confirming it is truly dead) |
| Unnecessary abstraction | A wrapper that adds no value | Inline the wrapper and call the underlying function directly |
| Over-engineered pattern | Factory-for-a-factory、strategy-with-one-strategy | Replace with a simple, direct solution |
| Redundant type assertion | Convert to a type that can already be inferred | Remove the assertion |
Step 3: Apply changes incrementally
Make only one simplification at a time. Run tests after every change.Commit refactoring changes separately from feature or bug fix changes. A PR that both refactors and adds features is actually two PRs and should be split.
FOR EACH SIMPLIFICATION:
1. Make the change
2. Run the test suite
3. If tests pass → commit (or continue to next simplification)
4. If tests fail → revert and reconsider
Avoid bundling multiple simplifications into one untested change. If something breaks, you need to know which simplification caused it.
The Rule of 500: If a refactor touches more than 500 lines, invest in automation (codemods, sed scripts, AST transforms) instead of editing by hand. Manual edits at this scale are error-prone and hard to review.
Step 4: Verify the result
After completing all simplifications, step back and evaluate the whole:
COMPARE BEFORE AND AFTER:
- Is the simplified version genuinely easier to understand?
- Did you introduce any new patterns inconsistent with the codebase?
- Is the diff clean and reviewable?
- Would a teammate approve this change?
If the "simplified" version is harder to understand or review, revert. Not every simplification attempt succeeds.
Language-Specific Guidance
TypeScript / JavaScript
// SIMPLIFY: Unnecessary async wrapper
// Before
async function getUser(id: string): Promise<User> {
return await userService.findById(id);
}
// After
function getUser(id: string): Promise<User> {
return userService.findById(id);
}
// SIMPLIFY: Verbose conditional assignment
// Before
let displayName: string;
if (user.nickname) {
displayName = user.nickname;
} else {
displayName = user.fullName;
}
// After
const displayName = user.nickname || user.fullName;
// SIMPLIFY: Manual array building
// Before
const activeUsers: User[] = [];
for (const user of users) {
if (user.isActive) {
activeUsers.push(user);
}
}
// After
const activeUsers = users.filter((user) => user.isActive);
// SIMPLIFY: Redundant boolean return
// Before
function isValid(input: string): boolean {
if (input.length > 0 && input.length < 100) {
return true;
}
return false;
}
// After
function isValid(input: string): boolean {
return input.length > 0 && input.length < 100;
}
Python
# SIMPLIFY: Verbose dictionary building
# Before
result = {}
for item in items:
result[item.id] = item.name
# After
result = {item.id: item.name for item in items}
# SIMPLIFY: Nested conditionals with early return
# Before
def process(data):
if data is not None:
if data.is_valid():
if data.has_permission():
return do_work(data)
else:
raise PermissionError("No permission")
else:
raise ValueError("Invalid data")
else:
raise TypeError("Data is None")
# After
def process(data):
if data is None:
raise TypeError("Data is None")
if not data.is_valid():
raise ValueError("Invalid data")
if not data.has_permission():
raise PermissionError("No permission")
return do_work(data)
React / JSX
// SIMPLIFY: Verbose conditional rendering
// Before
function UserBadge({ user }: Props) {
if (user.isAdmin) {
return <Badge variant="admin">Admin</Badge>;
} else {
return <Badge variant="default">User</Badge>;
}
}
// After
function UserBadge({ user }: Props) {
const variant = user.isAdmin ? 'admin' : 'default';
const label = user.isAdmin ? 'Admin' : 'User';
return <Badge variant={variant}>{label}</Badge>;
}
// SIMPLIFY: Prop drilling through intermediate components
// Before — consider whether context or composition solves this better.
// This is a judgment call — flag it, don't auto-refactor.
Common Rationalization Excuses
| Rationalization Excuse | Reality |
|---|---|
| "It works, no need to touch it" | Hard-to-read working code is also hard to fix when problems arise. Simplifying now will save time on every future modification. |
| "Fewer lines means simpler" | A one-line nested ternary expression is not simpler than five lines of if/else. Simplicity is about speed of understanding, not line count. |
| "I'll just quickly simplify this unrelated code while I'm at it" | Unscoped simplification creates noisy diffs and introduces regression risk to code you didn't intend to modify. Stay focused. |
| "The types make it self-explanatory" | Types record structure, not intent. Well-named functions explain more than type signatures do why。 |
| "This abstraction might be useful later" | Don't keep speculative abstractions. If it's not needed now, it's complexity with no value. Remove it, and add it back when needed. |
| "The original author must have had a reason" | Maybe. Check git blame, apply Chesterton's Fence. But accumulated complexity often has no reason, just residue from iterating under pressure. |
| "I'll refactor while adding features" | Separate refactoring from feature development. Mixed changes are harder to review, roll back, and understand in history. |
Red Flags
- Simplifications that require modifying tests to pass (you probably changed behavior)
- The "simplified" code is longer and harder to follow than before
- Renaming to match personal preference rather than project conventions
- Removing error handling because "the code is cleaner this way"
- Simplifying code you don't fully understand
- Packing many simplifications into one large, hard-to-review commit
- Refactoring code outside the current task's scope when not asked
Verification
After completing a simplification:
- All existing tests pass without modification
- Build succeeds with no new warnings
- Linter/formatter passes (no style regressions)
- Each simplification is a reviewable incremental change
- Diff is clean, with no unrelated changes mixed in
- The simplified code follows project conventions (checked against CLAUDE.md or equivalent)
- No error handling was removed or weakened
- No dead code was left behind (unused imports, unreachable branches)
- A teammate or review agent would agree the change is a net improvement