What happened
.ai/.githooks/commit-msg rejected this line in an ordinary commit message:
The audit file: `.audit/<user>_<branch>.md`, generated by `.ai/justfile` and
committed as the first commit of every dev branch by `just new-branch`.
with:
ERROR: Commit rejected - violates the project's AI_POLICY.md
The commit message contains AI authorship attribution which is prohibited.
There is no authorship attribution in that sentence. It describes which tool
produces a file.
Why
grep -i -E "(authored by|co-authored by|generated by).*(claude|gemini|copilot|ai|artificial intelligence)"
The ai alternative is a bare, unanchored substring. After generated by, the
.* will reach any later ai anywhere in the message - including inside an
ordinary word. All of these are rejected today:
| message fragment |
why it matches |
generated by `.ai/justfile` |
the literal path |
generated by the maintainer |
maintainer |
generated by an available script |
available |
generated by the certificate chain |
chain |
generated by the CI pipeline, details above |
details |
Pattern 3 has the same defect: (generated with|created with|built with|made with).*(claude|gemini|copilot|ai).
Pattern 2 is fine - it requires a trailer key first, and the alternation is
applied to a line already matched as ...-by:.
Why this matters more than a nuisance
A guard that refuses correct work is the guard that gets switched off.
That is not a hypothesis here. aaiare-metal-typedefint#58 records a clone of
system-and-software-assurance found running with
core.hooksPath = /dev/null - the exact value this hook's own error message
tells a human to set - with the re-arming step never performed. Every commit-msg
check was inactive there for months, on the machine that holds the credentials.
Nobody knows why it was disabled that time. But a hook that rejects
generated by the maintainer supplies the motive, and this is the second time
today it has refused a legitimate commit from an ordinary sentence.
The estate's own axioms name this: a check that cannot pass will be muted.
False positives are the slow version - the check can pass, but only after the
author rewrites a true sentence to appease a regex, and eventually someone
reaches for /dev/null instead.
Suggested fix
Require a word boundary and drop the bare two-letter alternative in the prose
patterns:
# was: (claude|gemini|copilot|ai|artificial intelligence)
# now:
"(authored by|co-authored by|generated by)[[:space:]]+(claude|gemini|copilot|chatgpt|an? ai\b|artificial intelligence)"
Two changes, and both matter:
[[:space:]]+ instead of .* - attribution is adjacent to the phrase.
.* lets the phrase match something twenty lines away that was never part of
the same claim.
an? ai\b instead of ai - "by an AI" and "by AI" are attribution;
"maintainer" is not.
Acceptance criteria
- Observed refusing, output pasted, for each of:
Co-Authored-By: Claude <...>, Generated with Claude Code, authored by an AI.
- Observed ACCEPTING, output pasted, for each of:
generated by `.ai/justfile` , generated by the maintainer, built with available tooling - and a message ending in the sanctioned
Note: This work was completed with AI assistance (Claude Code).
- A note in the hook explaining why the alternation is anchored, so the bare
ai is not reintroduced by someone broadening the check in good faith.
Note on blast radius
This hook is shared by every repository using .ai, so the fix propagates on
the next submodule update - which is the reason to keep the change small and its
negative cases explicit. The second acceptance criterion is the one that matters:
a stricter guard is easy, and this one is already too strict.
What happened
.ai/.githooks/commit-msgrejected this line in an ordinary commit message:with:
There is no authorship attribution in that sentence. It describes which tool
produces a file.
Why
grep -i -E "(authored by|co-authored by|generated by).*(claude|gemini|copilot|ai|artificial intelligence)"The
aialternative is a bare, unanchored substring. Aftergenerated by, the.*will reach any lateraianywhere in the message - including inside anordinary word. All of these are rejected today:
generated by `.ai/justfile`generated by the maintainergenerated by an available scriptgenerated by the certificate chaingenerated by the CI pipeline, details abovePattern 3 has the same defect:
(generated with|created with|built with|made with).*(claude|gemini|copilot|ai).Pattern 2 is fine - it requires a trailer key first, and the alternation is
applied to a line already matched as
...-by:.Why this matters more than a nuisance
A guard that refuses correct work is the guard that gets switched off.
That is not a hypothesis here.
aaiare-metal-typedefint#58records a clone ofsystem-and-software-assurancefound running withcore.hooksPath = /dev/null- the exact value this hook's own error messagetells a human to set - with the re-arming step never performed. Every commit-msg
check was inactive there for months, on the machine that holds the credentials.
Nobody knows why it was disabled that time. But a hook that rejects
generated by the maintainersupplies the motive, and this is the second timetoday it has refused a legitimate commit from an ordinary sentence.
The estate's own axioms name this: a check that cannot pass will be muted.
False positives are the slow version - the check can pass, but only after the
author rewrites a true sentence to appease a regex, and eventually someone
reaches for
/dev/nullinstead.Suggested fix
Require a word boundary and drop the bare two-letter alternative in the prose
patterns:
Two changes, and both matter:
[[:space:]]+instead of.*- attribution is adjacent to the phrase..*lets the phrase match something twenty lines away that was never part ofthe same claim.
an? ai\binstead ofai- "by an AI" and "by AI" are attribution;"maintainer" is not.
Acceptance criteria
Co-Authored-By: Claude <...>,Generated with Claude Code,authored by an AI.generated by `.ai/justfile`,generated by the maintainer,built with available tooling- and a message ending in the sanctionedNote: This work was completed with AI assistance (Claude Code).aiis not reintroduced by someone broadening the check in good faith.Note on blast radius
This hook is shared by every repository using
.ai, so the fix propagates onthe next submodule update - which is the reason to keep the change small and its
negative cases explicit. The second acceptance criterion is the one that matters:
a stricter guard is easy, and this one is already too strict.