A published code block on the live site had the macOS username baked into it. Not in a draft — in a fenced code block on a page that renders at load time, indexed by search engines. The page was a how-to on macOS permission models. The username was in an absolute path four lines into a plist install example.
PR #17 fixed it: four lines across two files. The inline plist became /Users/<your-username>/, the path template got __USERNAME__ with a runbook note on the substitution step. Twenty minutes of work.
That was the third leak, not the first.
The audit trail
The /learning page had shipped the same week — a build-time language aggregation view, numbers pulled from GitHub’s /languages API, sorted by career tier. Real sources, real counts. The repo names driving those counts were not anonymised.
BBBRAIN, EDITORIAL-STARTER, UBERTUTORS-INSTANCE, MARKET-VISION-CORE, BYTE-BRIDGES. Five private commercial projects, listed by name on a public page. Fix: replace repo names with display categories. Same-day commit.
/now carried the same gap. Private project names in the status prose — details meaningful inside the work, opaque to anyone outside it. Strip the names, soften the commitments, derive the last-updated timestamp from git rather than asserting it as a claim. One commit.
Two days later: the published plist example, the username in an absolute path, PR #17.
Phase 8, a month after that: the site had no /privacy/ page. The newsletter form was going live — real signups, real email addresses. The privacy policy rendered at /privacy/ as part of that ship. Different class of gap — a missing disclosure rather than a leaked identifier — but the same root: no publish-boundary privacy check.
Four surfaces. Four fixes. All after deployment.
What they had in common
Each fix looked different in the diff:
- Repo names in a stats component
- Project names in status prose
- A system username in a code block
- A missing privacy policy
The common shape: author-context material crossing the publish boundary without a check. The author knows what BBBRAIN is. The author knows their macOS username. That context makes the names meaningful while writing. At the publish boundary, they become identifiers a reader can collide against — in search results, in archived versions of the page, in cached renders.
The cognitive step required to catch them is: does this detail make sense to a reader with no access to my internal project structure? That question is not part of correcting code. It is not part of style review. It has to be applied separately, consciously, by someone shifting between two frames: author-context and reader-context.
The production audit that caught /learning and /now was explicitly a reader-context exercise. The stated goal was to read every page as a visitor would. Under those conditions, with that intent, the privacy issues were still follow-up commits rather than first-pass catches. That’s not an argument for more vigilance — it’s an argument for a different mechanism.
Four incidents across two months say that the reflex doesn’t fire reliably under normal review conditions. That’s not a character flaw; it’s a property of the review loop. Writing and editing both happen in author-context. Reader-context is a mode shift, and mode shifts have friction.
The right layer
Manual vigilance applies when you apply it. It misses when you forget to shift modes. Four incidents in two months is a rate, not a tail of the distribution. If the site were shipping content faster, the rate would scale.
The fix that addresses the rate is a boundary check. Not in the author’s head, where private names are working context. Not in a post-deploy audit, where the fix has to reach a live site. At the boundary — the point where content transitions from draft to staged to deployed.
For a Next.js site with an MDX pipeline, that boundary has a natural implementation point: a commit-time hook or a pre-build lint pass that scans staged content files against a known-private identifier list. Pattern matching, not semantic analysis. A maintained blocklist: project names, path patterns, identifiers that have appeared in prior leaks. New entries come from future findings, not from speculation about what might someday matter.
The check is not expensive. Scanning MDX files against a list of strings is seconds of work per commit. The cost of not running it is measured in time-on-site for private data: unknown, uncontrolled, and often non-zero given caching and archiving.
Out of scope: catching private concepts rather than private names. “This section implies I’m working on a client project” is human judgement. It stays in the author’s head where it belongs. The tool catches what is nameable and enumerable. That’s the class that has produced every incident so far.
The runbook convention isn’t enough
PR #17 established a documentation pattern: published plists use __USERNAME__ as a placeholder, with a runbook note on the substitution step. Right fix for the specific case. Not systemic.
The convention depends on every future article with a system-path example finding the existing plist and following it — or the author remembering the convention exists. The fourth incident came after the third. The convention was in place. It didn’t help.
Conventions that depend on memory lose to the same pressure that memory fails under: context-switching, drafting speed, in-author-context review. A hook runs regardless. That’s the difference.
The blocklist starts short: the repo names from the /learning leak, the username pattern from the plist fix, a handful of internal project identifiers. It grows as incidents surface — enumerated from real findings rather than speculatively broad. A broad blocklist produces false positives and gets bypassed. A narrow one from real data gets obeyed.
What this is
Four fixes, one missing systematic check. The check belongs at the publish boundary. That’s where author-context ends and reader-context begins, and it’s the right place to enforce the boundary mechanically rather than manually.
The lesson from four incidents isn’t to be more careful at each one. It’s to stop treating authorial memory as the mechanism.



