The identifiers I didn't know I'd published

The /learning page went live during block 15 of the production audit. The language aggregation was working: pulling from GitHub, bucketing by language, surfacing a tiered breakdown of what I was working in. The labels on the output were the raw repo names.

The repo names were BBBRAIN, EDITORIAL-STARTER, UBERTUTORS-INSTANCE, MARKET-VISION-CORE, BYTE-BRIDGES. All private. All commercial. Now rendering on a public page because the code was doing exactly what I’d asked it to do.

That was the first one. There were four more.

The pattern

Across five separate fixes, the same root cause repeated: an identifying string that made sense in a private context had reached a public file because no mechanism existed to stop it.

Not tracking. Not analytics. Not a deliberate data-collection decision I’d made and later reconsidered. The category was quieter: text typed somewhere internal, included in something public, without the boundary between the two ever being made explicit.

The five instances, in order of discovery:

/learning — raw repo identifiers as the labels on the language aggregation output. The aggregation logic was correct; the label set was wrong. Scrubbed to language categories: Python, TypeScript, SQL, Swift. The public story is what I’m working in, not what I’m working on.

/now — the same pattern at block 16, extended to implementation details: which private client project was the current focus, what internal tooling it depended on. Reframed to work-in-progress signals that don’t name the underlying work. Last-updated timestamp derived from git rather than hand-set — that change also closed a separate overcommitment issue on the same page.

A published post — one of the technical posts had a plist example with the macOS username embedded in an absolute path, rendered live on the site. PR #17 replaced four lines across two files with <your-username> placeholders. The article was technically correct; the path it showed was not.

/privacy/ — not a leak in the same sense, but an absence. P8-10 shipped the policy page alongside the newsletter form. Out of scope here: third-party script audit, cookie classification, analytics configuration. Those are their own sprint.

The pre-commit hook — the systemic fix rather than a fifth instance. A global hook that scans staged content for the macOS username and the full real name, blocking the commit if it finds either. Bypass token for deliberate use; the hook logs bypasses to stderr.

What the category actually is

Privacy failures in engineering discourse tend to assume infrastructure: trackers embedded without consent, over-broad data retention, harvested contact details. Those are real problems. They’re not what any of these were.

The category here is accidental identifier. A string that is identifying and private ends up in a public file because the person typing it was thinking about something else. In a team, code review catches this. A reviewer reads the diff as an outsider and notices the path that looks personal, the repo name that looks internal. They ask.

In a solo dev setup, there’s no second pair of eyes unless you build one.

The /learning fix illustrates the mechanism. The aggregation code pulled repo names as labels. Every repo I had was a private project. There was no logic error; the code did exactly what I’d written. The privacy issue was in an assumption I hadn’t made explicit: that whatever ended up as a label would be safe to show publicly.

The plist fix is more instructive, because the article it lived in was about something else entirely. I’d written a technical guide on macOS permissions. The guide was accurate. The plist had the real username in the path because that was the actual path on my machine, and I typed it, and nothing stopped it going through the PR. No amount of TCC-related verification catches a username in a code example.

The fix hierarchy

Name scrubs are tactical. The pre-commit hook is structural.

Tactical first: replace private identifiers with safe abstractions at the point of use. /learning shows languages, not repos. /now shows work-in-progress signals, not client names. These fixes are correct; they don’t prevent the next instance.

Template convention handles the authoring problem for code examples. Any machine-config that needs an absolute path now uses __USERNAME__ as a sed-friendly placeholder, with the substitution step documented in the relevant runbook. That convention was applied to the plist example via PR #17.

The hook operates at a different layer. It scans staged diff content for known personal identifiers and blocks the commit if it finds them. It doesn’t understand context — it doesn’t know whether the username is a template placeholder left unreplaced or an intentional reference. It just blocks, then asks. The bypass token covers genuine cases; the stderr log means bypasses are traceable.

Between the two: convention handles the deliberate authoring moment, the hook handles the rushed commit where you’ve forgotten what’s in the file. Neither replaces the other. Convention reduces noise for the hook; the hook catches what convention misses under pressure.

The audit method

Each of these fixes came out of the same structured production audit. The method was block by block: render the page, read it as an outsider, list every concern, triage, fix, commit, log. Blocks 15 and 16 — /learning and /now — came back-to-back in the same session. The published-post fix was a separate pass over already-shipped content rather than page surfaces.

The gap the audit closed wasn’t architectural. Both pages were working correctly. The gap was in an implicit assumption I’d been operating under: that internal naming conventions would stay internal. They don’t. The audit was the mechanism that surfaced it; the hook is the mechanism that catches it going forward.

What this is and isn’t

The /creative section got its first real track when Tasogare — Cinematic Boom Bap replaced the “first track coming soon” placeholder. That’s a different category: a placeholder honest about absence, replaced by a real thing. Not an accidental identifier.

The privacy policy at /privacy/ covers data collection and contact form handling — tracking-adjacent territory and its own concern. The five fixes above are specifically about the accidental-identifier pattern.

Out of scope: analytics configuration, third-party script audit, cookie handling. Separate sprints.

The solo dev version of code review

Code review in a team catches accidental identifiers as a byproduct of having outsiders read the diff. They notice the path that looks like it belongs to a specific machine, the repo name that sounds like a client. They ask, because they don’t share your mental model of what’s private.

The solo dev equivalent requires building that outsider check into the tooling. Structured audit passes cover the historical surface. Template conventions make the private/public boundary visible at the moment of authoring. Commit-time gates catch what both of those miss.

Five fixes. One hook. The site is cleaner than it was. The same class of mistake is harder to ship going forward — not impossible, just stopped by a prompt that asks for confirmation before it goes through.

All writing