Writing the privacy page found the sentence our pitch could not support
Private repo scans never leave the machine is only true with the LLM judge switched off. Auditing every outbound request in the source, rather than writing the page from the pitch, is what surfaced it.
A product whose pitch is we never see your code owes a page that survives being read closely by somebody who does not believe it.
So the page was written from an audit of the source rather than from the pitch. That is the only reason it ended up containing the thing a privacy page usually leaves out.
The sentence#
depwall scan over a local path sends the content
of your files to the judge.
That is real. "Private repo scans never leave the machine" is true only with the LLM judge switched off, and the honest thing was to write it in the table rather than in a footnote.
Two things make it a different bargain than that sentence sounds, and the page says both immediately after it:
- It goes to Anthropic under your own API key and your own account, directly. There is no DepWall proxy in the path and we never see it. The agreement that governs that content is the one you already have with your model provider, not ours.
- No key, no judge, no content leaves. Without
ANTHROPIC_API_KEYorANTHROPIC_AUTH_TOKEN, DepWall runs deterministic signals only and says so on stderr rather than failing quietly. The gray zone stays at ASK — noisier, never more permissive.
For a package, the judge's input is public anyway: name, version, description, README, manifest fields, install-script bodies. All of it was about to be downloaded from a public registry and executed on your machine. For a local path, the input is your files. Those are different enough to deserve different sentences.
Every outbound request, by name#
The useful form of this claim is not a paragraph, it is a list with nothing missing from it.
| Goes to | Carries | When |
|---|---|---|
api.depwall.com | Ecosystem, package name, version. No account, no key, no repo, no path. | Only if DEPWALL_CLOUD_URL is set. |
| Public registries | The package name, in a normal metadata request. | Every check — your package manager is about to ask the same registry the same thing. |
api.github.com | A repo URL you asked to scan, or a source repo named in a package's metadata. | depwall scan <url>, and README fetches for Go and Rust. |
api.anthropic.com | Content. Public for a package; your files for a local scan. | Only with a key set, and only for a package the signals could not resolve. |
| Your policy host | Your organisation id and its token. | Only with all four policy variables set. |
The counters, and the field we left out#
The other half of this work was verdict counters — the dashboard needs numbers. One datapoint per served verdict: ecosystem, tier, worst signal, cache hit.
Absent by construction: the caller's IP, every request header, and the package name.
The name is the deliberate omission and it deserves the argument, because the easy counter-argument is correct as far as it goes. The name is already in the request URL. We accept that per request; it is the one thing the cloud cache is for. Recording it in a metrics dataset leaks nothing additional about any single request.
The difference is durability. A cache entry expires and is a keyed lookup. A metrics dataset is a queryable history. "Someone asked about package X" is a claim this project accepts one request at a time; it is not a claim it wants accumulated into a record of what a population installs.
No counter we actually need requires the name. And a top-blocked-packages panel is a decision that should be made deliberately, with the privacy page open — not a field that arrives in the dataset because it happened to be in scope on the day someone wrote the schema. Defaults become permanent.
Two smaller calls that change what the numbers mean#
A 500 is not a verdict
Not counted: /health, robots.txt, unknown routes, and
500s. A 500 is the absence of a verdict rather than a verdict, and
counting it would inflate whichever tier happens to be the default — turning an outage
into an apparent shift in what the gate is seeing.
A quota must cost a counter, never an install
The write is fire-and-forget and wrapped. Analytics Engine has quotas, and the day one
is hit must be a day the dashboard is short some numbers, not a day installs fail. That is
tested by making writeDataPoint throw — the failure path exists because it was
exercised, not because a try block was typed around it.
Why publish the awkward version#
Because the alternative fails the first time a security reviewer reads the code, and it fails in the worst possible way: as a discovered omission rather than a disclosed one. A privacy claim's value is entirely in whether it holds up when someone hostile checks it. One that has to be quietly qualified later was never worth anything.