PR #17 closed with a four-line diff across two files. One line was in the plist example inside the launchd article — not a config template shipped alongside the post, the body of the article itself, rendered in the browser for anyone who read it. The path in the example contained a real macOS username. Not a placeholder. Not /Users/<your-username>/.... The actual string tied to the machine the site runs on. Three more absolute-path strings with the same value were in a second file. All four had shipped. All four were live.
Why it shipped
The launchd article needed a concrete installation path. An inline XML example with a bare placeholder is ambiguous at a glance — a reader has to infer whether to substitute it literally, and whether the angle brackets are markup or convention. The draft used the real system path to make the example unambiguous. That’s defensible at write-time: show the reader what the plist actually looks like on a working machine.
The mistake was leaving it in. The decision was made in a context where the file was a draft. That context disappeared at publish. The decision stayed.
This is a specific class of write-time decision: one that is correct precisely because it’s vivid, and dangerous precisely because it’s vivid. Replacing a real path with a placeholder makes the example marginally less concrete. The shortcut — keep the real thing, replace it later — is how the leak happens.
Blocks 15 and 16
The username fix landed in the same audit window as two other privacy-class problems.
Block 15 was /learning/. The page aggregated GitHub language data across the account and rendered it as a skill breakdown: percentages by language, grouped into career-level tiers. Accurate data. The problem was what the aggregation was naming. The page exposed five private repo names — BBBRAIN, EDITORIAL-STARTER, UBERTUTORS-INSTANCE, MARKET-VISION-CORE, BYTE-BRIDGES — as the source of the language data. All five are commercial projects, all private. The page was publishing an index of active commercial work alongside the public blog.
Block 16 was /now/. Same exposure as /learning/: private names visible with no reason for them to be public. On top of that, a hardcoded last-updated date written once and left, steadily drifting false. And a layer of overcommitments — implied availability and turnaround times that hadn’t been explicitly decided.
Three surfaces. Three variants of the same failure.
The common thread
None of these were bugs in the conventional sense. The username in the plist example was accurate. The repo names on /learning/ were accurate. The last-updated date on /now/ was accurate when it was written. Each value was correct in the context where it was produced.
The failure was that “correct at write-time” doesn’t mean “correct to publish,” and certainly doesn’t mean “correct to leave up as the site evolves around it.” A system path that identifies the machine is information the article doesn’t need to make its point. Private repo names that map back to client work have no business appearing on a public-facing page. A hardcoded date is accurate once.
The reflex that catches this is different from the correctness reflex. tsc doesn’t know the difference between a real system path and a placeholder. The linter doesn’t either. A human reading the rendered page notices in about three seconds.
The fixes
For the username, substitution. The four lines became four lines with <your-username> as the explicit placeholder form, HTML-escaped inside MDX code blocks where angle brackets would otherwise be parsed. The reader gets the path shape. The system state doesn’t ship.
For the private repo names on /learning/, the fix was to strip the identifiers from the output surface. The aggregation still runs against all repos; the page no longer names which repos it drew from. The skill breakdown is visible. The project names aren’t.
For /now/, the last-updated value stopped being a hardcoded string and started deriving from the most recent git commit to that file. The date shown is the date of the last edit. It can’t drift because it has no manual state to become stale.
Going forward
The ~/.git-hooks/pre-commit hook now blocks any staged content containing the real username, case-insensitive. Global scope — not just captainrandom.co.uk, every repo using the global hook path. The leak class surfaces as a pre-commit error rather than a production discovery.
CLAUDE.md carries the rule explicitly. For machine-config templates that genuinely need an absolute path — plists, install scripts — the convention is __USERNAME__ as a sed-friendly placeholder, with a documented substitution step in the runbook. The template stays clean in the repo. The substitution happens at install time on the target machine.
The /learning/ page now only exposes identifiers that are safe to expose: language names, percentages, tier labels. Nothing that maps back to a project or client.
The derivation principle applies beyond that one page. Where a value can be derived from git at render time rather than set manually at write time, it should be. Derived values stay current by definition. Hardcoded values are accurate once. The last-updated field on /now/ is the simplest case — written once, wrong a week later, now derived from the most recent git commit to the file. The value no longer drifts.
The pattern
Absolute paths expose system state the article doesn’t need to make its argument. Private project names expose context the public page doesn’t need to present its data. Hardcoded dates expose a claim the file can’t maintain.
In each case the value was doing real work at write-time. The path made the plist concrete. The repo names identified the data source. The date marked the revision. Each was a reasonable thing to include. The question — which the production audit surfaced only after it had worked through most of the site — was whether the specific value doing that work was the right vehicle for it.
A placeholder makes the same argument the absolute path made. Language percentages make the same data-source argument private repo names made. A git-derived date makes the same currency argument a hardcoded date made, without the drift.
The reflex that runs at write-time asks: is this correct? The reflex the audit is training asks: what does this expose, and does the page need to expose it? Both need to run. The second one is learnable. The hook enforces it now at commit time rather than waiting for a live-site review to catch it.



