On June 16, a 93-line script turned one publication rule into an executable gate.
The rule was simple: this site is pseudonymous. The failure mode was not. We had already made several anonymization passes over imported drafts, then the same categories of private text kept resurfacing in the next review. An old prompt held a forbidden punctuation mark. A draft could carry an identity marker in a paragraph, a code sample, or a stray editorial note. Each instance looked small. Across a publishing backlog, small is how a boundary turns into a hope.
Manual review was catching some of it. That was the diagnosis, and it was also the problem. A reviewer can notice a name in a sentence. They are less reliable at remembering every restricted variant while also checking story, facts, code, and tone. The review system had a human memory problem disguised as an editorial cleanup problem.
I stopped trying to solve it with one more cleanup pass.
Four patterns, one command
The commit added scripts/check-forbidden-terms.mjs, a 93-line Node script, and exposed it through a package command:
npm check:forbiddenThe scanner carries four forbidden patterns. They cover a personal identifier, two business identifiers, and a punctuation character that does not belong in this publication. I am not publishing the values. The public mechanism matters more than the strings: put the restricted vocabulary in code, search the publishable material deterministically, and fail the check before the content crosses the public boundary.
That last part changes the nature of the rule. A style note says, “Please remember this.” A command says, “Run this before you ship.” The first asks a person to keep context active. The second gives every draft the same test, regardless of who wrote it or which agent helped produce it.
The commit made 96 changes across three files. Most of that was the scanner. The remaining changes mattered too: one existing post needed a two-line correction for the forbidden punctuation in a flagship lesson prompt, a post that was already published, already public, when the scanner found it. Not a draft caught before it shipped. Manual review had passed that post at least once already, and the residue sat live on the site until a script, not a person, went looking. I do not know how long it had been up.
The options I rejected
The first rejected option was a bigger manual checklist. We already had review steps, and adding another line would have created another condition for someone to recall under pressure. The failure was not that the rule was unclear. The failure was that enforcement depended on attention.
The second rejected option was periodic batch remediation. We had used remediation passes because the backlog was real and needed to be cleaned. But a batch pass repairs yesterday’s files. It cannot stop a new private term from entering tomorrow’s draft. I needed a boundary that applied every time, not another event on the calendar.
I also did not fold this into a broad, subjective style check. Pseudonymity is not a taste judgment. A prohibited term is present or absent. Treating it as a deterministic lint lets the editorial review focus on the parts that actually need judgment.
What I actually believed
I ran those review passes because I believed a careful read was the safeguard. Not a hope, a belief I acted on: it is why I kept reaching for one more cleanup pass instead of building a gate, pass after pass, on the theory that catching a forbidden identifier was the same kind of job as catching a weak sentence, and a person doing both at once could be trusted to do it.
I was wrong, and the site carried the cost. That flagship post had already been through manual review at least once, the same review I was relying on to hold the boundary, and the forbidden punctuation and the identifier tied to it were still sitting live on a published page when the scanner went looking. I do not know how long they had been up. I know the review I trusted had already had its chance at that exact post and missed what was in it.
It would have been easy to blame whoever did that particular pass. That misses the point. The belief that put the leak there was mine: that a person checking story, facts, code, and tone at the same time could also reliably hold four forbidden patterns in memory. A script found in one run what a review process I trusted had already failed to catch.
What the failure really was
The actual failure was that the constraint lived outside the build, inside my own confidence that memory was enough. The repository could compile a site that violated a rule I agreed with, because the rule existed only in my head and my habits. Once npm check:forbidden became part of the workflow, it gained a stable interface, a script path, and a repeatable invocation that did not depend on how carefully I happened to be reading that day. The scanner is not sophisticated. It does not need to be. Its job is to catch what I already proved I would miss.
The 93-line script caught the leak on its first run, on a post the review process had already passed. That gap, between what my trust in that process said and what the script actually found, is the real measure of how much I had been asking memory to do a machine’s job.