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 is not the default behavior. Claude Code, Cursor, Codex, and Aider all apply edits in place to the existing file; a duplicate appears only when the agent couldn’t locate the original, lost track of it mid-session, or deliberately made a “safe copy.” The fix is the same in every case.
Fastest path: git status to spot the stray file, git diff --no-index old new to see what the duplicate actually changed, move only the real logic changes into the original file, then git rm the duplicate and run tsc --noEmit to catch every broken import in one pass. The rest of this guide covers the edge cases (case-collision traps, the rg --type tsx gotcha that hides remaining references) and a CLAUDE.md / AGENTS.md rule so it stops recurring.
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:
| Situation | Keep |
|---|---|
| New file is a complete rewrite; nothing imports the old one | Keep new, delete old |
| Old file is still imported; new file has the real fix | Cherry-pick the fix into the old file, delete new |
| Each file fixed a different bug | Manually merge into the old path, delete new |
| Files are identical | Delete 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
A trap worth calling out: do not write rg --type tsx. tsx is not a built-in ripgrep type, so rg --type tsx exits with rg: unrecognized file type: tsx — and if you let an agent run the search, its Grep wrapper silently turns that error into “0 matches” (Claude Code issue #55744). You then think no references remain when they do. The built-in ts type already covers both .ts and .tsx; for everything else use a glob:
rg "UserList2|user-list-v2" --glob '*.{ts,tsx,js,jsx}'
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.
How to confirm it’s fixed
Run all three before you commit. If any fails, you have not finished:
# 1. The duplicate is gone and not still tracked
git ls-files | grep -E 'UserList2|user-list-v2' # expect no output
# 2. No broken imports anywhere (TypeScript)
npx tsc --noEmit # expect exit code 0
# 3. No case-only duplicate slipped back in
git ls-files | sort -f | uniq -di # expect no output
Then npm test && npm run build. A clean build that still imports the wrong path is possible only if the survivor file is missing logic the duplicate had, so do a final git diff read-through of the survivor before pushing.
Prevention
- Say “edit
src/components/UserList.tsx” in the prompt, not “create an improved version”; use@-mentions for explicit paths so the agent never has to guess where the file is - Add a rule to your agent config. As of June 2026,
AGENTS.mdis the cross-tool standard read natively by Codex, Cursor, Copilot, Aider, Windsurf, Zed, and (alongside its richerCLAUDE.md) Claude Code; Cursor’s single-file.cursorrulesis deprecated in favor of.cursor/rules/*.mdc. Put the rule wherever your team’s tools read it: “Never create files with number / New / v2 / copy suffixes. To replace a file, edit it in place. If you cannot find a file, search before assuming it does not exist.” - Add a pre-commit hook that rejects new filenames matching
*[0-9].tsx,*New.tsx,*Copy.tsx,*-v[0-9].ts git diff --statreview every agent round; any unfamiliar new-file path is a stop sign/clearlong sessions or start fresh to avoid the “forgot I already created it” failure mode- TypeScript: set
forceConsistentCasingInFileNames: trueintsconfig.jsonand runtsc --noEmitin pre-commit to catch case duplicates before they reach Linux CI
FAQ
Why did the agent make a duplicate instead of editing the file?
Almost always because it could not locate the original. Path aliases (@/components/...), hyphenated filenames, or a component living under features/ instead of components/ cause the agent’s search to miss, so it assumes the file is new. The rg --type tsx bug above makes this worse: a search that should have found the file returns “0 matches” and the agent creates a fresh one.
Should I keep the new file or the old one?
Keep whichever path has more inbound imports — fewer import edits means fewer ways to break the build. Run rg "from .*UserList" --glob '*.{ts,tsx}' on each candidate and keep the higher count. The only exception is when the new file is a complete, correct rewrite and nothing imports the old one yet.
How do I find duplicates that don’t have an obvious suffix (case 2 and 6)?
Search for the duplicated symbol, not the filename: rg "export function formatDate" --type ts or rg "export (default )?(function|class|const) UserList\b" --type ts. Two hits in different paths is a duplicate even when the filenames look unrelated.
The duplicate only differs by case (userService.ts vs UserService.ts). Why doesn’t git see both?
On macOS and Windows the filesystem is case-insensitive, so git’s working tree shows one file while the index can hold two. Detect it with git ls-files | sort -f | uniq -di, then fix with a two-step rename through a temporary name: git mv UserService.ts tmp.ts && git mv tmp.ts userService.ts. Set forceConsistentCasingInFileNames: true so it cannot recur silently.
Can I just let the agent clean it up?
Yes for the diff and merge — ask it to “list only the logic changes in UserList2.tsx missing from UserList.tsx and apply them to UserList.tsx, no style or rename edits,” and approve its plan first. Do the git rm, the import search, and the final tsc --noEmit yourself so a swallowed search error (see the ripgrep trap) does not leave dead references behind.
Related
- Claude Code edited the wrong file
- AI removed working logic during refactor
- Rolling back AI code changes
- AI pre-commit review workflow
- AI dependency upgrade workflow
Tags: #AI coding #Debug #Troubleshooting