Edit tool: multi-line old_string matching unreliable on CRLF files with tab indentation
Title
Edit tool: multi-line old_string matching unreliable on CRLF files with tab indentation
Summary
The Edit tool fails to match multi-line old_string patterns in some cases on Windows files that use CRLF line endings and tab-based indentation, even when the string appears to exactly match the file content. Single-line match strings work reliably as a workaround.
Environment
- Platform: win32 (Windows 10 Pro 10.0.19045)
- Shell: bash (Git Bash / MSYS2 layer inside VSCode)
- Model: claude-sonnet-4-6
- Interface: VSCode native extension (Claude Code for VS Code v2.1.59)
Observed Behavior
While editing two C++ source files (linux_draw.cpp and linux_drawutil.cpp), both confirmed CRLF by file(1) and cat -A:
x:\cs\dxvscriptserver\cardmaker\linux_draw.cpp: C++ source, UTF-8 text, with CRLF line terminators
x:\cs\dxvscriptserver\cardmaker\linux_drawutil.cpp: C++ source, UTF-8 text, with CRLF line terminators
Tab indentation confirmed by cat -A (e.g. ^I^I^Iif (!fp) {^M$).
Edits that failed (old_string spanning multiple lines with leading tabs):
FILE* fp = fopen(filename.c_str(), "rb");
if (!fp) {
throw std::runtime_error(
std::format("could not open PNG file: {}", filename));
}
→ String to replace not found in file
A shorter 3-line version also failed:
if (!fp) {
throw std::runtime_error(
std::format("could not open PNG file: {}", filename));
→ String to replace not found in file
Edits that succeeded: Single-line strings with no leading whitespace (if (!png) {, if (!info) {, if (!matched) {, etc.) all matched correctly. After switching to shorter strings, a 3-line version of the !fp match eventually succeeded with what appeared to be identical content.
Root Cause Hypothesis
Likely one of two issues (or both):
- CRLF not normalized during matching: The tool constructs the
old_stringwith\nseparators but the file stores\r\n, causing a byte-level mismatch on multi-line patterns. Single-line matches are unaffected because they contain no embedded newlines.
- Tab count ambiguity in
Readoutput: TheReadtool output format (spaces + line_number + TAB + content) makes it difficult for the model to reconstruct the exact leading-tab count when code is deeply nested. This produces subtle whitespace mismatches in theold_string. Single-line strings avoid this because leading whitespace is not needed to make the match unique.
Both files use CRLF and tabs, yet multi-line edits to linux_draw.cpp eventually succeeded while early multi-line edits to linux_drawutil.cpp failed — suggesting the failure may be tab-depth-dependent or sensitive to exact whitespace reconstruction.
Impact
- Unexpected
String to replace not found in fileerrors on Windows projects - Forces a less-safe workaround (shorter, less-specific match strings), increasing the risk of unintended replacements in files with repeated patterns
- Adds friction and retry loops to routine editing on Windows
Workaround
Use the shortest unique single-line substring as old_string, avoiding leading whitespace. When two occurrences must be distinguished, include just enough trailing context to make the match unique, but limit it to one logical line.
Suggested Fix
Normalize line endings (\r\n → \n) in both the file content and the old_string before performing the match, and document this normalization so model prompts can rely on it.
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