Generative AI promised to eliminate one of the biggest sources of friction in software engineering:writing code. In many ways, it has delivered. Today, an AI coding agent can implement features in minutes that might have taken an engineer an hour. It can generate tests, refactor code, write documentation, navigate large repositories, and even execute development tasks autonomously.
But the real question isn’t whether AI can produce code quickly. It’s whether that code is actually good.
That’s where the productivity story becomes far more nuanced. We feel faster because code is generated almost instantly. Yet moments later, we’re staring at a 2,000-line diff, trying to understand why the agent refactored unrelated files, whether the new retry logic is safe, or which seemingly harmless change introduced a regression.
Code generation has become remarkably inexpensive. Engineering hasn’t.
This is what is called the AI productivity paradox. AI has dramatically reduced the cost of producing code, but the work that truly defines software engineering—understanding, reviewing, validating, integrating, and maintaining that code—has not become proportionally easier. In many cases, we’ve simply shifted the bottleneck from writing software to reasoning about it.
What Is Actually Breaking Down?
To figure out why this keeps happening, we have to look past the hype of “lines of code per minute” and examine how software actually gets built. When you step back and look at where time goes across a project lifecycle, the core issue becomes obvious.
The fundamental mistake is assuming that typing syntax was the primary bottleneck in software engineering. It wasn’t. The real bottlenecks have always been system design, debugging, coordination, testing, and maintainability. This becomes particularly obvious in large engineering organizations and complex open source projects. A change doesn’t live in isolation. It interacts with APIs, dependencies, CI pipelines, security tooling, release processes, downstream consumers, and sometimes several architectures and environments.
When you remove the friction of writing code without fixing our capacity to verify and maintain it, you just move the bottleneck elsewhere. Generating code is not the same thing as delivering software:
- AI makes it effortless to open massive PRs or spit out huge text outputs, but human attention doesn’t scale. We end up buried in endless review queues, context-switching constantly to review generated code.
- While AI is great at pattern-matching within a single file or function, it struggles to preserve boundaries, long-term architecture, and system-wide consistency.
- Generating 300 lines of code only takes five seconds, but verifying that those lines don’t break edge cases or security rules still requires the same amount of human reasoning.
- Metrics like lines of code written or PRs merged go way up, even while actual system reliability and feature quality stay flat or decline.
Lessons from the Trenches: Real Friction
1. The Prompting Overhead Threshold
One of the most frustrating things about working with AI every day is realizing that sometimes the AI isn’t actually saving you time. You ask for a quick fix, and the AI dumps a massive response or refactors half the file.
You end up spending so much time steering the prompt, providing context, and correcting small mistakes that you hit a clear realization: if I had just spent that effort writing it myself, it would have been done faster.
When you reach that point, take the keyboard back! Treat AI as an assistant, not a replacement for taking the wheel. When prompt steering costs more energy than writing the logic yourself, stop prompting and write the code.
2. Context Loss (The Memento Problem)
One of the strangest experiences with coding agents is that they can appear to understand a repository extremely well and then, a few iterations later, behave as if part of the conversation never happened. This feels a bit like Memento movie.
When modifying a codebase, an agent may need to rediscover files, conventions, relationships, and decisions repeatedly. If those constraints aren’t encoded somewhere durable, you’re relying on the model to reconstruct them from the current context every time. That is not a great foundation for maintaining architecture. This is why repository-level instructions matter. Also, having a large context window doesn’t necessarily mean that every important constraint will consistently remain active in the reasoning process.
3. The Default Shell Headache
A subtle issue on macOS is shell configuration. AI coding agents are very good at generating commands. They are less magical when those commands meet the actual environment they’re running in.
Because macOS defaults to zsh, running commands—especially array manipulations or nested SSH commands—can silently fail or confuse the model.
# A practical fix: explicitly set up a POSIX/bash environment for AI tooling
brew install bash
echo "$(brew --prefix)/bin/bash" | sudo tee -a /etc/shells
# Keep your primary terminal shell (zsh) separate from the terminal profile
# you assign to your AI automation tools.The lesson isn’t everyone should switch from zsh to bash.
The lesson is: Your development environment is part of the context the agent needs to understand. Unnecessary context needs to be minimized, so that agents can focus on what matters, not on incidental complexity. Therefore, everybody should switch from zsh to bash, at least if they are an agent, that is.
Practices That Actually Help
Instead of letting AI output wild, unconstrained code, high-performing teams set up hard boundaries.
Write explicit repo rules (AGENTS.md / .cursorrules)
Without repository context, AI predicts code based on generic internet patterns. Putting a simple AGENTS.md or .cursorrules file in your root repository dramatically improves output consistency.
# API Rules
- Services cannot directly access database tables outside their domain.
- Use the repository layer only.
- All external HTTP calls require explicit timeouts and retry handling.
- Use structured logging (no raw console output).
- Do not introduce new dependencies without explicit approval.Use “Vibe Prototyping,” Not “Vibe Production”
“Vibe coding“—steering AI purely through prompts without looking closely at the code—is a great way to learn or test ideas. But it doesn’t belong in production pipelines.
A smart compromise is Vibe Prototyping:
- Use AI to build a fast, high-fidelity, interactive prototype (up to ~2,000 lines).
- Use that prototype to validate ideas, UI/UX, and requirements with stakeholders.
- Throw the prototype code away. Keep what you learned from it—the validated requirements, UX, and technical assumptions—and let engineers build the real feature properly within the production architecture.
Stop Using One Model for Everything
Routing tasks to the right model saves both money and time:
- Use heavy reasoning models for architecture design, complex refactoring, distributed systems logic, and deep debugging.
- Use faster, cheaper models for boilerplate code, simple unit test scaffolding, documentation, and routine code transformations.
The goal isn’t to find the “best AI model.” It’s to find the cheapest model that can reliably solve the problem you’re giving it.
AI Is Like Fast Food for the Brain
There is another problem that is easier to ignore because it doesn’t show up immediately in a PR.
Skill atrophy.
AI-generated code is incredibly convenient. But if the AI always does the difficult part, eventually you stop exercising the part of your brain that understands why the difficult part works. Software engineering intuition comes from getting stuck. From debugging something. From reading the implementation instead of the documentation. From making a terrible abstraction and realizing six months later why it was terrible. From understanding how the system behaves underneath the framework.
If an engineer—especially someone early in their career—always asks the AI for the solution before trying to understand the problem, they may produce working code while never developing the mental models needed to build reliable systems. That’s the part that should worry us more than AI generating bad code.
Bad code can be reviewed. A missing mental model is much harder to detect.
So there are a few habits we try to stick to in our daily work:
- Spend some time understanding a problem manually before asking AI to solve it.
- Use AI as a sounding board to explain concepts and challenge design decisions, not just as a syntax generator.
- During code review, make sure we can explain the generated code. If we can’t defend the logic, it shouldn’t be merged.
- Treat generated code as code written by someone else until we’ve actually understood it.
- Don’t only talk to AI about your work, talk to your fellow team members sometimes, too!
Bottom Line
AI doesn’t replace engineering discipline. It amplifies it.
With clear architecture, strong testing, good observability, defined ownership, and effective code review, AI can significantly accelerate software development. But it doesn’t compensate for gaps in those practices. If the architecture is unclear or changes aren’t properly tested and reviewed, generating code faster can simply increase the amount of work needed later.
The important distinction is that software delivery is much more than writing code. It includes designing, reviewing, testing, integrating, releasing, operating, debugging, and maintaining software—and those activities still require engineering judgment.
I’m not interested in going back to writing everything by hand. I want AI to reduce the mechanical work so I can spend more time on the engineering decisions that matter.






