An idiot admires complexity, a genius admires simplicity.
Your AI agent doesn't fix bugs. It patches them. Conditionals on top of conditionals, error handling for things that shouldn't happen, complexity added to fight existing complexity. Every ticket gets a bandage. The pipe keeps leaking.
Plumber fixes the pipe.
5 tasks. Same model. Same prompt. Vanilla vs Plumber.
| Task | LOC Δ | Conditions Δ | Token Δ |
|---|---|---|---|
| Request handler | -18% | -25% | ~+1% |
| Retry with backoff | — | — | ~+1% |
| Config loader | — | — | +1.1% |
| Multi-format export | +27% | -25% | +1.2% |
| Logging decorator | +30% | — | +1.2% |
| Average | +12% | -18% | ~+1% |
Plumber wrote more lines on average — not fewer. The real metric is conditions: -18% fewer branches across all tasks. Token cost is negligible (~1% overhead).
The biggest wins aren't in the table: sentinel variables eliminated, catch-all exceptions replaced with specific ones, hardcoded lists replaced with formulas, booleans parsed safely. See benchmarks/ for full case studies.
The null crash
You report a null crash. Your agent adds three null checks.
def get_user_name(user):
if user is None:
return "Unknown"
if not hasattr(user, 'name'):
return "Unknown"
if user.name is None:
return "Unknown"
return user.namePlumber finds the unauthenticated route that should never have passed null here and kills it at the source.
# plumber: fixed the route guard — null shouldn't reach this function
def get_user_name(user):
return user.nameThe copy-paste guard
The same check ends up in every function because nobody fixed the caller.
def send_email(user):
if not user.email_verified:
return
...
def send_notification(user):
if not user.email_verified:
return
...
def send_sms(user):
if not user.email_verified:
return
...Plumber puts one guard where it belongs and removes the rest.
# plumber: @require_verified_email on the route — unverified users never reach these
def send_email(user): ...
def send_notification(user): ...
def send_sms(user): ...The phantom try/except
Your agent wraps everything in try/except just in case.
def get_config(key):
try:
return config[key]
except KeyError:
return None
except TypeError:
return None
except AttributeError:
return NonePlumber uses the one line the stdlib already provides.
def get_config(key):
return config.get(key)The isinstance chain
Your agent makes a function accept "anything" instead of fixing what calls it.
def process(data):
if isinstance(data, str):
data = json.loads(data)
elif isinstance(data, bytes):
data = json.loads(data.decode())
elif isinstance(data, list):
data = {"items": data}
return transform(data)Plumber normalizes at the boundary and keeps the function clean.
# plumber: deserialize at the API layer — process() only ever sees a dict
def process(data: dict):
return transform(data)Before touching any code, the agent runs this silently:
1. What is the actual problem? (not the symptom)
2. Is this patch covering a design flaw? → fix the design
3. Adding complexity to fight complexity? → redesign
4. Handling cases that shouldn't exist? → delete them
5. What can be removed? → strip it
6. Simple or just compact? short ≠ simple
7. Write the minimum. Refactor. Repeat.
Redesigns are marked plumber: so they read as intent, not accident.
Claude Code
/plugin marketplace add akshatnerella/plumber
/plugin install plumber@plumber
For Codex, Cursor, Windsurf, Cline, Kiro, Copilot, OpenCode, and more — see INSTALLATION.md.
| Command | |
|---|---|
/plumber [lite | full | ultra | off] |
Set intensity |
/plumber-diagnose |
Show root cause breakdown before acting |
/plumber-review |
Flag patchwork in the current diff |
/plumber-audit |
Scan the whole repo for accumulated patches |
/plumber ultra for when the codebase has wronged you personally.
