Edit/Write tools fail with 'File has been unexpectedly modified' on Windows (MINGW)
Bug Description
The Edit and Write tools consistently fail on Windows (MINGW64) even when files have not been modified between Read and Edit operations.
Environment
- Claude Code Version: 2.0.55
- OS: Windows 11 (MINGW64_NT-10.0-26200)
- Platform: win32
- Installation method: PowerShell installer (irm https://claude.ai/install.ps1 | iex)
Steps to Reproduce
-
Create a simple test file via Bash:
echo "test123" > testfile.txt -
Read the file using the Read tool - succeeds
-
Immediately attempt to Edit the file:
Edit: old_string="test123" new_string="test456" -
Error: "File has been unexpectedly modified. Read it again before attempting to write it."
-
Read the file again, then try Write tool: Error: "File has not been read yet. Read it first before writing to it."
Diagnostic Evidence
File was verified NOT modified on disk using stat and md5sum:
File: testfile.txt
Modify: 2025-12-01 01:35:23.636902300 +0100 # Unchanged
md5sum: 7dcc443bcee01e0800f9244380fe33cd # Unchanged
The file modification timestamp remained constant between Read and Edit attempts, confirming no actual disk modification occurred.
Behavior
- Read tool: Works correctly
- Edit tool: Always fails with "unexpectedly modified" error
- Write tool: Fails with "not read yet" even after successful Read
- Bash tool: Works correctly for file operations
Workaround
Currently using Bash with heredocs or Python to write files, bypassing the native Edit/Write tools.
Possible Causes
- Windows/MINGW file system timestamp resolution differences
- Line ending conversion (CRLF vs LF) being detected as modification
- Internal file hash/tracking cache not persisting correctly between tool calls
- Race condition in file state tracking
Impact
This completely breaks the Edit and Write tools on Windows, forcing all file modifications through Bash workarounds.
Updated Patch for Claude Code (Windows "File has been unexpectedly modified" fix)
The obfuscated variable names change between versions. Here are the patterns:
| Version | validateInput | validateInput2 | Write/Edit tool throw |
|---|---|---|---|
| v2.0.75 | Ew(B)>J.timestamp |
Ew(Y)>W.timestamp |
\!z||E>z.timestamp |
| v2.0.72 | Vw(B)>J.timestamp |
Vw(Y)>W.timestamp |
\!R||M>R.timestamp |
| v2.0.64 | KE(B)>I.timestamp |
KE(Y)>W.timestamp |
\!C||F>C.timestamp |
| v2.0.62 | oD(B)>J.timestamp |
oD(Y)>W.timestamp |
\!C||E>C.timestamp |
Bash/Git Bash Patch Commands (v2.0.75)
CLAUDE_CLI="/c/Users/$USER/AppData/Roaming/npm/node_modules/@anthropic-ai/claude-code/cli.js"
# Backup first
cp "$CLAUDE_CLI" "${CLAUDE_CLI}.backup"
# Patch 1: validateInput timestamp check
sed -i 's/if(Ew(B)>J.timestamp)return{result:\!1,message:"File has been modified since read/if(false)return{result:\!1,message:"File has been modified since read/' "$CLAUDE_CLI"
# Patch 2: validateInput timestamp check (behavior:ask variant)
sed -i 's/if(Ew(Y)>W.timestamp)return{result:\!1,behavior:"ask",message:"File has been modified since read/if(false)return{result:\!1,behavior:"ask",message:"File has been modified since read/' "$CLAUDE_CLI"
# Patch 3: Write tool throw check
sed -i 's/if(\!z||E>z.timestamp)throw Error("File has been unexpectedly modified/if(false)throw Error("File has been unexpectedly modified/' "$CLAUDE_CLI"
echo "Patch applied\! Restart Claude Code."
PowerShell Patch Commands (v2.0.75)
$cliPath = "$env:APPDATA\npm\node_modules\@anthropic-ai\claude-code\cli.js"
Copy-Item $cliPath "$cliPath.backup"
$content = Get-Content $cliPath -Raw
# Patch all 3 patterns for v2.0.75
$content = $content -replace 'if\(Ew\(B\)>J\.timestamp\)return\{result:\!1,message:"File has been modified since read', 'if(false)return{result:\!1,message:"File has been modified since read'
$content = $content -replace 'if\(Ew\(Y\)>W\.timestamp\)return\{result:\!1,behavior:"ask",message:"File has been modified since read', 'if(false)return{result:\!1,behavior:"ask",message:"File has been modified since read'
$content = $content -replace 'if\(\!z\|\|E>z\.timestamp\)throw Error\("File has been unexpectedly modified', 'if(false)throw Error("File has been unexpectedly modified'
Set-Content $cliPath $content -NoNewline
Write-Host "Patched\! Restart Claude Code."
Auto-Patch Script (SessionStart Hook)
For automatic patching on startup, save as ~/.claude/hooks/auto_patch_windows_edit.py:
#\!/usr/bin/env python3
import re, shutil
from pathlib import Path
def patch():
cli = Path("C:/Users/YOUR_USER/AppData/Roaming/npm/node_modules/@anthropic-ai/claude-code/cli.js")
if not cli.exists(): return
content = cli.read_text(encoding="utf-8")
patterns = [
# v2.0.75
(r'if\(Ew\(B\)>J\.timestamp\)return\{result:\!1,message:"File has been modified since read', 'if(false)return{result:\!1,message:"File has been modified since read'),
(r'if\(Ew\(Y\)>W\.timestamp\)return\{result:\!1,behavior:"ask",message:"File has been modified since read', 'if(false)return{result:\!1,behavior:"ask",message:"File has been modified since read'),
(r'if\(\!z\|\|E>z\.timestamp\)throw Error\("File has been unexpectedly modified', 'if(false)throw Error("File has been unexpectedly modified'),
# v2.0.72
(r'if\(Vw\(B\)>J\.timestamp\)return\{result:\!1,message:"File has been modified since read', 'if(false)return{result:\!1,message:"File has been modified since read'),
(r'if\(Vw\(Y\)>W\.timestamp\)return\{result:\!1,behavior:"ask",message:"File has been modified since read', 'if(false)return{result:\!1,behavior:"ask",message:"File has been modified since read'),
(r'if\(\!R\|\|M>R\.timestamp\)throw Error\("File has been unexpectedly modified', 'if(false)throw Error("File has been unexpectedly modified'),
# v2.0.64
(r'if\(KE\(B\)>I\.timestamp\)return\{result:\!1,message:"File has been modified since read', 'if(false)return{result:\!1,message:"File has been modified since read'),
(r'if\(\!C\|\|F>C\.timestamp\)throw Error\("File has been unexpectedly modified', 'if(false)throw Error("File has been unexpectedly modified'),
]
patched = False
for p, r in patterns:
if re.search(p, content):
content = re.sub(p, r, content)
patched = True
if patched:
shutil.copy(cli, cli.with_suffix(".js.backup"))
cli.write_text(content, encoding="utf-8")
print("PATCHED: Restart Claude Code")
elif "if(false)return{result:\!1" in content:
print("Already patched")
else:
print("Patterns not found - new version?")
if __name__ == "__main__": patch()
Add to ~/.claude/settings.json:
{
"hooks": {
"SessionStart": [{
"hooks": [{"type": "command", "command": "python ~/.claude/hooks/auto_patch_windows_edit.py", "timeout": 10}]
}]
}
}
Verification
grep -c "if(false)return{result:\!1" "$CLAUDE_CLI" # Should be 2
grep -c "if(false)throw Error" "$CLAUDE_CLI" # Should be 1+
Important Notes
- Restart Claude Code after patching
- Re-apply after updates (auto-patch hook handles this)
- Disable auto-updates: add
"DISABLE_AUTOUPDATER": "1"to env in settings.json
Additional Evidence: Transcript showing Read state not persisting
I have transcript evidence of this bug occurring in a real session. The file is 3e20bca3-7675-4f20-874a-05ebef4883ed.jsonl, lines 6556-6604.
Exact sequence observed:
Line 6556: Write(.wslconfig) → "File has not been read yet. Read it first"
Line 6557: Read(.wslconfig) → SUCCESS (returned file contents)
Line 6566: Write(.wslconfig) → "File has not been read yet" ← Read state LOST
Line 6585: Write(.wslconfig) → "File has not been read yet" ← Still not registered
Line 6591: Write(.wslconfig) → "Cannot create new file - file already exists"
Line 6604: User: "but didnt the error tell you to read first why the pivot"
Root cause confirmed:
The timestamp comparison in cli.js uses fs.statSync(path).mtime vs the stored read timestamp. On Windows:
- NTFS timestamps have different precision than
Date.now() - Node.js
mtimecan return stale cached values - The comparison
mtime > readTimestampincorrectly triggers due to timing jitter
Behavior consequence:
After 3+ cycles of Read→Write→Error, the model concludes "Read doesn't work" and pivots to Bash workarounds like:
echo "content" > file- PowerShell file operations
This is a reasonable inference given the feedback loop, but counterproductive since the error message literally says what to do.
Suggested fix:
The if(false) patch mentioned in this issue works. Alternatively, add tolerance to the timestamp comparison:
// Instead of: if (mtime > readTimestamp)
// Use: if (mtime > readTimestamp + 1000) // 1 second tolerance
Environment:
- Windows 11
- Claude Code via npm
- Node.js running in Windows (not WSL)
Updated Patch for Claude Code v2.0.61+ (December 2025)
Key Changes in v2.0.61+
The timestamp validation patterns have changed:
- Variable names changed:
B,J->Y,Wandj,T->z,C - New behavior flag: Now uses
behavior:"ask"instead of justmessage
Updated Manual Patches
Bash/Git Bash:
CLAUDE_CLI="/c/Users/$USER/AppData/Roaming/npm/node_modules/@anthropic-ai/claude-code/cli.js"
cp "$CLAUDE_CLI" "${CLAUDE_CLI}.backup.$(date +%Y%m%d%H%M%S)"
# v2.0.61+ Pattern 1: validateInput with behavior:"ask"
sed -i 's/if(sD(Y)>W.timestamp)return{result:!1,behavior:"ask"/if(false)return{result:!1,behavior:"ask"/' "$CLAUDE_CLI"
# v2.0.61+ Pattern 2: Edit tool throw check
sed -i 's/if(!z||C>z.timestamp)throw Error("File has been unexpectedly modified/if(false)throw Error("File has been unexpectedly modified/' "$CLAUDE_CLI"
echo "Patched! Restart Claude Code."
PowerShell:
$cliPath = "$env:APPDATA\npm\node_modules\@anthropic-ai\claude-code\cli.js"
$timestamp = Get-Date -Format "yyyyMMddHHmmss"
Copy-Item $cliPath "$cliPath.backup.$timestamp"
$content = Get-Content $cliPath -Raw
# v2.0.61+ patterns
$content = $content -replace 'if\(sD\(Y\)>W\.timestamp\)return\{result:!1,behavior:"ask"', 'if(false)return{result:!1,behavior:"ask"'
$content = $content -replace 'if\(!z\|\|C>z\.timestamp\)throw Error\("File has been unexpectedly modified', 'if(false)throw Error("File has been unexpectedly modified'
Set-Content $cliPath $content -NoNewline
Write-Host "Patched! Restart Claude Code."
Better Solution: Automatic SessionStart Hook
Instead of manually patching after every Claude Code update, use a SessionStart hook that auto-applies the patch whenever needed.
Step 1: Create ~/.claude/hooks/auto_patch_windows_edit.py:
#!/usr/bin/env python3
"""
Auto-patch Windows file editing issue on Claude Code startup.
GitHub Issue: https://github.com/anthropics/claude-code/issues/12805
"""
import os
import sys
import shutil
from datetime import datetime
from pathlib import Path
def find_cli_js():
"""Find Claude Code cli.js file."""
possible_paths = [
Path(os.environ.get("APPDATA", "")) / "npm/node_modules/@anthropic-ai/claude-code/cli.js",
Path.home() / "AppData/Roaming/npm/node_modules/@anthropic-ai/claude-code/cli.js",
Path.home() / ".npm-global/lib/node_modules/@anthropic-ai/claude-code/cli.js",
]
for path in possible_paths:
if path.exists():
return path
return None
def is_patched(content):
"""Check if both patches are applied."""
patch1 = 'if(false)return{result:!1,behavior:"ask"' in content or \
'if(false)return{result:!1,message:"File has been modified since read' in content
patch2 = 'if(false)throw Error("File has been unexpectedly modified' in content
return patch1, patch2
def needs_patching(content):
"""Check if original patterns exist that need patching."""
# v2.0.61+ patterns
orig1_new = 'if(sD(Y)>W.timestamp)return{result:!1,behavior:"ask"' in content
orig1_old = 'if(sD(B)>J.timestamp)return{result:!1,message:"File has been modified since read' in content
# Edit tool patterns
orig2_new = 'if(!z||C>z.timestamp)throw Error("File has been unexpectedly modified' in content
orig2_old = 'if(!j||T>j.timestamp)throw Error("File has been unexpectedly modified' in content
return orig1_new or orig1_old, orig2_new or orig2_old
def apply_patches(cli_path):
"""Apply patches to cli.js and return result."""
result = {"patched": False, "already_patched": False, "message": "", "backup_path": None}
try:
content = cli_path.read_text(encoding="utf-8")
except Exception as e:
result["message"] = f"Failed to read cli.js: {e}"
return result
patch1_applied, patch2_applied = is_patched(content)
if patch1_applied and patch2_applied:
result["already_patched"] = True
result["message"] = "Windows file edit patches already applied"
return result
orig1_exists, orig2_exists = needs_patching(content)
if not orig1_exists and not orig2_exists:
result["message"] = "Warning: Claude Code internals may have changed"
return result
# Create backup
backup_path = cli_path.with_suffix(f".js.backup.{datetime.now().strftime('%Y%m%d%H%M%S')}")
shutil.copy2(cli_path, backup_path)
result["backup_path"] = str(backup_path)
# Apply patches
new_content = content
patches_applied = []
if orig1_exists:
if 'if(sD(Y)>W.timestamp)return{result:!1,behavior:"ask"' in new_content:
new_content = new_content.replace(
'if(sD(Y)>W.timestamp)return{result:!1,behavior:"ask"',
'if(false)return{result:!1,behavior:"ask"'
)
patches_applied.append("validateInput-v2")
elif 'if(sD(B)>J.timestamp)return{result:!1,message:"File has been modified since read' in new_content:
new_content = new_content.replace(
'if(sD(B)>J.timestamp)return{result:!1,message:"File has been modified since read',
'if(false)return{result:!1,message:"File has been modified since read'
)
patches_applied.append("validateInput-v1")
if orig2_exists:
if 'if(!z||C>z.timestamp)throw Error("File has been unexpectedly modified' in new_content:
new_content = new_content.replace(
'if(!z||C>z.timestamp)throw Error("File has been unexpectedly modified',
'if(false)throw Error("File has been unexpectedly modified'
)
patches_applied.append("EditTool-v2")
elif 'if(!j||T>j.timestamp)throw Error("File has been unexpectedly modified' in new_content:
new_content = new_content.replace(
'if(!j||T>j.timestamp)throw Error("File has been unexpectedly modified',
'if(false)throw Error("File has been unexpectedly modified'
)
patches_applied.append("EditTool-v1")
cli_path.write_text(new_content, encoding="utf-8")
# Verify
verify_content = cli_path.read_text(encoding="utf-8")
v_patch1, v_patch2 = is_patched(verify_content)
if v_patch1 or v_patch2:
result["patched"] = True
result["message"] = f"Applied patches: {', '.join(patches_applied)}"
else:
result["message"] = "Verification failed"
shutil.copy2(backup_path, cli_path)
return result
def main():
if sys.platform != "win32":
return
cli_path = find_cli_js()
if not cli_path:
return
result = apply_patches(cli_path)
if result["patched"]:
print(f"[PATCH] {result['message']}. RESTART CLAUDE CODE.")
print(f"[PATCH] Backup: {result['backup_path']}")
if __name__ == "__main__":
main()
Step 2: Add to ~/.claude/settings.json:
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "python \"C:\\Users\\YOUR_USERNAME\\.claude\\hooks\\auto_patch_windows_edit.py\"",
"timeout": 15
}
]
}
]
}
}
Benefits of Hook Approach
| Manual Patch | SessionStart Hook |
|---|---|
| Re-apply after every update | Auto-applies when needed |
| Easy to forget | Set and forget |
| No backup management | Auto-creates timestamped backups |
| Single version support | Handles multiple CLI versions |
Tested On
- Claude Code v2.0.61 on Windows 11 (MINGW64/Git Bash)
- Both Edit and Write tools working correctly after patch
Broke in 2.0.61. When does it get fixed. This is pure ass
Broke in 2.0.61. When does it get fixed. This is pure ass
See my original reply above for v2.0.62 fix. I'll keep posting the fixes here as needed whenever new CC versions are released. Hopefully Anthropic fixes it permanently soon.
@erwinh22 Unfortunately the patch is for the npm installation but I'm using the standalone .exe
https://github.com/anthropics/claude-code/issues/13824
The cli.js patch workaround doesn't work for users running the bundled executable version (claude.exe in ~/.local/bin/), which is the default installation method on Windows.
Environment:
- Claude Code v2.0.69 (bundled exe, auto-updating)
- Windows 10/11
- Installation path:
C:\Users\<user>\.local\bin\claude.exe
The bundled version compiles all JavaScript into a single Bun binary, so there's no cli.js file to patch. This means Windows users on the default installation have no workaround available.
Request: Could the timestamp validation fix be included in the next bundled release? This is a frequent issue on Windows - almost every session hits this error multiple times when editing files.
Thanks for the workaround script for npm users, but it would be great to have an official fix that covers all installation types.
Isn't it fixed in 2.0.69? It seems to work for me.
ok wasn't sure if the patch was still applied or not...it was which is the reason why it worked for me. Meanwhile auto update kicked in to version 2.0.72 and it stopped working.
Anyway, patch can be applied to bundled executable version. Just instruct claude to apply the patch. It will extract the exe, apply the patch and put it together.
Crazy that this still isn't fixed. It's been going on for months. Still broken in 2.0.75. Even the workaround of using relative paths is being sabotaged by cc official tool descriptions, making it hard to workaround it the way that was documented in other related github issues (see below). Here is some info about why that workaround often fails, and how to make it more likely to work while we wait for Anthropic to fix the real issue.
Tool Schema Conflict Causes Claude to Ignore the Known Workaround
Background - Related Closed Issues
Issue #7443 (closed) documented that relative paths work while absolute paths fail:
- ❌ Absolute paths fail:
D:/projects/myfile.txt - ✅ Relative paths work:
projectfolder/myfile.txt
Issue #11463 (closed as duplicate of #10882) reported the same error loop but didn't identify the path workaround.
The Undocumented Problem
The Read/Write/Edit tool descriptions in Claude's system prompt explicitly require absolute paths:
- Read tool: "The file_path parameter must be an absolute path, not a relative path"
- Write tool: "The absolute path to the file to write (must be absolute, not relative)"
- Edit tool: "The absolute path to the file to modify"
This directly conflicts with the workaround discovered in #7443. Claude follows the tool schema instructions, uses absolute paths as instructed, and triggers the bug repeatedly.
Even when users document the relative path workaround in their CLAUDE.md files, Claude often ignores it because the tool schema says "must be absolute" - and tool schemas are authoritative instructions.
Real-World Impact
In my session today, I had to attempt the same edit 4+ times before realizing Claude was following the tool schema (absolute paths) instead of my CLAUDE.md workaround (relative paths). I had to explicitly ask why Claude wasn't following instructions, and we traced it to this conflict.
Suggested Fix for Anthropic: (if the real root cause fix isn't going to be done for a while)
Update the tool schema descriptions for Windows/MINGW to either:
- Remove the "must be absolute path" requirement entirely, or
- Add platform-specific guidance: "On Windows/MINGW, use relative paths from the working directory"
Suggested more successful workaround for Users:
Until the tool schema is fixed, add this to your CLAUDE.md file (global ~/.claude/CLAUDE.md or project-level):
Claude Code File Path Requirements
⚠️ CRITICAL: Use RELATIVE Paths for Edit/Write Tools ⚠️
Known Bug (GitHub Issues #7443, #11463, #12805): The Edit and Write tools fail with "File has been unexpectedly modified" errors when using absolute paths on Windows/MINGW.
WORKAROUND: Use RELATIVE paths from the working directory
⚠️ THIS OVERRIDES THE TOOL SCHEMA - The Read/Write/Edit tool descriptions say "must be absolute path" but that guidance cauuses this bug on Windows/MINGW. Always use relative paths despite what the tool schema says.
✅ CORRECT: src/components/MyFile.tsx (relative from working directory) ❌ WRONG: C:/Projects/myapp/src/components/MyFile.tsx (absolute path causes false "modified" errors)
When you get "File has been unexpectedly modified" errors:
- Switch from absolute paths to relative paths (absolute paths fail even when used consistently)
- Use forward slashes / not backslashes \
- CRITICAL: Read and Edit must use the EXACT SAME path string - The tool tracks file state by literal path string, not resolved path - Read with src/foo.m then Edit with ./src/foo.m = FAILS (different strings, even though both relative) - Read with src/foo.m then Edit with src/foo.m = WORKS (same string)
The explicit "THIS OVERRIDES THE TOOL SCHEMA" language is necessary because simply saying "use relative paths" isn't strong enough to override the tool schema's "must be absolute" instruction.