$ cat work/shipping-a-claims-lens.md
The lens that stops me lying about my own work
A placeholder number in a planning document was lifted, verbatim, into two published social drafts as a true client story. The fix was not being more careful. It was a linter for claims that runs on every commit.
Node.js · regex · git hooks · GitHub Actions
$ metrics
The problem
A voice document in this repo carried an example figure inside an '(e.g.)' placeholder. It was illustrative. It was not about anyone.
A drafting pass picked it up and wrote it into a LinkedIn draft and an X draft as a first-person client outcome, with a timeframe attached. Two channels of the same pack cited two different numbers for the same imaginary fact, which is the only reason anyone noticed at all. Nothing was published. Nothing in the repo could have stopped it if it had been.
The constraints
A checklist does not work, because the failure mode is not carelessness, it is a plausible sentence arriving from a file that was never meant to be a source.
So the gate has to be mechanical, it has to run without being remembered, and it has to fail closed. It also has to be quiet enough that it does not get switched off: a check that cries wolf gets bypassed within a week, which means false positives are not a cosmetic problem, they are the whole risk.
The architecture
Two lenses over the same file walker. The character lens is mechanical and fixable in place. The claims lens needs judgment, so it reports and blocks but never rewrites.
git commit
|
+-- .git/hooks/pre-commit
| node text-hygiene.mjs --staged (characters: fixable)
| node text-hygiene.mjs --staged --phrases (claims: judgment)
|
v push
GitHub Actions: npm run hygiene && lint && test
--phrases categories:
ai-vocab the assistant-shaped adjectives (22 patterns total)
round-number a tidy percentage range -> asks for an odd specific
unpermissioned-claim 9 patterns: client outcomes, shipped counts,
dated client interactions, self-funding claims
launch-cosplay
exemptions are per-CATEGORY, not per-file:
voice.md is exempt from round-number
voice.md is NOT exempt from unpermissioned-claim <- the pointDecisions that mattered
Exemptions are scoped to categories, not files. The document that defines the banned vocabulary necessarily contains it, so scanning it for ai-vocab is guaranteed noise. But the same document held six quotable first-person client claims for weeks, and an all-or-nothing exemption is exactly why nothing noticed. It is exempt from the vocabulary categories and permanently subject to the claims ones.
The claims lens refuses to run with --fix. There is no transliteration of an un-permissioned claim into a permitted one; the only fixes are 'cut it' or 'get permission and write it into the allowlist with its caveat'. A tool that offered to auto-fix this would be lying about what it does.
Permission from the client is not sufficient on its own. The figure also has to land in the allowlist with its exact scope, because a number without its caveat is a different claim.
The patterns stay in the past tense on purpose. A retrospective claim that a project funded itself is a statement about one engagement and needs permission. The present-tense version of the same words is an argument about a category, which anyone is free to make. Widening the pattern to catch both would have blocked a general thesis and forced an allowlist entry for the one file it caught, and that is how a gate starts collecting exceptions and then gets ignored.
What broke
The gate was armed against the wrong thing for weeks. Only the character lens ran in the pre-commit hook. The claims lens existed, had 64 passing tests, and had never scanned a single published file - the npm script ran it without --phrases, which silently selects the other lens.
Worse, the documented per-post ritual called it with --phrases and no path. That scans zero files and exits 0, printing '0 file(s) scanned, clean.' Every post would have passed through a check that was structurally incapable of failing.
And it lived only in .git/hooks, which is not versioned, absent on a fresh clone, and skipped by --no-verify. It now runs in CI, where the machine doing the committing does not get a vote.
The outcome
Turning the claims lens on across the repo produced 17 findings, and every one was a false positive in a file that quotes the banned patterns in order to define or test them. That was the reassuring result: the published surface was clean, and the exemptions needed to describe why those four files are different rather than waving them through.
The reason this is a case study and not a footnote: most engineering trust problems are solved by being more careful, right up until the day they are not. This is what the mechanical version looks like, including the part where the mechanism itself was broken and had to be caught by using it.