A grep for fetch() is not proof your app is offline
Every tool in this category makes the same claim. Runs locally. Nothing leaves your machine. Fully offline. The words cost nothing to write, which is why everyone writes them, and why none of them should move you.
We make the same claim about Velum, so this post is about what would have to be true for you to believe it, including the parts where our own evidence is not good enough.
Start with the honest version of the claim, because it is not "zero network calls."
Velum makes five network calls
None of them carry document text. All five are worth naming, because a claim of zero that turns out to be five is worse than a claim of five. Two of them carry an identifier of some kind, and those two are exactly the ones a post like this would be tempted to leave out.
1. An update check on launch. The Tauri updater fetches a manifest from
downloads.velumprivacy.com. It sends no document content. It does tell our CDN
that some IP launched the app.
2. A one-time detection model download, which you start deliberately and can decline. The regex tier works without it. This is an inbound transfer: the model comes to you, nothing about you goes out beyond the request itself.
3. A signed license revocation list, also from downloads.velumprivacy.com.
This one deserves its own section, further down, because "how do you revoke a
license without phoning home" turned out to be the most interesting constraint in
the whole product.
4. One activation report, sent at the moment you enter a license key. It carries the license id and a one-way hash of this install's random id. No email, no key, no hardware identifier. It goes once and is never repeated, including on later launches.
5. One trial registration, sent once on first run. It carries a one-way salted hash of this machine's platform UUID and nothing else, not the UUID itself. It is what stops the trial being restarted by deleting a local file, which makes it the one request in this list derived from your hardware rather than from a random per-install value. It stops entirely once a purchased key is in place.
Four and five are the ones that would embarrass me to omit, so they are named here, on the /desktop page, and in the protection receipt the app exports. If you find the app making a request I have not listed, that is the bug report I most want to receive.
What has no network call on it is the masking path. Text goes in, entities are detected, tokens go out, the mapping is written to local disk. That is the claim, and everything below is about whether you should believe it.
What we do internally, and why it is not proof
We run two guards in CI. I am going to describe them and then take them apart, because I would rather do that here than in a comment section.
The static scan
scan-runtime.mjs walks every authored source file under packages/*/src and
fails the build on a set of forbidden patterns:
const FORBIDDEN = [
/\bnew\s+XMLHttpRequest\b/,
/\bnew\s+WebSocket\b/,
/\bnavigator\.sendBeacon\b/,
/allowRemoteModels\s*[:=]\s*true/,
/allowLocalModels\s*[:=]\s*false/,
/\bfetch\s*\(/,
];
The last two entries are the transformers.js and onnxruntime knobs that would let the model layer reach for a remote weight file. The narrow allowlist is build-time scripts and benchmarks, which never get bundled.
Now the problems, in ascending order of seriousness.
It does not scan node_modules. The walk skips it explicitly. So it says nothing
at all about what a transitive dependency does. It is checking our discipline,
not our supply chain.
It is a regex over source text, so it is defeated by anything that does not spell
the call literally. globalThis["fet" + "ch"], a dynamic import(), the
Function constructor, a property lookup off a table. None of that is exotic;
it is what any obfuscated exfiltration would look like anyway.
And most importantly, it only reads JavaScript and TypeScript. Velum's
desktop app is a Tauri shell, which means there is Rust in the process. There is
a Node sidecar. There is a native ONNX runtime and a WASM build of the same. A
reqwest call in the Rust half would not trip a single one of those patterns,
because the scanner never looks at a .rs file.
The static scan is a lint rule that stops us from regressing. Calling it proof would be a category error.
The runtime test
no_network.test.ts replaces fetch and XMLHttpRequest with functions that
log and throw, then runs a full anonymize pass over a string with an email, an
IBAN and a national ID in it, and asserts nothing was called.
This is better, because it observes behaviour rather than spelling. It is still not proof, for reasons our own comment in the file admits:
Node-only network APIs (
node:http,node:https) are ESM bindings and cannot be monkey-patched in place; the static scanner catches those.
Read that together with the previous section and you can see the shape of the
hole. The runtime test cannot see node:http, and the static scanner that is
supposed to cover for it cannot see Rust. Neither one covers the sockets a native
module opens on its own.
It also runs in a test process against source modules, not against the signed binary you downloaded. Even if it were airtight, it would be airtight about something other than the artifact in your Applications folder.
So: two mechanisms that keep us honest with ourselves, and neither of which you have any reason to accept as evidence.
What you can actually check, from outside
The useful checks are the ones that do not require you to read or trust any of our code, because they observe the process rather than the repository. All of these run against the binary you already have.
Watch the sockets.
lsof -i -P | grep -i velum
While masking, you should see the loopback listener and nothing else. The
embedded service binds 127.0.0.1 only. If you see a socket to anything that is
not localhost during a mask operation, the claim in this post is false and I want
to hear about it.
Take the network away. Pull the cable, or turn off wifi, and mask a document. The whole point of the design is that this changes nothing. The desktop app has a self-test button that runs a masking round trip against a built-in sample so you can watch it work with the interface down.
Watch the browser extension directly. Load the popup, right click, Inspect, open the Network tab, then set Chrome's throttling to Offline. Paste something with personal data in it and mask. Zero requests, or the claim is false. Paste a token-laced reply back and unmask. Still zero.
Put it in a box. The strongest version, and the one I would run if I were you: launch the whole app inside a network namespace with no route out, or behind a default-deny firewall, and use it normally for a day. Little Snitch on macOS does this interactively and will show you every connection attempt, including ones we did not tell you about. That last part is the point. It is the only check in this list whose result does not depend on our good faith.
If you run that last one you will see the five calls named at the top of this post, at the times described. Most sessions will show fewer, because two of the five happen once in the life of an install. If you see a sixth, I have a problem.
The revocation list, or: how do you revoke a license without phoning home
This is the constraint I found most interesting to build against.
Velum's licenses are offline. A key is an Ed25519-signed payload with an expiry, verified locally against a public key embedded in the app. A license server hears from you exactly once, in the activation report at the moment you paste the key in, and never again: not on launch, not on renewal, not while you work. That is the whole reason the offline claim holds for paying customers and not just trial users.
That design has an obvious hole: a key that leaks cannot be taken back. The normal fix is an online activation check, which would mean the app phoning home with a license identifier every launch, which would destroy the thing we are selling.
What we do instead:
- The revocation list is a static file on the same host the updater already contacts, so it adds no new destination.
- It is requested with no identifier. No license id, no install id, no query string. The client downloads the entire list and matches locally. The server cannot learn which license is checking, only that somebody downloaded a file.
- It fails open. No network, no cache, malformed file, bad signature: the app behaves exactly as it would have anyway. A revocation mechanism that could lock out an offline paying customer during a flight would cost more than the leak it prevents.
- The list is signed with a control key that is deliberately not the license signing key, and which exists only offline. Someone who steals the issuance key can mint licenses but cannot forge a list that un-revokes them.
- Each list carries a monotonic sequence number, and a fetched list with a lower number than the cached one is discarded, so a network attacker cannot serve an old list to resurrect a revoked key.
The tradeoff is real and I will state it: revocation is best-effort. Someone who blocks that one URL never receives the list. We decided that a revocation you can evade is better than an activation check you cannot, given what we are claiming about the rest of the product.
Where this runs out
Three things I cannot prove to you in a blog post.
Native code. The ONNX runtime is a compiled dependency. I can tell you it loads weights from a local path with remote fetching disabled, and there is a test asserting that a missing model directory produces a filesystem error rather than a network call. I cannot personally audit every line of the runtime, and neither can you, and the socket-level checks above are the only real answer to that.
The build. The binary you downloaded was produced by our CI from our source. Reproducible builds would close that gap. We do not have them.
Intent. Every check in this post is a check on one version at one moment. A
vendor who wanted to defect could ship a clean build, earn the benchmark, and
change it in a point release. This is not solvable by promises, including mine.
It is solvable by you running the firewall check occasionally, which is the actual
reason I would rather hand you lsof than a trust badge.
The honest summary is that our internal guards are a regression suite, and the only evidence that should move you is the evidence you produce yourself on the machine in front of you. That is a slightly awkward thing for a vendor to write down, but it is the argument, and a privacy tool that asks you to take its word for it has already lost the plot.
Velum is a local PII masker for text and files. The detection engine is licensed under PolyForm Noncommercial 1.0.0, which is source-available rather than open source, and the packages are not published anywhere yet, so the source cannot currently be read. The desktop app and browser extension are proprietary and paid. It runs on macOS on Apple Silicon. The extension is distributed as a zip from this site while a Chrome Web Store listing is pending. Windows and Linux are not shipped, and neither has a date.