Skip to content

Normalize line endings to LF so local and CI fingerprint the same bytes (#26 part 1) - #27

Merged
conorbronsdon merged 2 commits into
mainfrom
fix/gitattributes-lf
Aug 3, 2026
Merged

conorbronsdon merged 2 commits into
mainfrom
fix/gitattributes-lf

Conversation

@conorbronsdon

Copy link
Copy Markdown
Owner

Part 1 of #26. Stops the platform divergence. It does not clear the nine standing warnings — that is part 2, and it is a decision, left out of this PR deliberately so this can merge alone.

What was wrong

Nothing was wrong with the repo's content. Git stores LF: every committed .md blob has zero CR bytes, and git add --renormalize . against origin/main changes nothing in the index.

The divergence is entirely in the working tree. With no .gitattributes, a clone on a machine with core.autocrlf=true checks out CRLF. Measured on a fresh clone of b749e99:

HEAD=b749e99  autocrlf=true
tracked text files: 36 | files containing CR: 36 | total CR bytes: 3784

The fingerprint clawhub skill publish computes is over those bytes, so the nine skills — published from such a tree — are stored under their CRLF fingerprint, and the ubuntu runner's LF checkout fingerprints differently. All nine differ:

skill CRLF (published from) LF (runner sees)
angel-diligence 878408a2… 17a3d92d…
code-review 4d8c26d7… 3c849fbb…
eval-integrity 5c321c66… 9289f42d…
guest-circuit 1e367cff… de507672…
reconcile 84ea32d4… 42ccc2d8…
recover 018ec6ed… 951b0b26…
session-management 7b53cad6… 2c92bc24…
skill-creator 38758458… e164a31a…
ssot-check 54e08e30… a8942926…

Confirmed against the live registry with a --dry-run probe from each tree. CRLF content:

$ clawhub skill publish reconcile --slug reconcile --name Reconcile --dry-run --json
{ "status": "unchanged", "version": "1.0.0", "latestVersion": "1.0.0",
  "fingerprint": "84ea32d4e80b8441fce57a9daeca60a854c76f8b47589eb5ba8b724be3334fd4" }

LF content, same commit:

{ "status": "would-publish", "version": "1.0.1", "latestVersion": "1.0.0",
  "fingerprint": "42ccc2d85e6d3d2b48220c2f0175281ba18ceb5de74df924103fb1ef72e06d49" }

So the registry really does hold the CRLF bytes, and stale=9 is a true statement about the artifacts, not a script bug.

The fix, and evidence it takes effect

* text=auto eol=lf pins the checkout to LF on every platform, plus explicit binary for the GIFs and PNG and eol=lf for .sh/.py (a CR after a shebang is a runtime failure, not a diff).

git add --renormalize . — no index change, because git's copy was already right:

$ git add --renormalize .
$ git diff --cached --stat
(no output)

A fresh clone of this branch, on Windows, with core.autocrlf=true:

HEAD=3022ef4  autocrlf=true
tracked text files: 37 | files containing CR: 0 | total CR bytes: 0

And the stronger form of the same claim — the working tree is now byte-identical to what git stores, which is by definition what a Linux runner checks out:

# fresh clone of this branch
43/43 tracked files byte-identical to their stored blob

# fresh clone of b749e99, same machine
6/42 tracked files byte-identical to their stored blob (36 differ)

The 6 that already matched are the GIFs and the PNG; they are still byte-identical here, so binary is doing its job and nothing was corrupted on the way through.

python3 scripts/clawhub-sync.py --check passes on the normalized tree.

One operational note

An existing clone does not renormalize itself. .gitattributes governs checkout, and git skips rewriting files whose stat already matches the index, so git checkout -- . is a no-op. A working tree that predates this commit keeps its CRLF until it is forced:

git ls-files -z | xargs -0 rm -f && git checkout -- .

Worth running once in any existing clone, otherwise a manual publish from there re-introduces exactly what this PR removes.

Second commit

.gitattributes is added to the publish workflow's trigger paths. The fingerprint is over the bytes CI checks out and this file decides those bytes, so editing it can change what publishes — it belongs beside clawhub.yml and scripts/clawhub-sync.py.

What this does not do

CI will still report stale=9, and now it will do so from every machine rather than only from the runner — before this, a Windows --dry-run compared CRLF against CRLF and read stale=0, which is why three review passes missed it. The published artifacts still hold CRLF. Realigning them is #26 part 2; the registry behaviour that decides it is now settled and written up in a comment on the issue.

Refs #26

🤖 Generated with Claude Code

conorbronsdon and others added 2 commits August 3, 2026 04:25
The publish workflow's first real run reported published=0 skipped=9
stale=9 — every skill warned that its content differs from the 1.0.0
published under it. Nothing had changed. The fingerprint is computed over
file bytes, and the bytes differ by platform.

Git stores these files correctly: every committed .md blob has zero CR
bytes, and `git add --renormalize .` against this commit changes nothing
in the index. The divergence is entirely in the working tree. With no
.gitattributes, a clone on a machine with core.autocrlf=true checks out
CRLF — 9 of 9 skill folders, 27 of 27 .md files. The nine skills were
published from such a tree, so the registry holds CRLF content, while the
Linux runner checks out LF and fingerprints LF.

`* text=auto eol=lf` pins the checkout to LF everywhere, so the next
publish from any machine agrees with the next dry-run on the runner.

This does not clear the nine warnings on its own — the published
artifacts still hold CRLF, and realigning them means republishing, which
the script deliberately refuses to do without a version bump. That is
issue #26 part 2, decided separately.

Refs #26

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The fingerprint is over the bytes CI checks out, and .gitattributes is
what decides those bytes. A change to it can change what publishes, so it
belongs in the trigger paths beside clawhub.yml and clawhub-sync.py.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant