We Published Forty-Seven Scopes and Built Eight
Our public registry advertised 47 things a person could offer the world. The server honoured 8. The other 39 resolved to nothing, silently, with no error - and five of our own published agent types depended on them. Here is how it happened and what we changed so it cannot again.

At hushh.ai/pchp/scopes.json there is a file that calls itself the canonical scope registry for the Preference Subscription Fabric. It is public. Agents fetch it to learn what a person can offer the world. It listed 47 scopes — privacy preferences, sizes and channels, what someone is in the market for, favourite brands and artists.
The server implemented eight of them.
The other thirty-nine resolved to zero fields. Not with an error — silently. A subscriber could hold a valid, signed, receipted grant for favorites.brands, call the read endpoint, get a clean 200, and receive nothing. Forever. Nobody would be told.
How something like this happens
Not through carelessness, which is the uncomfortable part.
The catalogue lived in the frontend, published as JSON. The field bindings lived in the backend, as a Python dictionary. Both were reasonable. Both were maintained. Neither knew the other existed, and no test compared them — so they were free to drift, and drift is not an event. It is what happens when nothing stops it.
The drift was also invisible by design. The resolver is fail-closed: a scope it does not recognise contributes no fields. That is the correct behaviour and we would not change it. But fail-closed and silent are different things, and we had built the first without noticing we had also built the second.
We found a third copy of the same vocabulary while fixing it. The directory's connect-the-dots flow grants wants.* scopes directly, and eight of the eighteen it offered — chiropractor, physical therapist, eye care, massage, acupuncturist, pediatrician, dermatologist, podiatrist — had never been published in the registry at all. Three sources of truth for one vocabulary, two of them wrong.
What it actually cost
Five of the fourteen agent archetypes we publish — advertising, targeting, shopping, personalization, publishing — request favorites.brands or favorites.things.
All five were decorative. They named scopes the server could not bind. Every one of them would have returned an empty payload to anyone who built against it.
And there was a second gap underneath. The API that writes a person's profile accepted exactly one section and dropped every other. So privacy.* — the input to the Google Consent Mode payload, the entire brand-facing product — had no write path at all. The pipeline was reading data that nothing could produce.
The fix, and why it is shaped this way
The hand-maintained map is gone. A scope now binds to the profile path of the same name — which was already true for every privacy scope and for the whole taxonomy. Only two legacy bindings needed stating explicitly.
That change alone takes the count from 8 to 58 and makes adding a scope a JSON edit rather than a code change and a deploy.
The server now vendors the published catalogue byte for byte, and pins its SHA-256. If the two copies ever differ, the build fails. We considered having the server fetch the file over the network instead and rejected it: the resolver sits on the fail-closed read path, and a network dependency there fails either open, which is unacceptable, or into an outage on a CDN blip.
Changing the registry now requires updating a digest in two repositories in the same change. That friction is deliberate. It is a published standard, and standards should be slightly annoying to alter.
Then the guards. Every published scope must resolve to at least one field — the direct inverse of the bug. Every want the directory offers must exist in the registry. Health and legal intent must stay marked sensitive. The free-handshake bundle must resolve and must stay coarse.
We tripped the guards ourselves within the hour
Two of them, immediately.
The sensitivity guard failed because I had written it as a rule about a whole family of scopes, and the registry deliberately treats childcare and elder-care differently from tutoring and pet care. I had encoded my own opinion as a test. That is a real distinction about vulnerable dependents and it was not mine to overwrite silently, so I narrowed the guard and left the question for a human.
Then the coarseness guard failed on a label that reads *"never a street or ZIP"* — my regex was forbidding the word "zip" anywhere, and the label mentioning ZIP in order to rule it out looked identical to a violation. Labels should be free to name the thing they are disclaiming. It is the scope names that must not offer that precision.
And the largest one: while writing about the free first handshake, we specified three fields — profile.age-band, profile.sex, profile.region — and shipped the economics before publishing the scopes. For a few hours the handshake promised to disclose fields that did not exist in the registry. The exact drift we had just spent a day eliminating, reintroduced by the person eliminating it.
The guards caught all three. That is the argument for guards.
Why write this down
Because the finding generalises, and because the alternative is worse.
A published contract with no test against the implementation is a wish. If you publish an API surface, a schema, a scope registry, a feature matrix — anything that says *here is what we do* — and nothing in your build compares it to what you actually do, the two will separate. Not immediately. Gradually, then completely.
Fail-closed is necessary and not sufficient. Failing safely is not the same as failing loudly. Ours failed safely and told nobody for an unknown length of time.
And we would rather you heard this from us. We publish a page listing what we have not done, we say our certifications are in pursuit rather than held, and this belongs in the same category. A company whose entire product is trust does not get to be selective about which unflattering facts it shares.
Where this actually stands, today
It would be a poor article about overclaiming that overclaimed in its last line, so precisely:
Shipped and live. The registry publishes 58 scopes, including the eight health wants the directory had been offering unpublished. The write path accepts every published root — before this, a privacy preference had nowhere to be stored. The guards that compare the three copies of the vocabulary run in our build.
Written, tested, not yet deployed. The resolver change — the one that takes the server from 8 to 58 — is verified against a real Postgres database and is open for review in the protocol repository. It is not in production as this is published. Until it deploys, the honest count is 58 published and 8 serving.
We are publishing this now rather than after the deploy, because "we will tell you once it looks good" is how the first gap lasted as long as it did. This page will be updated with the date the resolver ships.
The counts are public either way, and there is now a test that fails the day the published number and the built number disagree.
The 🤫 hussh magazine
Written by hussh Engineering, and built to read beautifully here — and to travel to 🤫 One on your phone, your glasses, and visionOS, as one immersive magazine you own.