You ask your AI agent to add a feature, and it generates an abstract factory pattern with three layers of indirection, a plugin system, and configuration objects for things that will never change. The code works. It's also ten times more complex than it needs to be.
This isn't a fringe problem. Every developer using Claude Code, Cursor, or Copilot runs into the same thing: AI coding agents default to over-engineering. They love abstraction, they love extensibility, and they love writing "clean architecture" that would make a junior developer proud and a senior developer roll their eyes.
Ponytail (https://github.com/DietrichGebert/ponytail) is a project that addresses this head-on. It's a prompt-engineering approach — packaged as a configurable tool — that forces your AI agent to think like the laziest senior developer in the room. Not lazy as in careless. Lazy as in refusing to write code that doesn't need to exist yet.
The Over-Engineering Problem Is Real
Watch any AI coding agent work and you'll see the same patterns. You ask for a simple data-fetching function, and you get:
- A repository pattern with an interface
- A caching layer "for when you need it later"
- Error handling that catches seventeen edge cases, most of which are impossible
- A configuration object so you can "customize behavior without changing code"
- Comments explaining what the code does, line by line
The agent thinks it's being helpful. It's read every best-practices article and internalized every design pattern. What it hasn't internalized is the most important principle in real software development: don't build what you don't need.
Senior developers learn this through years of maintaining codebases that collapsed under their own weight. The best code is code you never wrote. The best abstraction is no abstraction — until the third time you've copied something. AI agents don't know this. They optimize for completeness and extensibility because that's what their training data rewards.
The result is churn. You spend more time simplifying AI-generated code than you would have spent writing it yourself. Or worse, you ship the over-engineered version, and six months later your team is navigating three unnecessary layers to fix a simple bug.
Why AI Agents Default to Complexity
Understanding why AI agents over-engineer helps you understand how to fix it.
Training data bias. The AI has ingested millions of lines of production code, tutorials, and Stack Overflow answers. Comprehensive, well-abstracted code is overrepresented in public repositories and documentation. Nobody writes blog posts about the time they inlined a function and deleted a file, even though that's often the right call.
Prompt framing. When you say "build a user authentication system," the agent interprets that as a mandate to build a system — complete, extensible, production-grade. It doesn't hear "write the minimum code I need to authenticate a user right now."
Suggestion optimization. AI models are trained to produce outputs that look correct and complete. An interface with five methods looks more complete than a single function that does the job. The model isn't optimizing for maintainability or simplicity; it's optimizing for resemblance to "good code" as represented in its training data.
No skin in the game. The AI agent doesn't have to maintain what it generates. It doesn't pay the cost of navigating unnecessary abstractions six months from now. Every over-engineered solution it suggests is free from consequences — for the AI. You're the one paying the technical debt.
The Lazy Senior Dev Pattern
The best senior developers share a trait: they resist writing code until it's unavoidable. Not because they're lazy in the pejorative sense, but because they've learned that every line of code is a liability. Code needs to be read, understood, tested, debugged, and maintained. The less of it there is, the better.
The lazy senior dev pattern works like this:
-
Write the simplest thing that could possibly work. No interfaces for single implementations. No configuration objects for values that won't change. No abstract factories when a function call does the job.
-
Wait for the third use case. Don't abstract until you've seen the pattern three times. Copy-paste twice. The third time, extract.
-
Optimize for deletion. Write code that's easy to delete. Small functions that do one thing. Minimal dependencies between modules. No premature generalization that locks you into a structure.
-
Default to inline. If a function is called in one place, inline it. If a constant is used once, hardcode it. Create abstractions only when duplication is proven, not anticipated.
-
Question every layer. Before adding a layer of indirection, ask: what would break if I didn't add this? If the answer is "nothing right now," don't add it.
This isn't about writing bad code or skipping error handling. It's about writing exactly the code that's needed and nothing more. The code should be correct, readable, and minimal.
Introducing Ponytail
Ponytail (https://github.com/DietrichGebert/ponytail) operationalizes the lazy senior dev pattern as a prompt configuration for AI coding agents. Instead of manually telling your agent "keep it simple" every time you make a request — and then still getting an abstract factory because the model's defaults are stronger than your ad-hoc instructions — Ponytail provides a structured system prompt that reorients the agent's behavior from the ground up.
The project is written in JavaScript and works with agent setups that allow custom system prompts or instruction files. At its core, Ponytail injects a set of principles that counteract the AI's natural tendency toward over-engineering. These principles echo what experienced developers actually do when they write code: prioritize simplicity, delay abstraction, and resist the urge to build for hypothetical futures.
What makes Ponytail different from just adding "keep it simple" to your prompt is specificity. Telling an AI to "write simple code" doesn't work because the model's definition of simple doesn't match yours. You have to be explicit about what simplicity means in practice: no interfaces for single implementations, no configuration objects for hardcoded values, no early abstraction without three concrete use cases, no speculative error handling. Ponytail encodes these rules precisely.
Configuring Your Agent for Simpler Code
If you're using an AI coding agent that supports custom system prompts — Claude Code, Cursor, or similar tools — you can apply the lazy senior dev pattern directly. Here's what an effective configuration looks like, based on the principles in Ponytail.
First, create a system prompt or instruction file that explicitly defines the rules:
## Code Generation Principles
1. Write the minimum code to solve the stated problem. Do not add features,
layers, or abstractions that were not explicitly requested.
2. Do not create interfaces for single implementations. Use concrete
implementations directly.
3. Do not add configuration objects for values that will not change. Hardcode
values until there's a demonstrated need to change them.
4. Do not abstract until you have seen the same pattern three times. Copy
code twice before extracting a shared function or module.
5. Do not add error handling for theoretically possible but unobserved edge
cases. Handle the errors you know about.
6. Do not add comments that repeat what the code says. Add comments only
to explain why, not what.
7. Favor inline code over extracted functions when the function is called
in only one place and is short enough to understand at a glance.
8. When in doubt, write less code.
This isn't just about style. Each rule targets a specific over-engineering pattern that AI agents produce by default. The "three times" rule is particularly important — it directly contradicts the AI's instinct to abstract on first sight.
For tools like Claude Code, you can place this in your CLAUDE.md file. For Cursor, it goes in your .cursorrules file. For any agent that reads instruction files from the repo, drop it in the appropriate location. Ponytail (https://github.com/DietrichGebert/ponytail) provides a ready-made version of these principles that you can incorporate directly.
Reducing AI Code Complexity in Practice
Let's look at a concrete example. You ask an AI agent to "add logging to the API handlers."
Without lazy-senior-dev instructions, you might get:
class Logger {
constructor(config) {
this.level = config.level || 'info';
this.format = config.format || 'json';
this.output = config.output || 'console';
}
log(level, message, meta) {
if (this.shouldLog(level)) {
const entry = this.formatEntry(level, message, meta);
this.write(entry);
}
}
shouldLog(level) {
const levels = ['debug', 'info', 'warn', 'error'];
return levels.indexOf(level) >= levels.indexOf(this.level);
}
formatEntry(level, message, meta) {
if (this.format === 'json') {
return JSON.stringify({ level, message, ...meta, timestamp: Date.now() });
}
return `[${level}] ${message}`;
}
write(entry) {
if (this.output === 'console') {
console.log(entry);
}
}
}
// API handler usage
const logger = new Logger({ level: 'info' });
app.get('/users', (req, res) => {
logger.log('info', 'Fetching users', { path: req.path });
// ... handler logic
});
That's a full logging framework for a problem that might just need console.log. The configuration object, the log level system, the format abstraction — all of it is scaffolding for functionality you didn't ask for and may never need.
With lazy-senior-dev instructions, you get something closer to this:
app.get('/users', (req, res) => {
console.log('[info] Fetching users', req.path);
// ... handler logic
});
If you later need log levels, structured output, or multiple transports, add them then. Right now, you need logging in your API handlers. Two lines accomplish that. Done.
The difference isn't just line count. It's cognitive load. The over-engineered version requires anyone reading the codebase to understand the Logger class, its configuration schema, and its method chain before they can understand what should be a trivial operation. The simple version requires understanding console.log, which every JavaScript developer already knows.
This principle scales. Every unnecessary abstraction you avoid is a module nobody has to review, a test nobody has to write, a dependency nobody has to track, and a concept nobody has to learn. The compound effect across a codebase is enormous.
Prompt Engineering for Simpler Output
Beyond system prompts, the way you frame your individual requests matters. Here are practical techniques that work across tools.
Be explicit about scope. Instead of "build a caching system," say "add an in-memory cache to the getUser function." The first framing invites a system. The second invites a few lines of code.
Specify what not to build. "Add rate limiting to this endpoint. Don't create a general-purpose middleware framework for rate limiting. Just make it work for this one route." The negative constraint is often more powerful than the positive one.
Request the simplest version first. "Write the simplest possible version of X. No error handling, no configuration, no extensibility. Just the core logic." You can always ask for additions later. It's much easier to add complexity to a simple base than to remove complexity from an over-engineered one.
Reference Ponytail directly. If you're using a tool that reads repository instructions, adding Ponytail's (https://github.com/DietrichGebert/ponytail) principles to your project's instruction file means every interaction with your AI agent is shaped by the lazy-senior-dev philosophy. You don't have to repeat yourself. The context carries forward.
In Practice, Start Small
The next time your AI agent hands you a "solution" with more files than features, remember: you're the one maintaining this code. The agent doesn't have to live with its decisions. You do.
Here's where to start: take Ponytail's (https://github.com/DietrichGebert/ponytail) principles, drop them into your agent's instruction file, and try it on your next task. Compare the output to what you were getting before. You'll likely see fewer interfaces, less unnecessary abstraction, and code that does what you asked without also building the hotel around the room.
If the simpler code breaks, add the complexity you actually need. That's how senior developers work. They don't build for a future that might not arrive. They build for today, and they refactor when tomorrow proves them wrong. Your AI agent should do the same.