Codex Creates a Duplicate TypeScript Interface for One That Already Exists

Codex defines a fresh User or ApiResponse type when an identical one already lives in your repo. Make the agent search first using AGENTS.md, a types barrel, ripgrep, and a ts-morph CI check.

You review a Codex PR that adds a User interface inside src/auth/types.ts. Three problems at once: you already have User in src/types/user.ts, the new one has slightly different fields, and now half the codebase imports the old User and half the new one. TypeScript stays quiet because the two shapes are structurally compatible enough to assign across — so the bug surfaces at runtime, not at compile time.

Codex did not search before defining. It read the file it was editing, recognized the shape it needed, and inlined a fresh declaration. Without a forcing function — an explicit “search first” rule in AGENTS.md, a single import path the agent knows to check, and a CI check that fails on duplicate names — this repeats on every refactor task.

TL;DR — fastest fix

  1. Add a Type reuse rule to AGENTS.md that tells Codex to run a rg (ripgrep) search for the name before declaring it, and to import the existing one if found. Codex already uses rg as its search backbone, so this is a tool it runs instinctively (as of June 2026).
  2. Make src/types/index.ts a barrel and route all shared types through @/types, so “does this already exist?” is answerable in one search.
  3. Add a ts-morph CI check that fails the build on duplicate interface / type / enum / class names. Codex respects red CI and self-corrects.

Step 1 alone stops most cases. Steps 2 and 3 close the gap for generic names like User and Response that collide most often.

Common causes — which bucket are you in

Symptom in the PR / transcriptLikely causeFix
New interface X added; rg "interface X" src/ shows another oneAgent never searched before creatingAGENTS.md “search first” rule (Step 1)
find src -name 'types.ts' returns 10+ paths, no central indexTypes scattered, no obvious homeTypes barrel at @/types (Step 2)
rg -i 'type|interface|reuse' AGENTS.md returns nothingNo guidance to reuse typesAGENTS.md rule (Step 1)
Transcript only shows read_file on the edited file, no searchExisting type sits in an unloaded barrelBarrel + AGENTS.md pointer (Step 1 + 2)
Diff adds a generic-named type (User, Response, Config)Name collides with a DOM/library type, agent assumed it was thatBroader search + ts-morph CI (Step 1 + 3)

1. Agent did not search before creating

Codex’s default loop is “read the file I am editing, add what is missing.” If the missing piece is a type, it writes one inline. It does not always run a repo-wide search for an existing definition first.

How to spot it: the PR introduces a new interface X or type X = ..., and rg -n "interface X|type X " src/ shows another definition elsewhere.

2. Types are scattered with no central index

Half your types live in src/types/, the other half are colocated with their first user (src/auth/types.ts, src/api/types.ts). There is no obvious “where types live” answer, so Codex adds to the local file.

How to spot it: find src -name 'types.ts' -o -path '*/types/*.ts' returns ten or more paths, and there is no src/types/index.ts re-exporting them.

Codex loads AGENTS.md before it does any work (it walks from the Git root down to the working directory, closest file wins, with a combined cap of 32 KiB by default). If that file has no rule about reusing types, Codex takes the locally convenient path.

How to spot it: rg -i 'type|interface|reuse' AGENTS.md returns nothing.

4. Existing type lives in a barrel the agent did not load

You have src/types/index.ts re-exporting everything, but the agent only opened the file it was editing. It never saw the barrel and assumed nothing existed.

How to spot it: the transcript shows read_file calls only on the immediate file, with no rg / search across the repo for the type name.

5. Type name overlaps with a common library name

Codex saw Response and assumed it was the DOM Response, so it created a fresh ApiResponse instead of checking whether you already had one. Generic names (User, Item, Response, Error, Config) collide most often.

How to spot it: the diff adds a generic-named type, and an existing one has the same name.

Shortest path to fix

Step 1: Add a “search before creating” rule to AGENTS.md

Codex runs rg (ripgrep) constantly during its planning loop, so a rule phrased as an rg command is one it will actually execute:

## Type and interface reuse

Before defining a new `type`, `interface`, `class`, or `enum`:

1. Search for the name first:
   `rg -n "(interface|class|enum) <Name>\b|type <Name>\b" src/`
2. If a definition exists, import it. Do not duplicate.
3. If the existing one is missing fields you need, extend it
   (`interface X extends BaseX`) or update it in place — do not fork.
4. New shared types belong in `src/types/<domain>.ts`. Do not inline new
   types in feature files unless the type is private to one module.

When a name is generic (`User`, `Item`, `Response`), search broadly —
include `packages/`, `apps/`, and `shared/`.

This is a rule the agent can actually follow: one search command plus a decision tree. Keep AGENTS.md under the 32 KiB cap so none of it gets truncated.

Step 2: Centralize types behind a single import path

Make src/types/index.ts a barrel that re-exports every shared type:

// src/types/index.ts
export type { User } from './user'
export type { Session } from './session'
export type { ApiResponse, ApiError } from './api'
export type { Permission } from './permission'

Then point AGENTS.md at it:

