Next action: none — fixed in #810, awaiting CI. Closes automatically on merge. schema.ts:165 held a case-sensitive Set of one spelling while types.ts documented three and matched them case-insensitively; the copy that decided was the strict one.
Summary
Co-Authored-By: in a record's trailer block is refused as unknown-key, so the commit is blocked. Co-authored-by: passes. The two spellings are the same trailer, and this repository already says so in writing — src/core/types.ts documents that all three casings reach a commit message and matches them case-insensitively. The list that validateRecord actually consults is a case-sensitive Set holding one of them.
The casing that fails is the one observed from Claude Code, which is the host with the most complete integration here.
Reproduction
Verified at 42cb0328 and at v1.2.0, macOS, git 2.50.1.
printf 'fix: t\n\nbody.\n\nLimit: a limit\nCo-Authored-By: Someone <a@b.c>\n' > cap.txt
printf 'fix: t\n\nbody.\n\nLimit: a limit\nCo-authored-by: Someone <a@b.c>\n' > low.txt
$ commitlore validate --message-file cap.txt
shape failed · references ok
6: unknown-key Co-Authored-By — got "Co-Authored-By", want "a key from SPEC §3 or X-<Name>"
commitlore: 1 violation (SPEC §6) — the message was not modified
$ commitlore validate --message-file low.txt
shape ok · references ok
And through the installed hook, in a repository with commitlore init run:
$ git commit -F cap.txt
shape failed · references ok
6: unknown-key Co-Authored-By — ...
commitlore: 1 violation (SPEC §6) — the message was not modified
# commit count unchanged — the commit did not happen
Position matters, which is why this is not hit constantly: a Co-Authored-By: line separated from the record by a blank line sits in the body, is not parsed as a trailer, and is never judged. It fails when it sits in the final trailer block — where a co-author trailer belongs.
Source boundary
Two lists of foreign trailer keys, with different matching semantics, and validation reads the stricter one.
src/core/schema.ts:165
const WELL_KNOWN_FOREIGN_KEYS: ReadonlySet<string> =
new Set(['Signed-off-by', 'Co-authored-by']);
src/core/schema.ts:181-184
const isDefinedKey = (key: string): boolean =>
(KNOWN_KEYS as readonly string[]).includes(key) ||
EXTENSION_KEY_RE.test(key) ||
WELL_KNOWN_FOREIGN_KEYS.has(key); // ← Set.has is case-sensitive
against this, in the same repository:
src/core/types.ts:95-104
Matched case-insensitively via isConventionalTrailerKey ...
`Co-authored-by`, `Co-Authored-By` and `Co-authored-By` all reach a commit
message from GitHub, git and various editors for the identical trailer
(bug-issue-150's own report shows all three in one repository).
So the fact is already established here, in a comment, citing a report that observed all three casings in one repository. Ingestion acts on it; validation does not.
Present in the shipped release:
$ git show v1.2.0:src/core/schema.ts | sed -n '165p'
const WELL_KNOWN_FOREIGN_KEYS: ReadonlySet<string> = new Set(['Signed-off-by', 'Co-authored-by']);
How it was found
Two headless Claude Code runs (claude -p, plugin loaded, init --unattended) on unrelated tasks, neither told anything about CommitLore. Both emitted Co-Authored-By: unprompted. The first placed it in the body and committed. The second placed it in the trailer block alongside the record it had just captured, was refused, and worked around it by rewriting the trailer to git's casing.
That second run is the shape a user meets: capture succeeds, the record is well-formed, and the commit is then refused for the attribution line the host wrote itself.
Proposed resolution
The narrow fix is case-insensitive membership in WELL_KNOWN_FOREIGN_KEYS. The better one is that this list stops existing twice: isDefinedKey delegates to isConventionalTrailerKey, so there is one set of foreign keys with one matching rule, and a casing added for ingestion cannot fail to reach validation.
The comment above the constant pins the constraint the fix must not weaken — Constraint: must keep failing as unknown-key, which spec/fixtures/invalid/03-unknown-key.txt exists to hold. Case-insensitive matching over a two-element foreign list does not reach that fixture; delegation has to be checked against it, and against EXTENSION_KEY_RE, before it is a safe swap.
Whichever it is, the regression test is a validate over each casing this repository already lists in types.ts, since the defect is precisely that one of them was never asked.
I can send the PR if this reads correctly.
Scope note: no dist/ rebuild — aware of docs/CANONICAL-BUILD.md.
Next action: none — fixed in #810, awaiting CI. Closes automatically on merge.
schema.ts:165held a case-sensitive Set of one spelling whiletypes.tsdocumented three and matched them case-insensitively; the copy that decided was the strict one.Summary
Co-Authored-By:in a record's trailer block is refused asunknown-key, so the commit is blocked.Co-authored-by:passes. The two spellings are the same trailer, and this repository already says so in writing —src/core/types.tsdocuments that all three casings reach a commit message and matches them case-insensitively. The list thatvalidateRecordactually consults is a case-sensitiveSetholding one of them.The casing that fails is the one observed from Claude Code, which is the host with the most complete integration here.
Reproduction
Verified at
42cb0328and atv1.2.0, macOS,git 2.50.1.And through the installed hook, in a repository with
commitlore initrun:$ git commit -F cap.txt shape failed · references ok 6: unknown-key Co-Authored-By — ... commitlore: 1 violation (SPEC §6) — the message was not modified # commit count unchanged — the commit did not happenPosition matters, which is why this is not hit constantly: a
Co-Authored-By:line separated from the record by a blank line sits in the body, is not parsed as a trailer, and is never judged. It fails when it sits in the final trailer block — where a co-author trailer belongs.Source boundary
Two lists of foreign trailer keys, with different matching semantics, and validation reads the stricter one.
against this, in the same repository:
So the fact is already established here, in a comment, citing a report that observed all three casings in one repository. Ingestion acts on it; validation does not.
Present in the shipped release:
How it was found
Two headless Claude Code runs (
claude -p, plugin loaded,init --unattended) on unrelated tasks, neither told anything about CommitLore. Both emittedCo-Authored-By:unprompted. The first placed it in the body and committed. The second placed it in the trailer block alongside the record it had just captured, was refused, and worked around it by rewriting the trailer to git's casing.That second run is the shape a user meets: capture succeeds, the record is well-formed, and the commit is then refused for the attribution line the host wrote itself.
Proposed resolution
The narrow fix is case-insensitive membership in
WELL_KNOWN_FOREIGN_KEYS. The better one is that this list stops existing twice:isDefinedKeydelegates toisConventionalTrailerKey, so there is one set of foreign keys with one matching rule, and a casing added for ingestion cannot fail to reach validation.The comment above the constant pins the constraint the fix must not weaken —
Constraint:must keep failing asunknown-key, whichspec/fixtures/invalid/03-unknown-key.txtexists to hold. Case-insensitive matching over a two-element foreign list does not reach that fixture; delegation has to be checked against it, and againstEXTENSION_KEY_RE, before it is a safe swap.Whichever it is, the regression test is a validate over each casing this repository already lists in
types.ts, since the defect is precisely that one of them was never asked.I can send the PR if this reads correctly.
Scope note: no
dist/rebuild — aware ofdocs/CANONICAL-BUILD.md.