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:

  1. Search before invent — rg / IDE search for the intended symbol, path, or behavior
  2. Cite the hit — paste the file path + a one-line snippet that proves it exists
  3. 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 Y from 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: formatMoney already lives in src/lib/money.ts. Drop formatPriceLocale. 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 / hooks without 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.