Skip to content

Commit 6894146

Browse files
ptr727claude
andcommitted
Normalize a Bare CR Too, Through One Helper Both Digests Share
`payload_digest` reduced CRLF and left a bare CR alone, while the installer reads a snippet in text mode, where a bare CR arrives as a newline and installs as one. A snippet carrying CR-only endings therefore installed normalized content and hashed to something else, and the machine was reported STALE against its own content. Four sites were doing this replacement by hand and two of them disagreed, which is the arrangement that produced the defect. They all call one helper now, so the two digests cannot normalize differently. Two cases added. The first builds the CR-only variant from the normalized form rather than by replacing newlines in the file, because these snippets are CRLF in this repository and a blind replace produces a doubled CR rather than a bare one. My first version of that test made exactly that mistake and failed against the fixed code. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 23e80ac commit 6894146

2 files changed

Lines changed: 40 additions & 4 deletions

File tree

host-setup/agent-safety/install.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,19 @@ def git(*args):
136136
return ref
137137

138138

139+
def normalized(data):
140+
"""Line endings reduced to newlines, covering CRLF and a bare CR.
141+
142+
One helper rather than a replace at each site. Both digests have to normalize identically or a
143+
machine drifts on nothing, and a site that handled CRLF while missing CR did exactly that: the
144+
installer reads a snippet in text mode, so a bare CR arrives as a newline and installs as one,
145+
while a digest that left it alone reported the machine STALE against its own content.
146+
"""
147+
if isinstance(data, bytes):
148+
return data.replace(b"\r\n", b"\n").replace(b"\r", b"\n")
149+
return data.replace("\r\n", "\n").replace("\r", "\n")
150+
151+
139152
def payload_digest():
140153
"""One digest over the content this kit installs, normalized the way the installer writes it.
141154
@@ -150,7 +163,7 @@ def payload_digest():
150163
"""
151164
h = hashlib.sha256()
152165
for name in PAYLOAD_FILES:
153-
raw = (HERE / name).read_bytes().replace(b"\r\n", b"\n")
166+
raw = normalized((HERE / name).read_bytes())
154167
# A snippet is embedded stripped, so trailing whitespace is not installed content.
155168
# The hook is copied byte for byte, so nothing about it is stripped.
156169
if name.endswith(".md"):
@@ -213,8 +226,8 @@ def installed_digest(claude_home):
213226
if not hook.is_file() or not claude_md.is_file():
214227
return None
215228
h = hashlib.sha256()
216-
h.update(hook.read_bytes().replace(b"\r\n", b"\n"))
217-
text = claude_md.read_text(encoding="utf-8", errors="replace").replace("\r\n", "\n")
229+
h.update(normalized(hook.read_bytes()))
230+
text = normalized(claude_md.read_text(encoding="utf-8", errors="replace"))
218231
for marker in ("agent-safety", "fleet-bootstrap"):
219232
found = re.search(rf"<!-- {marker} v\d+ start -->.*?<!-- {marker} v\d+ end -->", text, re.DOTALL)
220233
if not found:
@@ -536,7 +549,7 @@ def reject(where, held, want):
536549
if claude_md.exists():
537550
raw = claude_md.read_bytes()
538551
newline = "\r\n" if b"\r\n" in raw else "\n"
539-
existing = raw.decode("utf-8").replace("\r\n", "\n").replace("\r", "\n")
552+
existing = normalized(raw.decode("utf-8"))
540553
else:
541554
newline, existing = "\n", ""
542555
for marker, filename in blocks:

host-setup/agent-safety/test_install.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,29 @@ def test_trailing_whitespace_on_a_snippet_is_not_reported_as_drift(self):
412412
finally:
413413
target.write_bytes(original)
414414

415+
def test_a_bare_cr_in_a_snippet_is_not_reported_as_drift(self):
416+
"""The installer reads snippets in text mode, so a bare CR arrives and installs as a newline.
417+
418+
A digest normalizing CRLF but not CR reported the machine STALE against its own content.
419+
"""
420+
baseline = install.payload_digest()
421+
target = HERE / "claude-md-safety.md"
422+
original = target.read_bytes()
423+
try:
424+
# Built from the normalized form, since the snippets are CRLF in this repo.
425+
# A blind newline replace would turn each CRLF into a doubled CR rather than a bare one.
426+
target.write_bytes(install.normalized(original).replace(b"\n", b"\r"))
427+
self.assertEqual(install.payload_digest(), baseline)
428+
finally:
429+
target.write_bytes(original)
430+
431+
def test_every_normalization_site_agrees(self):
432+
"""The two digests must normalize identically, or a machine drifts against nothing."""
433+
for raw, want in ((b"a\r\nb", b"a\nb"), (b"a\rb", b"a\nb"), (b"a\nb", b"a\nb")):
434+
self.assertEqual(install.normalized(raw), want)
435+
for raw, want in (("a\r\nb", "a\nb"), ("a\rb", "a\nb"), ("a\nb", "a\nb")):
436+
self.assertEqual(install.normalized(raw), want)
437+
415438
def test_a_real_edit_to_a_snippet_is_still_reported(self):
416439
"""The normalization must not swallow a change that does reach the installed block."""
417440
baseline = install.payload_digest()

0 commit comments

Comments
 (0)