The Guard That Ate the Catalog
A supply-chain scanner I wrote rejected a real model name, took 222 providers down with it, and reported success the whole time. Then I fixed it and did the same thing again.
I was adding a model provider to a registry I maintain. Routine work. A table entry, a base URL, a test.
Then I noticed one provider sitting at zero models.
Then another.
Then nine more.
Nothing had crashed. No test had failed. Every log said the run succeeded.
The name that did it
The registry reads a public catalog of AI models. Who serves what, at what price, with what context limit. 222 providers in one JSON file, maintained by strangers.
Because strangers maintain it, I scan it before I trust it. One of the things I look for is base64 — a long run of encoded characters sitting where a model name should be. That check is not paranoia. Someone once slipped encoded credentials into a community-maintained pricing file, and every tool that ingested it passed the payload along.
So the rule seemed fair. Thirty-two or more characters from the base64 alphabet, mixed case, in a field that should hold a name. Reject the file.
Here is what tripped it:
deepinfra/thinkingmachines/Inkling
Thirty-three characters. Upper and lower case. Every one of them legal base64.
It is also, plainly, a path. A host, a lab, a model called Inkling.
My scanner could not tell those apart. And because it threw out the whole file instead of the one line, every provider in it went dark together. Eleven reported zero models. NVIDIA, Together, Fireworks, Groq, Mistral, DeepInfra, Cohere, Cerebras, Perplexity. All of them working. All of them invisible.
Everything said it was fine
An error that announces itself is cheap. You see it, you fix it, you carry on.
This one reported success.
→ The error surface I had built for exactly this returned an empty list.
→ 948 tests passed. The build was green.
→ The weekly job that publishes all of this ran every Monday for two months, reported success every time, and committed nothing — because a second guard, somewhere else, had a condition that could never be met.
Three layers of vigilance. All agreeing with each other that everything was fine.
The data had been stale since July. I found that out the same afternoon.
Sitting there with a scanner that had locked me out of my own catalog, one thought kept circling.
A rule that binds the thing it was written to protect is not protection.
It is a rule wearing protection’s clothes.
Then I did it again
I fixed the base64 rule properly. Instead of guessing from the shape of a string, it now decodes the string and looks at what comes out. An encoded credential gets caught on its contents. A model path decodes to noise and passes. I measured the cost of the change over two hundred thousand samples so I could say what it gave up rather than guess.
I was pleased with myself. I wrote a long note explaining why the old rule had been wrong, and something in me said, good — now you understand this.
In the same release, I added two new rules. One of them was for what is called trojan source — invisible characters that make text render differently from how it actually reads, so a model ID can display as one thing and resolve as another. Real attack. Worth catching.
I wrote it too broadly.
A few hours later, regenerating the snapshot, I watched Vercel come back with zero models. 375 of them, gone.
The culprit was a single character in OpenAI’s own description of one of their models:
GPT-5.2-Codex is a version of GPT-5.2 further optimized for agentic coding
^
U+2060 WORD JOINER sits here, invisible
U+2060. A word joiner. A typographic nicety that stops a line breaking in an awkward place.
Same shape. Same failure. Same week.
Written by the person who had just finished explaining why that mistake was a mistake.
What actually changed
Three things, and only one of them was a rule.
Decode, do not guess. A heuristic about what a string looks like will always mistake real names for threats, because real names are not random. So the check stopped reasoning about shape and started reading contents.
Quarantine the entry, not the feed. A file with one bad line now loses that line. Two hundred contributors maintain that catalog. Any one of them should be able to publish something odd without taking down everyone else. If more than half the file trips the scan, that is a different situation and it still fails shut.
Name only what does harm. The trojan source rule now lists the nine characters that actually reorder text, and nothing else. I had been flagging every invisible character, including two called ZWNJ and ZWJ.
Those two are not attacks. They are required to render Devanagari, Persian and Arabic correctly.
The registry is called kosha. कोश. Treasury.
I had written a rule that would reject its own name.
The part that stays with me
I keep thinking about how cheap it was to add that rule.
Six lines. It felt responsible. Nobody reviews a commit that makes a security check stricter and asks whether it is too strict, least of all the person writing it. Strictness reads as care.
But every guard I add is a claim about the world — that I know what belongs and what does not. Most of the time that claim is untested. It sits there being correct about the cases I imagined and silently wrong about the ones I did not, and it reports success either way.
The first bug cost me eleven providers. The second cost me 375 models. Neither announced itself. I found both by noticing a zero I had no reason to be looking at.
What unsettles me is not that I got it wrong twice. It is that being careful is what got me there. I was not being sloppy when I wrote either rule. I was being protective, and protective went unexamined, because protective never has to justify itself.
I do not think the lesson is to guard less.
It is that a guard is something I built, not something I inherited, and it deserves the same suspicion I give everything else I build. The ones that never fire look exactly like the ones quietly eating what they were meant to keep.
Something sits in the doorway refusing everyone, and calls it safety.
And nobody notices, because from the outside a locked room and an empty one look the same.