Quick answer: When an agent adds a "small helper" that already exists elsewhere in the repo, you don't just have a duplicate file. You now have two functions that look identical today but will drift apart the first time someone fixes a bug in one of them. Treat the discovery of an existing utility as a hard stop: reuse it, or deliberately consolidate, never silently fork it.
I found the pattern on a routine review of a checkout change. The agent had introduced formatMoneyCents() in a new pricing.ts. It was well written, it had a unit test, and the feature worked. The problem was that packages/currency/src/format.ts already exported formatCurrency(), which did the same job with one extra responsibility: it applied the rounding rules that finance had signed off on after a real incident with fractional cents. The new helper rounded half-up instead of half-even. Nothing failed in the pull request. It would have failed quietly, months later, on a specific refund total.
Why agents do this so often
An agent does not have a stable, searchable map of your entire codebase in its head the way a tenured engineer does. It works from the context it was given, and that context is usually the files in the current task plus whatever retrieval surfaced. A few forces make duplication almost inevitable:
- Local context bias. If the relevant utility is three packages away and nothing in the prompt points to it, the agent solves the problem inside the files it can see.
- Name mismatch. The existing helper may be called
formatCurrencywhile the agent thinks in terms of "money cents," so even a fuzzy search misses it. - Reward for completion. A self-contained helper makes the current change pass without touching shared code. That is the lowest-risk path to a green build, so the agent takes it.
- Barrel exports hide the source. When everything is re-exported through an index, the agent may not realize the function it needs is already on the public surface.
None of these is a model failure you can prompt away with a single line. It is a retrieval and codebase-ownership problem, and the fix is a combination of conventions and a review gate.
What the duplication actually costs
The cost is not disk space or a few extra lines. It is the loss of a single place to change behavior. The table below is how I think about the trade-off when I find a duplicate.
| Factor | Reusing the existing utility | Adding a new helper |
|---|---|---|
| Bug fixes | Every caller gets the fix at once | Must be applied in two places; one will be missed |
| Rounding/domain rules | Stays consistent with the approved behavior | Can silently diverge under a different default |
| Bundle size | One implementation | Dead or duplicated code ships until tree-shaking proves otherwise |
| Review surface | Change is localized to call sites | A new public-looking function needs its own tests and docs |
| Short-term risk | Slightly higher, since shared code is touched | Near zero, which is why agents prefer it |
How to detect it before merge
I rely on a cheap, repeatable check rather than willpower. Before accepting any new helper, I ask the agent to justify why an existing one cannot be used, and I make the search concrete.
If your editor is agent-driven, fold this into the prompt as an explicit instruction before code is written, not after:
For shared domains such as money, dates, and identifiers, I go further and add a lint or architecture rule that marks those packages as the only allowed home for that logic. A dependency rule or an import boundary turns "please remember" into something the CI enforces.
When a second copy is legitimate
It is rare, but duplication is defensible when the two functions encode genuinely different policies. A reporting module that deliberately rounds for display while the ledger keeps exact precision may want separate, explicitly named functions. The key word is deliberate. The difference should be visible in the names, documented at both sites, and covered by a test that pins the divergent behavior. If you cannot articulate the policy difference in one sentence, you do not have two functions; you have one function and a future bug.
When you find one, consolidate forward
Do not just leave a comment. Replace the new call sites with the shared utility, delete the duplicate, and run the full suite. If the shared helper is missing a feature the new one had, extend the shared one behind its existing tests rather than keeping both. That keeps the domain rule in one place and makes the next agent more likely to find the right function, because there is now only one name to find.
FAQ
Isn't a small local helper safer than touching shared code?
It feels safer in the moment because it avoids a broad change, but it externalizes the risk. Shared code carries the approved domain behavior; a fresh local copy can diverge without any test noticing. Safety here means reusing the reviewed utility, even though that touches a shared module.
How do I make the agent search before it writes?
Make retrieval an explicit first step in the prompt and require the agent to report the path of any existing function it considered. Pair that with barrel exports that put shared utilities on a predictable public surface so the search has somewhere obvious to land.
What if the existing utility is badly designed?
Improve that one utility rather than forking it. Add the missing capability behind its existing tests, update call sites, and remove the would-be duplicate. Refactoring one shared function is still cheaper than maintaining two that drift.
Can linting really prevent this?
It cannot recognize every semantic duplicate, but it can protect high-value domains. Import boundaries and module rules can force money, time, and identifier logic to live in a single package, which eliminates the most damaging class of duplicated helpers.
This is part of the AI Coding Agent Guide — start there for the full picture of how agent-generated code drifts and how to keep it reviewable.