vibeeval/ai-slop-cleaner
Post-implementation cleanup that removes AI-generated bloat while preserving functionality. Runs pass-by-pass with test verification after each pass. Activate after kraken/spark complete a feature, or when a codebase needs hygiene work.
npx skills add https://github.com/vibeeval/vibecosystem --skill ai-slop-cleaner
AI code generation produces working code. It also produces unnecessary code alongside it. This skill removes the unnecessary parts while keeping everything that matters.
AI slop is code that:
Common slop categories and their signals:
| Category | Signal |
|----------|--------|
| Dead imports | Imported but never referenced in the file |
| Unused variables | Declared, never read |
| Commented-out code | Blocks of // old code or /* removed */ |
| Debug remnants | console.log, print(), debugger, fmt.Println |
| Obvious comments | // increment counter above count++ |
| Redundant JSDoc | @param name - the name above name: string |
| Premature abstractions | A factory that creates exactly one thing |
| One-use helpers | Private function called exactly once, trivially inlinable |
| Overly generic types | <T extends object> when T is always User |
| Over-parameterized | fn(a, b, c, d, e) where 4 params never vary |
| Unreachable branches | if (false) or if (isLoggedIn && !isLoggedIn) |
| Speculative features | Code paths for requirements that don't exist |
| Copy-paste duplication | Two blocks identical except one variable name |
| Placeholder remnants | TODO: implement, lorem ipsum, example data in prod |
Tests are sacred. Never clean test files.
Tests exist to protect behavior. Any cleanup that breaks a test reveals that the "slop" was actually load-bearing. That is good information. The test wins.
BEFORE ANYTHING: Run full test suite → all tests must pass (baseline)
FOR EACH PASS:
1. Identify targets for this pass category
2. Apply cleanup
3. Run tests
4. If tests pass: keep cleanup, continue
5. If tests fail: git checkout -- . (revert), skip this pass category
6. Log what was reverted and why
AFTER ALL PASSES: Run full test suite → confirm all tests still pass
Report: lines removed, files touched, passes skipped, reason for each skip
Never batch multiple pass categories together. If combined changes break a test, you cannot know which change caused it.
Risk: Very Low
What to remove:
let/const/var that are never read after assignment_)Before:
import { useState, useEffect, useCallback, useMemo } from 'react'
import { formatDate } from '@/lib/utils'
import { ApiClient } from '@/lib/api'
export function UserCard({ user }) {
const [count, setCount] = useState(0)
const formatted = formatDate(user.createdAt)
return <div>{user.name}</div>
}
After:
import { useState } from 'react'
import { formatDate } from '@/lib/utils'
export function UserCard({ user }) {
const [count, setCount] = useState(0)
const formatted = formatDate(user.createdAt)
return <div>{user.name}</div>
}
Note: count, setCount, and formatted are still present because they may be used elsewhere in a larger component. Pass 1 only removes imports.
Risk: Very Low
What to remove:
console.log, console.debug, console.warn (unless it is a legitimate error logger)debugger statementsprint() in Python when not serving as actual program outputfmt.Println in Go debug instrumentationBefore:
async function processOrder(orderId: string) {
console.log('processing order', orderId)
const order = await db.orders.findById(orderId)
// const cached = await cache.get(orderId)
// if (cached) return cached
console.log('order fetched:', order)
const result = await payments.charge(order)
// TODO: add retry logic here
// console.log('charge result', result)
return result
}
After:
async function processOrder(orderId: string) {
const order = await db.orders.findById(orderId)
const result = await payments.charge(order)
// TODO: add retry logic here
return result
}
Rule: // TODO: comments are preserved. They are documentation of known gaps, not slop.
Risk: Low
What to remove:
@param blocks that just repeat the parameter name and type (TypeScript already says this)// ===== COMPONENT =====)} // end if, } // end for)Before:
/**
* Gets a user by ID.
* @param id - the user ID
* @param db - the database instance
* @returns the user object
*/
async function getUserById(id: string, db: Database): Promise<User> {
// Query the database for the user
const user = await db.users.findById(id)
// Return the user
return user
} // end getUserById
After:
async function getUserById(id: string, db: Database): Promise<User> {
return db.users.findById(id)
}
Keep comments that explain WHY (business rules, performance choices, known gotchas). Remove comments that explain WHAT (the code already says what).
Risk: Medium — Run tests immediately after
What to remove:
return, throw, or breakBefore:
function getStatus(user: User): string {
if (user.role === 'admin' || user.role === 'admin') {
return 'ADMIN'
}
if (user.isActive) {
return 'ACTIVE'
} else {
return 'INACTIVE'
}
// This never runs
return 'UNKNOWN'
}
After:
function getStatus(user: User): string {
if (user.role === 'admin') {
return 'ADMIN'
}
return user.isActive ? 'ACTIVE' : 'INACTIVE'
}
Do not remove branches that look unreachable but depend on runtime data you cannot verify statically. When uncertain, leave it.
Risk: Medium — Run tests immediately after
What to inline:
Before:
function formatUserDisplayName(user: User): string {
return `${user.firstName} ${user.lastName}`.trim()
}
function renderUserCard(user: User) {
const displayName = formatUserDisplayName(user)
return `<div class="card">${displayName}</div>`
}
After (if formatUserDisplayName is only called from renderUserCard):
function renderUserCard(user: User) {
const displayName = `${user.firstName} ${user.lastName}`.trim()
return `<div class="card">${displayName}</div>`
}
Do NOT inline if:
Risk: Medium-High — Run tests immediately after each consolidation
What to consolidate:
Before:
// In UserService
async function getActiveUsers() {
const users = await db.query(
'SELECT * FROM users WHERE status = $1 AND deleted_at IS NULL',
['active']
)
return users.rows
}
// In AdminService (same file or different file)
async function getActiveAdmins() {
const admins = await db.query(
'SELECT * FROM users WHERE status = $1 AND deleted_at IS NULL AND role = $2',
['active', 'admin']
)
return admins.rows
}
After:
async function getActiveUsers(role?: string) {
const params: unknown[] = ['active']
let sql = 'SELECT * FROM users WHERE status = $1 AND deleted_at IS NULL'
if (role) {
sql += ' AND role = $2'
params.push(role)
}
const result = await db.query(sql, params)
return result.rows
}
Be careful: consolidation that requires complex parameterization may make code harder to understand. If the consolidated version is more complex than the two originals, leave them separate.
Risk: High — High scrutiny, run tests after every individual change
What to simplify:
Before:
interface DataProcessor<T extends BaseData> {
process(data: T): ProcessedData<T>
validate(data: T): ValidationResult
}
class UserDataProcessorFactory {
create(): DataProcessor<UserData> {
return new UserDataProcessor()
}
}
class UserDataProcessor implements DataProcessor<UserData> {
process(data: UserData): ProcessedData<UserData> {
return { ...data, processed: true }
}
validate(data: UserData): ValidationResult {
return { valid: !!data.id }
}
}
After (if only UserData ever flows through this):
function processUserData(data: UserData) {
if (!data.id) throw new Error('Invalid user data: missing id')
return { ...data, processed: true }
}
Rule for Pass 7: If you need more than 2 minutes to understand why an abstraction exists, and there is only one concrete case, remove the abstraction. If you find yourself unsure whether it is load-bearing, leave it. Pass 7 is optional.
| Target | Reason |
|--------|--------|
| Test files (*.test.*, *.spec.*, __tests__/) | Tests are sacred |
| Public API signatures | Breaks callers |
| Error handling at system boundaries (API routes, top-level handlers) | Defense-in-depth |
| Comments explaining regulatory/compliance requirements | Legal context |
| Feature flags | May be toggled at runtime |
| Anything marked // KEEP or // intentional | Explicit author decision |
Place at project root to exclude paths:
# .slopignore
src/legacy/ # Old code being migrated, don't touch
src/generated/ # Auto-generated, cleaned by generator
vendor/ # Third-party code
After all passes complete, output:
AI Slop Cleaner Report
======================
Files touched: 12
Lines removed: 147
Lines remaining: 1,843
Reduction: 7.4%
Pass results:
Pass 1 (Dead imports): DONE — 23 lines removed
Pass 2 (Debug code): DONE — 18 lines removed
Pass 3 (Obvious comments): DONE — 41 lines removed
Pass 4 (Dead code): DONE — 12 lines removed
Pass 5 (One-use helpers): DONE — 31 lines removed
Pass 6 (Duplication): SKIPPED — test failed after consolidation (UserService)
Pass 7 (Over-engineering): DONE — 22 lines removed
Skipped details:
Pass 6 reverted: UserService query consolidation broke getUsersByStatus test.
Root cause: test was asserting on the exact SQL string. Left original.
Test status: ALL PASSING (127/127)
AI slop cleaner runs at the code level (syntactic cleanup). The refactor-cleaner agent runs at the architecture level (structural refactoring). Run this skill first, then refactor-cleaner if structural improvement is needed.
Order:
1. ai-slop-cleaner (remove the noise)
2. code-reviewer (verify quality after cleanup)
3. refactor-cleaner (structural improvements if needed)
4. verifier (final gate)
This skill is automatically triggered after:
The trigger runs Pass 1 and Pass 2 only by default (very low risk). Passes 3-7 require explicit activation or a /clean command.
Remember: The goal is not minimum lines of code. The goal is maximum clarity per line. If removing something makes the code harder to understand, put it back.
Take vibeeval/ai-slop-cleaner from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.