A clean scan and three things it did not tell me

I ran a credential and PII audit across every repo I have on disk — gitleaks plus a scanner of my own for the things gitleaks does not care about, both against full history rather than the working tree. The verdict was clean: no keys, no private key material, no connection strings, nothing personally identifying that belonged to me.

The verdict was also the least informative output of the exercise.

The denominator is part of the finding

I scanned four repos because four repos were cloned locally. “What is on my disk” and “what is published under my account” are different sets, and I only had evidence about the first.

Enumerating the account over the API failed immediately — sixty requests an hour unauthenticated, shared across whatever else egresses from the same address, exhausted on the first call. The fix is one command with the GitHub CLI, which uses a stored token and never puts a credential on the command line:

gh repo list <owner> --limit 200 --json name,isPrivate,url

But the tooling is not the point. A security result without its scope stated is a claim, not a measurement. “Four repos clean” is a fact I earned. “My repos are clean” is a sentence I wanted to write and had not yet paid for.

Not every secret-shaped string is a secret, and not every secret is yours

Four Firebase web API keys came back flagged. They were in upstream history, authored by the original maintainer of a project I had forked, and Firebase web API keys are public by design — they identify a project, they do not authorize anything. Security in that model lives in the rules layer, and a key sitting in a JavaScript bundle served to every visitor is not a leak.

So: not a finding. And separately, not mine — I cannot rotate another maintainer’s project identifiers, and pretending otherwise would have generated a remediation ticket that could never close.

Triage runs in that order, and both questions matter. Skip the first and you file noise. Skip the second and you file work that cannot be done. Either way you get a scanner people learn to ignore, which is strictly worse than no scanner, because it consumes attention while providing false assurance.

The real finding was boring, which is how these always look

One thing was genuinely attributable to me: an email address baked into release tags: my username joined to my machine’s hostname, with a .local suffix on the end. Both identifiers, sitting in git metadata on a public repo.

Nobody is compromising anything with that. It is one line in somebody’s reconnaissance notes, and it cost me nothing to give away — which is exactly why it is the finding. The catastrophic leaks get caught, because everyone is watching for them. The metadata leaks accumulate because each one individually does not justify the effort of caring.

Set user.email per-repo on anything public and the class of problem disappears.

There was also an untracked tooling config that a careless git add -A would have committed. Nothing sensitive in it today. “Harmless right now” is a property of the current contents, not of the file — that is an argument for gitignoring it, not for leaving it alone.

An audit is a snapshot; the gate is the product

The genuinely useful output was not the report. It was turning the scan into a pre-commit hook, and then into a server-side pre-receive gate on the box that serves this site.

The distinction matters more than it looks. A pre-commit hook is advice — it runs on my machine, with my config, and --no-verify bypasses it in eleven keystrokes. A pre-receive gate runs on the server, cannot be skipped by the person pushing, and is the only one of the two that is actually a control. The hook is for catching mistakes. The gate is for the days I am in a hurry and would have overridden the hook.

Both matter, because the economics are lopsided. Catching a secret before the commit object exists is free. Removing one from published history is a rewrite, a force-push, and a rotation of everything exposed — and the rotation is mandatory regardless, because the object was public for however long it took you to notice.

I did not wire the same scan into CI, which was a deliberate call rather than an oversight: I had just spent an evening tracking down a surprise bill for scheduled workflow minutes, and adding more of them to solve a problem the pre-receive gate already solves is how that bill got surprising in the first place.

The four lines I would keep

— Cooper.