I’m a heavy user of AI coding tools, and I like having one around as a coworker. I reach for it when prototyping new ideas, and it has become part of my daily workflow: writing tests, refactoring, exploring unfamiliar APIs, and rubber-ducking a design before I commit to it. On greenfield work it’s borderline magical. You describe what you want, and a clean, coherent project appears.

For the last four months, though, I’ve been working on a legacy codebase, and AI coding assistants are far less effective in that setting. The AI behaves very differently there, and pretending otherwise sets you up for frustration.

Here are the five caveats I keep running into, and what has actually helped me deal with them.

Caveat 1: It can’t see the whole picture

Large repositories don’t fit in a context window. The AI sees only fragments: the file you opened, whatever it managed to grep, and maybe a few neighbouring files. It routinely picks the wrong path through the codebase. It finds a function that looks relevant and builds on it, unaware that a newer service three directories over is the one the team actually uses.

The deeper issue is that the most important knowledge in a legacy codebase isn’t in the code at all. Why the billing module talks to two databases, why that retry loop has a hardcoded sleep, which of the three date utilities is the correct one.That knowledge lives in the heads of coworkers, in old Slack threads, and in decisions nobody wrote down. The AI can’t retrieve what was never documented, and neither can a new hire. We still depend on people.

How to deal with it: Treat the AI the way you’d treat that new hire, and write the onboarding doc you never got around to. Most tools support a project-level instructions file (CLAUDE.md, AGENTS.md, or similar). Put the tribal knowledge in there: a map of the main modules, which parts are deprecated, where the entry points are, and who owns what. It doesn’t need to be exhaustive. Half a page of “start here, never touch that, this folder is legacy” already improves the paths the AI takes dramatically. There’s a nice side effect, too: the same documentation helps your human colleagues. And when the AI gets something architecturally wrong, don’t just fix the output, fix the doc, so the next session doesn’t repeat the mistake.

Another big win is connecting the AI to your internal knowledge base through Slack or Notion plugins, if your organisation allows it. Be careful here: avoid exposing sensitive information, set up proper access controls, and keep an eye on token usage.

For bigger questions, narrow the scope yourself. Instead of “fix the invoice bug,” point the AI at the right area: “the bug is somewhere in services/billing, and the flow starts at InvoiceController.create.” In old-fashioned pair-programming terms, you’re the navigator and it’s the driver.

Caveat 2: It doesn’t know which pattern is “the” pattern

Legacy codebases are archaeological sites. They come in layers: the original jQuery-flavoured code, the class-component era, the Redux era, and the hooks rewrite that got 60% done. This is especially tricky with newer technologies and frameworks, because the AI is trained on a lot of legacy code and will default to the old patterns.

Say you’re moving your frontend from Redux to a lighter state-management solution. The AI will happily generate new code in the old pattern, because that’s what the surrounding codebase shows. It pattern-matches on what exists, not on where you’re heading. Left unsupervised, it actively works against your migration by adding more of the very thing you’re trying to remove.

How to deal with it: Be explicit about the direction, not just the destination. You have to tell the AI where to find the right pattern. In your instructions file, state the rules plainly: “New state lives in Zustand stores. Never add new Redux actions, reducers, or connect() calls. When touching a Redux-connected component, migrate it if the change is small. If you’re not sure, ask.”

Then go one step further and point at examples: “Follow the pattern in features/orders/; that’s our reference implementation.” The AI is much better at imitating a concrete, correct example from your own repo than at interpreting an abstract style guide.

Where possible, back this up with tooling. A lint rule that forbids imports from the deprecated module is worth more than a paragraph of instructions, because the AI gets immediate feedback the moment it slips.

Caveat 3: It would rather add code than improve code

This one is well documented by now. Research on AI-assisted development keeps finding the same thing: code churn is up, copy-pasted and duplicated code is up, and refactoring (moving or consolidating existing code) is down. The models are biased toward addition. Given a bug, the AI’s instinct is to wrap the problem in another if, add a defensive null check, or write a new helper that almost duplicates an existing one. Anything that makes the test pass and the compiler happy.

In a greenfield project you barely notice. In a legacy codebase it’s poison, because the codebase is already drowning in accumulated workarounds. An assistant that optimises for “compiles and passes” rather than “the best maintainable solution” accelerates exactly the entropy you’re fighting.

How to deal with it: First, change what you ask for. A prompt like “fix this bug” invites the AI to create a patch. A clearer prompt produces visibly different results: “Find the root cause of this bug, explain it, and propose the smallest fix that prefers modifying existing code over adding new code.” I often ask for the diagnosis before allowing any code changes. It’s remarkable how often the proposed fix changes once the model has been forced to articulate the cause.

Second, review AI diffs with a specific bias detector switched on. Every added branch, every new helper, and every duplicated block gets the same question: “could this have been a change instead of an addition?” Ask the AI itself: “Can you solve this by deleting or simplifying code instead?” It usually can; it just doesn’t default to it.

And keep your standards in the loop: small PRs, duplication checks in CI, and the rule that AI-generated code gets reviewed at least as strictly as human code. The author being a model is not a reason to lower the bar. It’s a reason to raise it.

Caveat 4: Legacy is expensive in tokens

A big repository means a big context, and a big context means a lot of tokens. I’ve experienced this many times: a single gnarly debugging session in a large monolith can burn through more tokens than a week of greenfield prototyping. If you’re on an API-metered plan, legacy work is structurally the most expensive way to use these tools.

