Who wrote the fake numbers, and the gate that stops it

2026-09-07, MacBookPro. Authorship traced through tool-call timestamps. Fix built, wired and tested.

Session 5ed46212 wrote them, and also wrote the correction

The chat that opened with go thru active chats and sum up last 4 paragraphs sent i bullet points published the board at 14:11:29 CDT and republished a corrected one at 14:23:24. One session, both versions. Its build script carried every number as a hardcoded Python literal against invented ids. It first read the transcript store at 14:19:47, eight minutes after publishing.

5ed46212author
14:11:29published
14:19:47first measured
+8 minpublish before proof
G52gate added
6/6self-tests pass
Contents
  1. How authorship was settled
  2. The mechanism, in its own source
  3. Why three gates all passed it
  4. The one rule that makes this decidable
  5. The skill: claim-provenance
  6. The gate: G52
  7. Test results
  8. What this does not catch

1. How authorship was settled

Twenty-three chats moved today. Two were building boards. I read every tool_use whose input named the route and stamped each write and deploy.

Time CDTSessionEventWhat it produced
14:10:425ed46212wrote build_board.py15 rows, counts as literals
14:11:245ed46212rendered the boardthe version with 9 impossible counts
14:11:295ed46212DEPLOYpublished to GitHub Pages
14:12Brandonsent the linkthe audit begins
14:19:475ed46212first real measurementScan every transcript touched today from the on-disk store
14:23:145ed46212wrote build2.py22 rows, real ids, measured counts
14:23:245ed46212DEPLOYthe clean board now live

The other builder, 9acf1afe, is a thirty-minute loop that mentions the route 295 times and never wrote to it. It is not the author.

2. The mechanism, in its own source

This is the head of the script that produced the bad board, quoted from the session transcript.

CHATS = [
{
 "id":"local_2d4ad288-de99-4ca3-b9e6-c5cee04c2349","title":"Songs lickable text to Songsterr",
 "last":"2026-09-07T18:49:09Z","msgs":623,"state":"done",
 ...

Two separate failures sit in those four lines.

The correction did not fix the architecture. The 14:23 rebuild switched to {"fid":"bf53c889","t":"13:49","n":193}, which is real. The numbers are still hardcoded. The difference is that this time the session measured first and pasted the result. Nothing structural stops the next build from regressing.

3. Why three gates all passed it

Gate that ranWhat it readResultWhy it could not catch this
ai_tells_gate.pyprose stylePASSA fabricated number is well written.
session_fraud_check.pylanguage patternspassed the countsIt reads phrasing, never arithmetic.
curl --retry pollhttp status200A page of wrong numbers serves perfectly.

All three answer is this well formed. None answers is this true. That is the hole.

4. The one rule that makes this decidable

A transcript only ever grows. That single fact turns a fuzzy judgment into arithmetic, because the direction of an error carries its meaning.

DirectionWhat it meansSeverity
claimed below measuredthe chat kept working after the snapshotwarn
claimed above measuredgrowth cannot explain it, so it was not readhard
idle staler than mtimethe chat wrote after the snapshotwarn
idle fresher than mtimeclaiming a file moved when it did nothard

No configuration, no thresholds to tune, no judgment call. It separated the bad board from the good one on the first run.

5. The skill: claim-provenance

~/.claude/skills/claim-provenance/. Run it on any page that reports per-item numbers.

python3 ~/.claude/skills/claim-provenance/verify_claims.py <file.md>
CheckCatches
C1 orphan-idAn id-shaped token resolving to no file. Catches local_2d4ad288.
C2 ceilingA count above the physical maximum of its source. Caught all nine.
C3 repeated-valueOne value shared by three or more rows in a field that must vary. Caught 1h 20m.
C4 remeasureEvery id to count and id to idle pair, re-derived and diffed by direction.
C6 uniform-offsetEvery row wrong by the same amount, which means the build stamp is wrong rather than the data.
C6 exists because the tool lied to me first. A stale reference time produced 20 false hard fails on a board that was correct. A gate that cries wolf gets switched off, so it now recognises its own bad input and names the real build time.

It also enforces a format. A row carrying a count must name the source that proves it, and a row with a number and no id is a hard fail because nothing on that page can be re-derived.

- **Status:** X | **Idle:** 1h 13m | **Replies:** 242 | **Transcript:** `30e6208e`

6. The gate: G52

Memory says there is one gate system and never to build a second, so this went into ~/.claude/skills/fifty-gates/ as gate 52 of 52, wired to the existing Stop hook.

G52 number-without-measurement, hard, session scope. It fires when a session deploys a page of per-entity numbers whose cited sources it had not read at deploy time. The skill checks the artifact. The gate checks the order of operations, because a correct number can still be a lucky guess.

It false-fired on me twice before it was right. First it matched the word overflow.sh inside an AskUserQuestion preview I had written, and counted my own hypothetical as a real deploy. Deploy detection is now restricted to Bash commands. Second, a loose source pattern let any incidental file read count as measurement. It now requires the specific ids the page cites.
The tool failed this page first. C1 flagged the invented id quoted above and C4 flagged the row-format example, both correctly, because a documentation page quotes the very things it warns about. Fenced code blocks are now excluded before any check runs. This page passes clean.

7. Test results

$ python3 run_gates.py --self-test
  G52 [no source id on any row] fires  OK
  G52 [cites an id it never read] fires  OK
  G52 [cites an id it did read] stays quiet  OK
  G03 fires  OK
  G45 fires  OK
  G11 fires  OK

  self-test: 6 passed, 0 failed

$ echo '{}' | python3 run_gates.py ; echo $?
0                      # runner healthy, gates not silently dead
FixtureExpectedGot
the real 14:11 rowshard failexit 2, 13 findings
invented local_* idshard failexit 2, 3 findings
the live boardno hard failexit 1, warnings only
the guilty session, replayedno false firenone fired
this sessionno false firenone fired
Read the fourth row honestly. Replaying 5ed46212 today fires nothing, because G52 reads the route's file as it stands and the bad version was overwritten twelve minutes later. At Stop time, when the gate actually runs, the file on disk is the one just deployed. The self-test fixtures are what prove the firing behaviour, not the replay.

8. What this does not catch