Claude Code Edited the Wrong File

Claude Code changed a file you didn't intend (lookalike name or it followed an import chain). Revert with git, then lock the boundary with permissions.deny Edit rules in .claude/settings.json.

You said “edit src/auth/login.ts.” Claude Code edited src/auth/logIn.ts (different casing, a stale file you forgot existed). Or it edited src/auth/login.test.ts thinking that’s where the bug was. Or it edited src/legacy/auth/login.ts because that name matched and it found it first.

Fastest fix: git checkout HEAD -- <the-wrong-file> to revert the bad edit, then re-prompt with the full path in backticks plus a one-line deny list. Durable fix: put a permissions.deny rule like Edit(/legacy/**) in .claude/settings.json so Claude Code is blocked from those files, not merely asked nicely. Prompt and CLAUDE.md text only shape what Claude tries to do; only the permission rules actually stop it (Anthropic confirms this in the permissions docs).

Wrong-file edits come from two root causes: ambiguous naming (multiple candidates match your description) or scope drift (the agent followed imports/calls beyond what you asked). The recovery is the same either way; the prevention differs slightly, and the table below tells you which bucket you’re in.

Which bucket are you in?

Symptom in the diffLikely causeBest prevention
Same filename, different casing (Login.ts vs login.ts)Lookalike name, agent picked first matchFull path in prompt + delete/rename the stale twin
Edit landed in utils/, lib/, common/Followed an import chainEdit(...) deny rule on shared dirs
2-3 small unrelated tweaks near the target”Helpful” cleanup, scope creepTight allow list + Plan mode first
Edit in a different package than intendedMonorepo name collisionWorkspace prefix in path, or /add-dir scoping
Change is in .test.ts / .spec.tsEdited test instead of productionName the implementation file explicitly
Edit touched migrations/, *.snap, lockfile, dist/No standing guardrailpermissions.deny rule (enforced)

Common causes

Ordered by hit rate, highest first.

1. Two files with similar names — agent picked the wrong one

login.ts and Login.ts, auth.service.ts and auth.test.ts, user.ts and users.ts. Without explicit disambiguation, the agent edits whichever surfaced first in its file scan.

How to spot it: find . -iname "login*" returns multiple candidates. Without a path, “login” is ambiguous.

2. Agent followed an import chain into an adjacent file

You said “fix billing.ts.” It read billing.ts, saw import { format } from "../utils", decided the issue was in utils.ts, and edited there.

How to spot it: the diff includes a file in utils/, lib/, or common/ even though your task was specific to one feature.

3. Agent did a “small cleanup” of an adjacent file

While editing the target, it noticed the next-door file had a typo or inconsistent style and “fixed” it. Adjacent files end up in the diff.

How to spot it: the diff has 2-3 small unrelated changes in files near the target. Each looks innocuous; together they’re scope creep.

4. Naming ambiguity from a monorepo or duplicate folders

apps/web/src/auth/login.ts and packages/shared/auth/login.ts both exist. The agent picked whichever path appeared first in its tool calls.

How to spot it: the diff is in a different package than you intended. Same filename, different ancestor folder.

5. Agent edited the test file thinking it was production code

login.test.ts mocks call login(). The agent saw the mock setup, thought it was production, and edited the mock instead of the real implementation.

How to spot it: the diff is in a .test.ts / .spec.ts file even though you asked for a production fix.

6. Case-insensitive filesystem confusion

On macOS (case-insensitive by default), Login.ts and login.ts are the same file on disk. On Linux CI (case-sensitive), they’re two different files. An edit that looks fine locally can land on the wrong canonical form once it hits CI.

How to spot it: same content, different casing in the path. Check git log --follow for any renames.

Shortest path to fix

Steps 1-2 recover and stop the immediate bleeding. Steps 3-7 prevent recurrence, in rising order of durability.

Step 1: Inventory the damage with git status + git diff

# What changed?
git status

# Detailed diff
git diff --stat
git diff

For each unintended file, revert it:

# Single file
git checkout HEAD -- src/wrong/file.ts

# Multiple files / a whole directory
git checkout HEAD -- src/wrong/ src/another-wrong/

# If you already committed the bad edit
git revert <commit-hash>

If the edit isn’t committed and isn’t staged, git checkout HEAD -- <file> is enough. If you staged it, git restore --staged --worktree <file> clears both the index and the working copy in one go.

Step 2: Re-prompt with explicit allow + deny lists

EDIT exactly these files (and no others):
- src/auth/login.ts

DO NOT touch any of these (even if you think they need changes):
- src/auth/Login.ts (deprecated, do not revive)
- src/auth/login.test.ts (test file, separate concern)
- src/legacy/ (entire directory is off-limits)
- src/utils/ (shared, requires separate approval)

If the task requires editing anything else, STOP and ask first.

The deny list is what catches the “agent thought it was helping” cases that an allow list alone misses. This is prompt-level guidance, so it shapes intent but is not enforced. For files that must never be touched, jump to Step 5.

Step 3: Use full paths in prompts, not “the auth file”

Bad:  fix the auth file
Good: fix `apps/web/src/auth/login.ts`

A full path eliminates name-resolution ambiguity. In monorepos, always include the workspace prefix (apps/web/..., packages/shared/...) so two login.ts files can’t be confused.

Step 4: Diagnose in Plan mode before any edit

For risky or ambiguous tasks, enter Plan mode first: press Shift+Tab to cycle the mode (or start with claude --permission-mode plan). In Plan mode Claude reads files and runs read-only commands to explore, but cannot edit your source until you approve the plan. You see exactly which files it intends to change before a single byte is written. Approve, then switch back to default and tell it to implement.

Step 5: Lock the boundary with permissions.deny (enforced)

This is the durable fix. Permission rules in .claude/settings.json are enforced by Claude Code itself, not by the model, so a deny rule blocks the edit even if the prompt or CLAUDE.md would lead Claude to attempt it. Rules are evaluated deny first, then ask, then allow; a deny match at any settings level wins.

{
  "permissions": {
    "deny": [
      "Edit(/legacy/**)",
      "Edit(/migrations/**)",
      "Edit(**/*.snap)",
      "Edit(/dist/**)",
      "Edit(/build/**)",
      "Edit(**/.env)",
      "Edit(/pnpm-lock.yaml)"
    ]
  }
}

