AI Refactor Created Duplicate Files

Agent created `UserList2.tsx` next to the original or scattered duplicates. How to clean up safely.

You asked Claude Code or Cursor to refactor a component. Instead of editing the file, it created UserList2.tsx / UserListNew.tsx / user-list-v2.tsx and wrote the changes there. Worse, sometimes it imports the new file while leaving the old one in place — two copies live in parallel, with half a bug fixed in the new file and the other half still in the old one.

This guide gives one cleanup path: diff the two files, merge the good parts into the surviving file, delete the duplicate, and fix imports. Then add a CLAUDE.md rule so it doesn’t happen again.

Common causes

Ordered by hit rate.

1. Agent didn’t read the original and wrote from imagination

The most common cause. You said “refactor UserList”; the agent’s grep or file search missed the exact match (path alias, hyphenated filename, the component lives under features/ not components/), so it assumed the file didn’t exist and created a new one.

> Tool: write_file
  path: src/components/UserList2.tsx
  reason: "Creating a new improved version of the UserList component."

How to spot it: The agent’s log or chat transcript says “creating new file” when you intended an edit; or git status shows files like UserList2.tsx / *New.tsx / *v2.tsx.

2. Multi-step agent forgot it already created the file

Aider or Codex in a long session created lib/utils/format.ts early on. A few steps later you asked for “a formatting helper”; it created src/helpers/format.ts again. Same signature, slightly different implementation.

How to spot it: rg "export function formatDate" -t ts returns more than one hit across different paths.

3. Naming convention drifted mid-conversation

You called it userService in the first half; the agent later wrote UserService / user-service. When import resolution failed, it created a new file. On case-insensitive filesystems (macOS by default) this is invisible locally and explodes on Linux CI.

How to spot it: git ls-files | sort -f | uniq -di lists names that only differ by case.

4. Agent makes a “safe copy” to avoid breaking the original

Some agents (especially those repeatedly told “be careful with committed code”) default to copying before changing. To convert a class component to hooks it creates UserListHooks.tsx instead of replacing UserList.tsx.

How to spot it: The new file is a “style-upgraded” version of the old one with a near-identical public API.

5. Parallel agents or branches built the same thing

You had Cursor write analytics/track.ts in the morning; in the afternoon you had Claude Code write lib/analytics.ts on another branch. They both land in the merge.

How to spot it: git log --diff-filter=A --since="1 week" -- "*analytics*" shows multiple “new file” commits in the same week.

6. Agent doesn’t know your directory conventions

The prompt didn’t say “src/components/ is UI, src/features/<name>/ is business logic,” so the agent puts a UI component under features/ while the previous UI version still sits in components/.

How to spot it: Two functionally-equivalent files exist under different top-level directories.

Shortest path to fix

Step 1: List every suspect duplicate

# Files with number / New / v2 / copy suffixes
git status --short | grep -E '(2|3|New|new|v2|copy|Copy)\.(ts|tsx|js|jsx|py)$'

# New files in the last week whose basename matches an existing file
git log --diff-filter=A --since="1 week" --name-only --pretty=format: \
  | sort -u | awk -F/ '{print $NF}' | sort | uniq -d

Write the path pairs down before doing anything else.

Step 2: Diff each pair and pick the survivor

diff -u src/components/UserList.tsx src/components/UserList2.tsx
# or side by side
git diff --no-index src/components/UserList.tsx src/components/UserList2.tsx

Decide using this table:

SituationKeep
New file is a complete rewrite; nothing imports the old oneKeep new, delete old
Old file is still imported; new file has the real fixCherry-pick the fix into the old file, delete new
Each file fixed a different bugManually merge into the old path, delete new
Files are identicalDelete either

Always keep the path with more imports — fewer import edits means fewer ways to break things.

Step 3: Merge the good content into the surviving file

Let the agent cherry-pick rather than rewrite — much safer:

Prompt: Compare src/components/UserList.tsx and src/components/UserList2.tsx.
List only the logic changes present in UserList2 but missing from UserList.
Apply only those changes to UserList.tsx — no style, ordering, or rename edits.

Have it output the diff plan first; approve before it writes.

Step 4: Delete the duplicate and fix imports

git rm src/components/UserList2.tsx

# Find any lingering references
rg "UserList2|user-list-v2" --type ts --type tsx

Update each remaining import to the correct path. In a TypeScript project, tsc --noEmit flags every broken import in one shot:

npx tsc --noEmit

Step 5: Run tests and build to verify

npm test
npm run build

Pay special attention to case-insensitive filesystem traps (works on macOS, breaks on Linux CI) — run git ls-files | sort -f | uniq -di one more time.

Prevention

  • Say “edit src/components/UserList.tsx” in the prompt, not “create an improved version”; use @-mentions for explicit paths
  • Add to CLAUDE.md / .cursorrules / AGENTS.md: “never create files with number / New / v2 / copy suffixes; to replace a file, edit it in place”
  • Pre-commit hook to reject filenames like *2.tsx, *New.tsx, *Copy.tsx
  • git diff --stat review every agent round; any unfamiliar new-file path is a stop sign
  • /clear long sessions or start fresh to avoid the “forgot I already created it” failure mode
  • TypeScript: set forceConsistentCasingInFileNames: true and run tsc --noEmit in pre-commit to catch case duplicates before they merge

Tags: #AI coding #Debug #Troubleshooting