Bug #23: CRLF/LF Mismatch in Edit Risk Assessment
Status: RESOLVED in v4.0.2 — still valid as of v4.5.25 (no regression; entry-point normalization remains in core/edit_operations.go and core/streaming_operations.go. If a CRLF/LF mismatch still surfaces, prefer tolerant_whitespace: true on edit_file / multi_edit — added v4.5.7.)
Category: Correctness / Line Endings
Severity: High (edits incorrectly blocked on Windows files)
Resolution Date: 2026-03-16
Related bugs (cross-references)
Section titled “Related bugs (cross-references)”The Memory log captures a number of related issues from 2026-06 incidents. If their dedicated pages don’t yet exist in this directory, the authoritative write-ups live at ~/.claude/projects/c--MCPs-clone-mcp-filesystem-go-ultra/memory/:
- Bug #24 — TOCTOU + cross-volume hardening (memory)
- Bug #25 —
read_filestart_lineedge cases - Bug #26 —
multi_editJSON newlines - Bug #27 —
multi_editatomic rollback whenold_textno longer matches after a prior edit (silent file truncation) — seeproject_bug27_multi_edit_silent_failure.md - Bug #28 —
edit_fileHTML/Aspx editing edge cases - Bug B1 —
read_file# content_hash:trailer indistinguishable from Markdown content (fixed in v4.5.13 by moving the hash intoStructuredContent["content_hash"]) — seeproject_bug_b1_content_hash_presentation.md - Bug B3 —
multi_editnot acceptingexpected_hash(OCC parity withedit_file); fixed v4.5.13 — seeproject_bug_b3_multi_edit_occ_expected_hash.md - Path case (2026-06-11) — see
project_bug_path_case_mismatch.md: a path written from memory with the wrong case (estats.razorvsEstats.razor) silently resolved but failed 3 layers down in the Razor compiler (RZ10011 class estats). Rule: ALWAYS copy paths fromlist_directory/read_file, never retype from memory.
The listed bug-page stubs (bug-24-...md through bug-28-...md, bug-b1-...md, bug-b3-...md) are not yet in this directory at v4.5.25; the memory files above are the canonical references for now.
Problem
Section titled “Problem”When a file on disk uses CRLF (\r\n) line endings and Claude Desktop sends old_text with LF (\n), the risk assessment reports 0 matches and flags the edit as a full rewrite (CRITICAL risk), even though the actual edit engine would find and apply the match correctly.
Symptoms
Section titled “Symptoms”edit_fileon a CRLF file triggers false CRITICAL risk warningmulti_editreports 0 matches for valid old_text- User must add
force: trueto bypass the false warning - Risk assessment and actual edit behavior disagree
Example
Section titled “Example”File content (hex): 6C696E6531 0D0A 6C696E6532 0D0A l i n e 1 \r\n l i n e 2 \r\n
old_text from Claude: "line2\n" (LF only)
Risk assessment: strings.Count("line1\r\nline2\r\n", "line2\n") = 0 matches → CRITICAL: full rewrite detected
Actual edit engine: normalizeLineEndings() converts both to LF → strings.Count("line1\nline2\n", "line2\n") = 1 match → Edit applies correctlyRoot Cause
Section titled “Root Cause”normalizeLineEndings() (which converts \r\n → \n and \r → \n) already existed and was called inside performIntelligentEdit(), but was NOT called before:
CalculateChangeImpact()inEditFile()(impact_analyzer.go)strings.Count()instreamingEditLargeFile()(streaming_operations.go)
The risk assessment ran on raw CRLF content with LF search text, always getting 0 matches.
Solution
Section titled “Solution”Added normalizeLineEndings() at the entry point of CalculateChangeImpact():
func CalculateChangeImpact(content, oldText, newText string, thresholds RiskThresholds) *ChangeImpact { // Normalize line endings so CRLF files match LF search text (Bug #23) content = normalizeLineEndings(content) oldText = normalizeLineEndings(oldText) newText = normalizeLineEndings(newText)
impact := &ChangeImpact{...}Also added normalization in streamingEditLargeFile() before its own strings.Count() call.
Why at CalculateChangeImpact()? All callers (EditFile, MultiEdit simulation, streamingEditLargeFile) get the fix automatically without modifying each call site.
Files Changed
Section titled “Files Changed”| File | Change |
|---|---|
core/impact_analyzer.go | 3 normalization lines at top of CalculateChangeImpact() |
core/streaming_operations.go | Normalization before strings.Count() in streamingEditLargeFile() |
tests/bug23_test.go | 4 regression tests |
Testing
Section titled “Testing”go test ./tests/ -run TestBug23 -vTest Cases
Section titled “Test Cases”- TestBug23_EditFile_CRLFMatchesLF — CRLF file edited with LF old_text succeeds
- TestBug23_EditFile_CRLFRiskNotCritical — Small edit in CRLF file is not flagged CRITICAL
- TestBug23_MultiEdit_CRLFMatchesLF — multi_edit on CRLF files works
- TestBug23_PureLF_StillWorks — LF files continue working (no regression)