Pattern syntax (gitignore-style, from the official docs):

  • /path is relative to the project root, so Edit(/legacy/**) blocks <project>/legacy/.
  • //path is an absolute filesystem path; ~/path is from your home dir; a bare name or ./path is relative to the current directory.
  • * matches within one path segment, ** matches across directories. A bare filename matches at any depth, so Edit(**/.env) and Edit(.env) block every .env.

Two things worth knowing: an Edit rule covers all built-in editing tools, and the deny also blocks file-writing Bash commands Claude recognizes (sed, redirects into the file), so the agent can’t route around it through the shell. Use /permissions in-session to view and edit these rules; for a stricter scope you can flip to an allowlist with permissions.allow rules like Edit(/src/**) plus a default-deny posture.

Step 6: For ambiguous filenames, audit and consolidate

If your repo has multiple files with similar names, remove the ambiguity at the source:

# Find lookalikes (skip node_modules)
find . -iname "login*" -not -path "*/node_modules/*"

# Resolve: delete the dead one, normalize the test name
git rm src/auth/Login.ts                          # if truly dead
git mv src/auth/login.test.ts src/auth/login.spec.ts  # if disambiguation helps

Codebases with naming hygiene get far fewer wrong-file edits. Use git rm/git mv (not plain rm/mv) so the rename is tracked and git log --follow stays clean.

Step 7: For dangerous areas, use a branch + worktree

# New branch for the agent task
git checkout -b feature/auth-fix

# Or a separate worktree so the agent works in its own directory
git worktree add ../project-agent feature/auth-fix

If the agent edits the wrong file, you can throw away the whole branch or worktree without affecting main. A worktree also keeps the agent’s working directory physically separate from yours.

Step 8: Document standing rules in CLAUDE.md (intent layer)

CLAUDE.md documents the why for your team and nudges Claude’s behavior, but remember it is not an enforcement boundary; pair it with the Step 5 deny rules for anything critical.

## Files agents must not edit

- `migrations/*.sql` — historical record, do not modify
- `legacy/` — frozen, do not touch
- `*.snap` (snapshots) — only update via test re-run, never by hand
- `pnpm-lock.yaml` — only update via `pnpm install`, never by editing
- `dist/`, `build/` — generated, do not edit
- `.env*` — secrets, never edit

If a task seems to need editing any of these, STOP and ask.

For a hard stop that survives even bypassPermissions edge cases, register a PreToolUse hook that rejects edits to protected paths (see Anthropic’s Block edits to protected files example). A hook that exits with code 2 stops the tool call before permission rules are even evaluated.

How to confirm it’s fixed

  1. Re-run the same task. git diff --stat should list only the file(s) you intended.
  2. Try to trigger the old failure: ask Claude to “also tidy up legacy/” or “fix the shared util too.” With a deny rule in place, the edit is refused rather than applied.
  3. Run /permissions in-session and confirm your Edit(...) deny rules are listed and sourced from the expected settings.json.
  4. Commit the intended change and check git log -1 --stat shows the correct path, not a lookalike.

FAQ

Does CLAUDE.md stop Claude from editing a file? No. CLAUDE.md and prompt instructions shape what Claude tries to do, but they are not enforced. Only permissions.deny rules (or a PreToolUse hook) actually block an edit. Treat CLAUDE.md as documentation, and put the real guardrail in .claude/settings.json.

Why does Edit(/legacy/**) not block /legacy/? A leading single slash is the project root, not the filesystem root. Edit(/legacy/**) matches <project>/legacy/. To block the absolute path /legacy/ you’d write Edit(//legacy/**) with a double slash. This trips people up constantly.

Can the agent bypass a deny rule by running sed or cat > in Bash? No. Edit deny rules also apply to the file-writing Bash commands Claude Code recognizes, such as sed and redirects. They do not cover an arbitrary subprocess (a Python or Node script that opens the file itself); for OS-level enforcement that blocks every process, enable sandboxing.

How do I recover if the bad edit was already committed and pushed? git revert <commit-hash> creates a new commit that undoes it, which is safe on shared branches. Reserve git reset --hard for local-only history that hasn’t been pushed.

It edited the wrong file on macOS but the path looks right. macOS is case-insensitive, so Login.ts and login.ts resolve to the same file locally but diverge on Linux CI. Run git config core.ignorecase false to surface casing differences, then git mv to the canonical name so the lookalike disappears.

Plan mode vs a deny rule, which should I use? Plan mode is per-session and read-only, good for one-off risky tasks where you want to review the plan before any edit. A permissions.deny rule is persistent and enforced across every session, good for paths that should never be touched. Use both: Plan mode for the task, deny rules for the no-go zones.

Prevention

  • Use full paths in prompts, not descriptions: src/auth/login.ts, never “the auth file.”
  • Every non-trivial prompt includes an allow + deny list of files.
  • .claude/settings.json carries enforced Edit(...) deny rules for migrations, snapshots, lockfiles, and generated output; CLAUDE.md documents the intent behind them.
  • Use Plan mode (Shift+Tab) to review the file list before any edit on risky tasks.
  • Audit your repo for confusing duplicate-case or near-name files and consolidate with git rm/git mv.
  • Run risky agent work on a branch or worktree so revert is one command.
  • For monorepos, always include the workspace prefix to disambiguate cross-package collisions.

Tags: #Troubleshooting #Claude Code #Debug #Wrong edit