The default that failed open
16 October 2026 · Guardrails & fail-closed · the engineering team
The story
We added a deny-list to our publisher: a list of terms that must never appear on a public page, however they are spelled. It was threaded through every check, carried on the same object as the other rules, and tested on prose, headings, table cells, link addresses and the summary. The last check before a page is written, a scan over the finished page data, took the deny-list as its third argument.
That third argument had a default: an empty list. It seemed harmless. Every real caller passed the list. The default was there so that a dozen existing unit tests, written before the deny-list existed, would keep compiling without being touched.
The reviewer of that change wrote one line about it: a caller that forgets the argument fails open. We agreed, and we fixed it in that same function. Then the final review of the whole branch found the same shape three more times, on three other entry points, left exactly as we had left them before the first review. The readiness preview, the review-site builder and the profile builder all took the deny-list as an optional argument with an empty default. Every real caller passed it. Every one of them was one forgotten argument away from publishing a term the list forbade, and nothing in the code would have said a word.
The pattern
A default is a decision the caller did not make. For most arguments that is fine; a page size of twenty is a reasonable thing to assume. For an argument that protects a boundary, it is the opposite of fine. The whole value of the guard is that every path through the code carries it. A default means a path can omit it and still compile, still run, still pass every test that does not happen to exercise the omission.
Required arguments make the compiler the reviewer. We removed the defaults. Every function that takes the deny-list, the forbidden-identifier patterns, or the set of sections a page is allowed to show now takes them as required parameters. The call sites that had relied on the default broke at type-check time, which is the moment you want them to break. Each one was fixed by passing the real list, or, in a test, by passing an empty one on purpose, where a reader can see that the choice was made.
Look for the same shape everywhere, not only where it was found. The first fix was correct and incomplete. A finding that reads "this argument has a fail-open default" is almost never about one argument. It is about a habit, and the habit left the same mark in every file written under it. When a review finds one, grep for the rest before closing the finding. Our final review did that for us, at the cost of a second fix round that a grep would have avoided.
The same rule applies to configuration. A path variable that is set but empty should not silently mean the default; it should refuse and name the variable. A date override meant for tests should be refused by the command that publishes. A missing key in a settings file should stop the build, not become undefined that renders as nothing. In each case the question is the same: if someone gets this wrong, does the system stop, or does it carry on with a value nobody chose?
There is a cost. Required arguments make tests more verbose, and a few call sites pass an explicit empty value that looks pointless. We think that is the right trade. The verbose line says "no deny-list here, on purpose", and a reader can disagree with it. The default said nothing, and nobody could.
Try it
Pick one function in your code that enforces a boundary: an allow-list, a permission check, a redaction step. Look at its signature. If the thing it enforces with is optional, remove the default and let the type-checker find every caller. Fix each one by passing the real value, or an explicit empty one with a comment saying why. Then search for the same pattern in the function's neighbours, because the habit that put the first default there put others nearby.