All shared types are exported from `@/types`. Import from there:

    import type { User } from '@/types'

If a type is not in `@/types`, treat it as module-private. Search `@/types`
before defining anything that could plausibly be reused.

A single import path makes “did this already exist?” answerable in one search.

Step 3: Add a ts-morph CI check for duplicate type names

This catches what review and AGENTS.md miss. The script below uses ts-morph (28.x as of June 2026) and covers interfaces, type aliases, enums, and classes — the same four kinds the AGENTS.md rule names:

// scripts/check-duplicate-types.ts
import { Project } from 'ts-morph'

const project = new Project({ tsConfigFilePath: 'tsconfig.json' })
const decls = new Map<string, string[]>()

const record = (name: string, path: string) => {
  const list = decls.get(name) ?? []
  list.push(path)
  decls.set(name, list)
}

for (const file of project.getSourceFiles('src/**/*.ts')) {
  const path = file.getFilePath()
  for (const d of file.getInterfaces()) record(d.getName(), path)
  for (const d of file.getTypeAliases()) record(d.getName(), path)
  for (const d of file.getEnums()) record(d.getName(), path)
  for (const d of file.getClasses()) {
    const name = d.getName()
    if (name) record(name, path)
  }
}

const dupes = [...decls.entries()].filter(([, files]) => files.length > 1)
if (dupes.length > 0) {
  for (const [name, files] of dupes) {
    console.error(`Duplicate type ${name}:`)
    for (const f of files) console.error(`  ${f}`)
  }
  process.exit(1)
}

Wire it to a required CI check (npm run check:types). Codex reads failing CI output and self-corrects on the next turn.

Step 4: Make ESLint point at the canonical location

If your codebase has custom ESLint rules, add one that flags re-declaring a shared type and points at the canonical path. Even without custom rules, eslint-plugin-import with no-duplicates plus no-restricted-imports keeps imports flowing through @/types:

{
  "rules": {
    "no-restricted-imports": ["error", {
      "patterns": [{
        "group": ["**/auth/types", "**/api/types", "**/user/types"],
        "message": "Import shared types from @/types, not deep paths."
      }]
    }]
  }
}

Codex reads ESLint output and fixes the import path itself.

Step 5: Review imports in agent PRs first

This is the human safety net for cases ts-morph cannot catch — same name, slightly different shape. Add to your PR checklist:

- [ ] No new `interface` or `type` declarations duplicating something in `@/types`
- [ ] All new shared types go in `src/types/<domain>.ts`, not inline in feature files
- [ ] `npm run check:types` passes (runs the ts-morph duplicate check)

How to confirm it’s fixed

  1. Run the duplicate check locally: npm run check:types should exit 0 and print nothing.
  2. Confirm Codex loaded the new rule. From the repo root, run codex --ask-for-approval never "Summarize the type-reuse rules in AGENTS.md." and check that the rule appears in its answer. If it does not, your AGENTS.md is past the 32 KiB cap or in the wrong directory.
  3. Give Codex a task that touches a shared type and read the transcript: you should see an rg search for the type name before any new declaration.

Prevention

  • AGENTS.md mandates “search before define” with the exact rg command
  • All shared types live in src/types/ and re-export from src/types/index.ts
  • ts-morph CI check fails on duplicate interface / type / enum / class names
  • ESLint no-restricted-imports keeps imports flowing through @/types
  • Reviewers spot-check new interface and type declarations in agent PRs
  • For generic names (User, Item, Response) require a domain prefix to avoid future collisions

FAQ

Why doesn’t TypeScript just catch the duplicate? Because the two declarations live in different files with different module scopes, they are two distinct types that happen to share a name. As long as their fields are structurally compatible, TypeScript lets you assign one to the other. You only feel the bug when a field exists on one shape but not the other.

Should I use rg or grep in the AGENTS.md rule? Use rg (ripgrep). Codex, Cursor, and most coding agents use ripgrep as their search backbone as of June 2026 — it respects .gitignore, is far faster on large repos, and is the command Codex reaches for by default. Plain grep still works if rg is not installed, but the agent is less likely to run it unprompted.

Codex keeps ignoring the rule. What now? First confirm it loaded: run codex --ask-for-approval never "Summarize the type-reuse rules in AGENTS.md.". If the rule is missing from the answer, your AGENTS.md likely exceeds the default 32 KiB cap (project_doc_max_bytes) or sits outside the path Codex walks (Git root down to the working directory). Trim the file or move the rule into the closest AGENTS.md to the code being edited — the nearest file wins.

How do I clean up the duplicates already in the repo? Run the ts-morph script in Step 3 to list every duplicate and its file paths. Pick the canonical declaration, delete the others, then let the type-checker drive the fix: every broken import points at code that needs to switch to @/types. Update them and re-run npm run check:types until it is green.

Does this apply to Codex in the IDE and the CLI? Yes. Both read the same AGENTS.md chain (global ~/.codex/AGENTS.md, then project files from the Git root down). The ts-morph CI check and ESLint rules run in your pipeline regardless of which Codex surface produced the PR.

Tags: #Codex #agent #Troubleshooting #types