Edit tool: multi-line old_string matching unreliable on CRLF files with tab indentation

Resolved 💬 3 comments Opened Feb 26, 2026 by jopejope Closed Feb 26, 2026

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):

  1. CRLF not normalized during matching: The tool constructs the old_string with \n separators 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.
  1. Tab count ambiguity in Read output: The Read tool 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 the old_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 file errors 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.

View original on GitHub ↗

This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