Bug #23: CRLF/LF Mismatch in Edit Risk Assessment
Status: RESOLVED in v4.0.2 Category: Correctness / Line Endings Severity: High (edits incorrectly blocked on Windows files) Resolution Date: 2026-03-16
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)