Essay

A blocklist fails open


Published
2026-08-25
Reading time
5 min
Tags
security, defaults

Sixteen of the nineteen repositories on this site are private. Deciding what a private repository is allowed to say about itself turned out to be a question about defaults, not about secrets.

01

A page called Code with nothing readable on it

The repository index used to render whatever the GitHub API returned. That meant eight of nine rows read "no description set", two repositories were literally named `test`, and the three strongest projects were missing entirely, because they are private and the public endpoint cannot see them.

Fixing the list was easy. Fixing what a private row is allowed to publish was not, because it is not one decision, it is a decision per field, forever, made by whoever edits the file next.

Language and last-push date are harmless. A README might not be. A directory listing might not be. A code excerpt depends entirely on which lines. And the answer differs per repository: two of them hold live trading credentials and a specification of what the system currently exploits.

02

The two shapes this can take

A blocklist says: publish everything, except these named things. It is the natural first instinct, because you are looking at the things you want to hide and they are right in front of you.

An allowlist says: publish nothing, except these named things. It is more work up front and it feels paranoid, because most of what you are refusing to publish is harmless.

The difference is not how much they hide today. It is what happens when something new arrives. A blocklist fails open: add a field to the output, forget to add it to the list, and it publishes. An allowlist fails closed: add a field, forget to allow it, and it is missing. Missing is visible. Leaked is not.

03

What that looks like in code

Every private repository on this site carries a policy object with three booleans: metadata, readme, structure. All three default to false. A repository with no policy publishes nothing at all, not even its existence.

The consequence is that every published field is a decision somebody made rather than a default nobody reviewed. Reading the file, you can see exactly what was considered and what was declined, because declining is the same amount of typing as allowing.

The build-time script that reads private repository metadata is fenced the same way. It reads its own environment variable, deliberately not the one the runtime uses, so granting the runtime token more scope cannot silently turn on enumeration. Everything the endpoint returns is discarded unless the name is in the allowlist. Even then only four scalars survive: name, language, last push, visibility. Never a description, a README, a topic list or a URL.

04

The same shape, one layer down

The retrieval corpus started as a blocklist too. It read a documents directory and skipped a handful of files by name. That is fine until somebody adds a file.

It is an allowlist now: an explicit set of filenames, and anything not in it is not indexed regardless of where it sits. There is a second set of things that must never be indexed under any circumstance, and a build-time assertion that fails the build if any of them reach the output.

That assertion caught something real. A document I had written myself mentioned an excluded project by name, in a sentence about a different project's origins. It was not a secret and it was not dangerous. It was simply not supposed to be there, and the only reason it was found is that the check was written to fail closed and then proved by planting a violation.

05

What it costs

Allowlists are more typing and they are annoying in exactly the way that makes them work. Ten of the private repositories on this site have a field explaining why they are private, and nine of those started as a placeholder that rendered nothing rather than a guess that rendered something.

The cost is real: a page that says less than it could, for a while. The benefit is that the failure mode is a hole rather than a leak, and holes get filled.

All writing