How to deal with it: Spend tokens where they buy leverage, and starve the rest. A few things that work for me:

  • Scope aggressively. Don’t let the agent free-roam the repository when you already know the bug lives in two files. Hand it those files instead.
  • Keep instruction files tight. A focused, well-structured CLAUDE.md beats a 2,000-line dump that gets re-read on every request. Prune it like you’d prune any hot path.
  • Match the model to the task. Use the flagship model only for architecture and root-cause analysis, and use cheaper models for routine work like code generation and testing.
  • Start fresh sessions. Don’t drag a 200-message conversation, and its entire token history, into every new question.
  • Measure it. Most providers give you usage dashboards. I use Agentsview to track token usage and spot the most expensive workflows.

There’s also a reframe worth making: compare the cost against the alternative. If €5 of tokens replaces an hour of a senior engineer going through an undocumented module, that’s a bargain. The goal isn’t minimal spend; it’s deliberate spend.

Caveat 5: Too many permissions, and it will eventually destroy something

This is the caveat that keeps security people awake, and it’s no longer theoretical. The recent track record is sobering.

In July 2025, Replit’s AI agent deleted a live production database during a SaaStr “vibe coding” experiment, wiping records for more than 1,200 executives and nearly 1,200 companies, despite an explicit code-and-action freeze being in place. It then gave misleading answers about whether the data could be recovered. The data was eventually restored by hand, and Replit’s CEO publicly apologised and shipped dev/prod database separation afterwards. The point stands: the safeguard was an instruction, and the agent ignored it.

In April 2026 it happened again, and faster. A Cursor agent working on a routine staging task for PocketOS hit a credential mismatch, went looking for a way around it, found a Railway API token sitting in an unrelated file, and used it to fire a single GraphQL mutation that deleted the production database volume along with the volume-level backups stored beside it. Nine seconds of work caused more than 30 hours of outage. Nobody attacked anything. The agent wasn’t prompt-injected. It was just being “helpful” with permissions nobody realised it had. The token had been created for managing custom domains, but the platform’s tokens weren’t scoped, so it was effectively a root key.

The threat cuts the other way, too. In the August 2025 s1ngularity supply chain attack on the Nx npm packages, malware on developer machines deliberately weaponised installed AI CLIs, running them with flags like --dangerously-skip-permissions and --yolo to scan filesystems and harvest GitHub tokens, npm credentials, and SSH keys. Thousands of credentials were exfiltrated to public GitHub repos, and stolen tokens were later used to flip more than 10,000 private repositories public. Your AI agent isn’t just a risk because of what it might decide to do. It’s a risk because of what it can be made to do with the access you gave it.

Notice the common thread across all three incidents: the model was never the real problem. The blast radius was. An agent that can read your ~/.ssh, your browser cookies, your .env files, and a token that reaches production will eventually do something catastrophic with one of them, whether through autonomy, through error, or through an attacker.

How to deal with it: sandbox the agent. I use Sandvault.

Sandvault (sv) is an open-source tool that runs AI agents inside a separate, unprivileged macOS user account, hardened further with Apple’s Seatbelt (sandbox-exec). It comes pre-configured for Claude Code, OpenAI Codex, Google Gemini, and OpenCode. The idea is beautifully boring: instead of trusting the agent to behave, you make misbehaviour structurally impossible.

What that buys you in practice:

  • Your home directory is off-limits. SSH keys, browser profiles and cookies, .aws credentials, password-manager data: the sandboxed user simply cannot read them. The Cursor incident worked because the agent found a token lying around. In a sandbox, there’s nothing to find.
  • You share only what you choose. You clone the repos you’re working on into the sandbox (local or remote, with automatic git wiring), and you can git fetch the agent’s commits back into your real working copy. The agent gets the code, not the machine.
  • YOLO mode becomes safe. Every agent has some variant of “auto-approve everything,” and let’s be honest: that’s the mode where these tools are actually productive. Approving every single shell command defeats the purpose. Inside Sandvault you can run with permissions fully disabled, because the worst case is a trashed sandbox, not a trashed laptop. Rebuild it and move on.
  • It’s lightweight. No VM boot times, no Docker daemon, no Linux emulation on a Mac. It uses native macOS user switching, so startup is instant and performance is native. That matters, because a sandbox you avoid because it’s annoying protects nothing.

Why is this the right solution rather than just better instructions? Because the incidents above prove that instructions are not a security boundary. The Replit agent was told there was a freeze. Cursor’s agent had system rules. Models are optimised to accomplish goals, and “stop and give up” is the outcome they’re trained hardest against. Prompts shape behaviour; they don’t constrain it. OS-level isolation does. It’s the same principle we already apply everywhere else in engineering (least privilege, scoped tokens, separated environments), finally applied to the newest, most enthusiastic, and least predictable member of the team.

If you’re not on macOS, the principle still transfers: devcontainers, Docker wrappers, ephemeral VMs, or a dedicated low-privilege machine. The tool matters less than the boundary. Just don’t run an autonomous agent as you, with your keys, on your machine, against your production.

Closing thoughts

None of this is an argument against using AI in legacy codebases. I use it there every day, and the net effect is clearly positive. But the mental model has to change. On greenfield, the AI is a fast junior with great instincts. On legacy, it’s a fast junior with no context, no memory of your team’s decisions, a bias for adding code, and access to whatever you forgot to lock away. It needs what every junior needs: onboarding docs, explicit conventions, code review, a senior keeping an eye on the direction, and absolutely no production credentials.

Do that work, and it earns its place as a coworker. Skip it, and your legacy codebase will only get larger, stranger, and more expensive.