Skip to content

Hash the hook with normalized line endings: CRLF checkouts attested as edited (0.22.0) - #39

Merged
davidcrowe merged 1 commit into
mainfrom
fix/crlf-hook-hash
Sep 17, 2026
Merged

davidcrowe merged 1 commit into
mainfrom
fix/crlf-hook-hash

Conversation

@davidcrowe

Copy link
Copy Markdown
Collaborator

What happened

Two Windows installs of 0.21.0 on 2026-09-17 reported different hook hashes for the same unmodified bin/govern.mjs. The first (LF) seeded the registry's first-seen hash for 0.21.0. The second machine had Git for Windows at its default core.autocrlf=true, checked the file out as CRLF, and was flagged hook.attestation.hash_mismatch at its first session start, with a founder page for an edit that never happened.

Verified byte for byte against origin/main:

File sha256
bin/govern.mjs, LF cedbe165… = registry first-seen for 0.21.0
same file, CRLF d4c90fe1… = the flagged machine's reported hash

Every default-settings Windows install of every version attests as an edited hook unless it happens to be the first machine seen for that version. The trust-on-first-use baseline is set by whichever OS attests first.

Change

  • bin/govern.mjs: sha256FileHex strips CRLF to LF before hashing. Byte-transparent latin1 round trip, so an LF file hashes exactly as before and no existing first-seen hash changes. Lone CRs are kept.
  • lib/attestation.mjs: normalizeEol and sha256FileHex mirror the inline logic so the tests pin it.
  • .gitattributes: eol=lf for every text file, so future checkouts do not convert in the first place.
  • Version 0.22.0 in plugin.json, .claude-plugin/marketplace.json, PLUGIN_VERSION.

Tests

  • Unit: CRLF and LF copies hash identically; the committed hook is LF and unchanged by normalization; a lone CR is preserved.
  • End to end: a CRLF copy of the real hook, run at SessionStart against a stub gateway, reports the LF release hash, and the lib copy agrees with the inline copy.
  • Full suite: 243 pass, 0 fail. node --check and validate-manifest clean.

After merge

  • Set platformHookRegistry/claude-code-plugin.latest to 0.22.0 so 0.21.0 machines get the stale-hook notice.
  • Existing 0.21.0 CRLF checkouts keep flagging once a day until they update. The registry's 0.21.0 entry stays as is.

…s edited (0.22.0)

Two Windows installs of 0.21.0 on 2026-09-17 reported different hook
hashes for the same unmodified bin/govern.mjs. The first (LF) seeded the
registry's first-seen hash; the second, whose Git for Windows had the
default core.autocrlf=true and checked the file out as CRLF, was flagged
hook.attestation.hash_mismatch and paged the founder for an edit that
never happened. Verified byte for byte: sha256 of the LF file is the
registry value, sha256 of the same file with CRLF is the reported one.

The hook now strips CRLF to LF before hashing itself (byte-transparent
latin1 round trip, so an LF file hashes exactly as before and no existing
first-seen hash changes). lib/attestation.mjs carries the same
normalizeEol so the tests pin it. A .gitattributes pins eol=lf so future
checkouts do not convert in the first place.

Tests: unit (CRLF == LF, LF unchanged, lone CR kept) and end to end (a
CRLF copy of the real hook, run at SessionStart against a stub gateway,
reports the LF hash).
@davidcrowe
davidcrowe merged commit 10bf1df into main Sep 17, 2026
2 checks passed
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