When agents invent APIs — make grep-first a merge gate
I keep catching agent PRs that call helpers that do not exist. Not typos — confident imports of fetchUserProfileV2, useCheckoutMachine, formatMoneyLocale. Green TypeScript locally (because the agent also invented a stub)
I keep catching agent PRs that call helpers that do not exist. Not typos — confident imports of fetchUserProfileV2, useCheckoutMachine, formatMoneyLocale. Green TypeScript locally (because the agent also invented a stub), red in CI once the stub is gone, or worse: green forever with a hand-rolled fake that nobody asked for.
The fix that stuck for me is boring: grep-first as a merge gate. Before the model writes a call site, it has to prove the symbol already lives in the repo — or stop and ask.
The failure mode
Agents optimize for “the feature works in this chat.” They do not feel the cost of a new private API. So they:
- Import a name that sounds like your codebase
- Add a 20-line util next to the feature instead of reusing the real one three folders away
- Duplicate a hook with a slightly different signature
- “Fix” missing exports by creating them, then leave the real module untouched
Reviewers then spend time deciding whether the new thing is intentional architecture or hallucinations with types.
Grep-first means what, exactly?
In the prompt (and later in CI), I require:
-
Search before invent —
rg/ IDE search for the intended symbol, path, or behavior - Cite the hit — paste the file path + a one-line snippet that proves it exists
- Reuse or stop — if nothing matches, do not invent a public helper; either use a nearby existing pattern or stop with a short note: “no existing X; options are A/B”
Inventing a local private function inside the file you’re already editing is fine. Inventing a shared API is not — that’s a separate decision.
Paste this into the agent prompt
## API invent ban (HARD)
Before you import or call any symbol outside the files you are allowed to edit:
1. Search the repo for an existing equivalent (`rg -n "functionName|HookName|ModuleName"`).
2. In your reply, cite at least one real path + line that you will reuse.
3. If search returns nothing: STOP. Do not create a new shared util/hook/service.
List 2–3 options (reuse nearby pattern / extend existing module / new module needs human OK).
4. Allowed without search: private helpers scoped to the current file only (not exported).
Do not invent `*V2`, `*New`, or parallel hooks “for clarity.”
Do not add barrel re-exports to paper over a missing symbol.
The STOP clause is the whole point. Without it, agents treat “prefer reuse” as vibes and keep typing.
What I grep for (frontend patterns)
I don’t only search the exact name the model wants. I search for behavior:
| Intent | Grep seeds I actually try |
|---|---|
| Money / currency format |
formatMoney, Intl.NumberFormat, currency
|
| Auth header / session |
getSession, authHeaders, useUser
|
| Feature flag |
useFlag, isEnabled, Statsig, LaunchDarkly
|
| Analytics event |
track(, analytics., capture(
|
| Date display |
formatDate, dayjs, date-fns, Temporal
|
If any of those hit a shared module, the agent must wire that — not ship utils/formatPrice.ts #4.
Merge gate, not just a prompt
Prompts alone drift. I also fail the PR when the diff introduces new exports under shared folders without a checklist comment.
Lightweight version that has worked:
# fail if agent added exports under shared API dirs without REVIEW_OK
git diff --name-only origin/dev...HEAD | rg '^src/(lib|shared|api|hooks)/' || true
Then in review (or a bot comment): any new file under those roots needs either:
- A link to the existing API it replaces, or
- A one-line
REVIEW_OK: new shared X because Yfrom a human
I’m not trying to ban new APIs. I’m trying to ban silent new APIs from a model that never looked.
How I push back in review
When I see a fresh helper that duplicates something:
Grep-first miss:
formatMoneyalready lives insrc/lib/money.ts. DropformatPriceLocale. Reuse the existing export. If the signature is wrong, extend that module in a follow-up — don’t fork it in the feature folder.
Concrete path beats “please reuse existing utilities.”
Exceptions I allow
- Generated clients (OpenAPI / GraphQL) — inventing wrappers is still gated; calling generated types is fine
- One-file prototypes behind a feature flag — still no new shared exports
-
True greenfield modules — human says so up front in the prompt (“create
src/lib/playback/”)
If the task is “add a shared date lib,” say that. Don’t let “fix checkout copy” smuggle one in.
Tiny checklist before I merge agent UI/API work
- [ ] Every new import outside the blast radius was grepped and cited
- [ ] No new files under
shared/lib/hookswithout human OK - [ ] No
*V2/ parallel hook next to an existing one - [ ] Private helpers stay unexported
- [ ] CI or review bot can spot new shared exports
What I stopped doing
- Trusting “I checked the codebase” without a path citation
- Merging “small util, we’ll consolidate later”
- Letting TypeScript green mean the API is real
- Arguing about taste when the issue is a missing
rg
Agents invent APIs because inventing is cheap and searching is optional. Make search mandatory, cite the hit, and treat new shared exports as a merge gate. The feature still ships — just on the APIs you already own.
Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.