Skip to content

Verification & evidence

Why this page exists

“Client-side tokenisation” is an architectural claim, and architecture claims are cheap. A page of prose about where code runs proves nothing about what a given build sends. The only artefact that settles it is the request body on the wire.

So the standing rule internally is: a claim is not verified by reading the source, by a unit test, or by an SDK-level harness. It is verified by capturing the real outgoing request from the real deployed bundle and grepping it for the raw values. Everything below follows from that rule.

The architecture the measurement is testing

Three facts, in the order they matter:

  1. Detection and tokenisation run on your device — in browser WASM, the desktop app, the local proxy, or the SDK — before any byte is sent. See How privacy works.
  2. The gateway receives only the tokenised body. It performs no content inspection; the detection engine is deleted from the gateway at the code level rather than disabled behind a flag. See Content-blindness attestation.
  3. The token→value map stays on the device. The gateway never receives it, so it cannot re-identify surrogates it relays.

Points 1–3 are properties of the system’s construction. The measurement below is the check that a particular shipped build actually behaves that way.

Method

What is capturedThe outgoing HTTP request body, intercepted at the browser’s network layer on the live origin (e.g. the same-origin relay POST /api/<product>). Not a preview pane, not a unit-test fixture, not an SDK-only harness.
How it is drivenHeadless Chromium (Playwright) drives the real UI of the deployed site — file pickers, text areas, mode tabs, send buttons — exactly as a person would.
What is assertedEach planted raw value is searched for in the captured body twice: exact match, and again after JSON-decoding and collapsing whitespace (so a value split across an escape or a line-wrap still counts as a leak). Surrogates and [CLK-…] locked tokens must be present.
Which bytesThe engine that produced the body is re-fetched from the live origin during the run and hashed, so every result is pinned to a specific engine build rather than to “whatever was deployed at the time”.
Corpus56 texts — 50 PII-dense and multilingual (16 scripts, including bare/uncued identifier forms), plus 6 edge cases: empty, whitespace-only, very short, short-PII, mixed-script, and a long multi-paragraph document. Plus 12 attachment fixtures (pdf / docx / txt / md / csv / xlsx / png / jpg / scanned-pdf / oversize / executable).

Two honest notes about the method

  • The name model was deliberately switched off. These runs pin the deterministic detection lane: the on-device NER name model was blocked so results are reproducible run-to-run. That means the free-form-name numbers below are a floor, measured with the weaker configuration — not the best case. We would rather publish the floor.
  • Most relays were operationally gated at capture time. At the time of the run, ten of the eleven product relays returned HTTP 503 (a server-side key was not yet provisioned); the chat product relayed 200 and completed a full round-trip. This does not affect what is being measured: the request body is composed and sent by the browser before the server answers, so the privacy-relevant artefact is captured either way. The full round-trip is separately evidenced by the chat product.

Scope and results

Wire-level egress, per product

Eleven live products were measured individually — the writing, CV, social-post, translation, document-conversion, spreadsheet, redaction, chat, document-library, meeting-notes and image-scan apps.

Result: 0 raw structured PII in any captured request body, on all eleven. In each case the structured classes — email, phone, IBAN, payment card, national ID / SSN — were absent from the body, and locale-matched surrogates or locked [CLK-…] tokens were present in their place. The redaction product is a special case: it emits no network request at all for the processing path, so its “egress” is empty by construction.

Illustrative shape of a captured body (synthetic, not a fixture):

sent: "Contact A. Lindqvist at a.lindqvist@example-legal.test, IBAN [CLK-IBAN-…]"
original: "Contact <a different, real-looking name> at <a different address>, IBAN <a real IBAN>"

Identifier classes with a checksum (IBAN, payment card, several national-ID formats) are replaced with a locked token rather than a plausible surrogate, so no downstream reader can mistake the stand-in for a genuine identifier.

Cross-surface consistency

The same adversarial corpus was run across every surface that can cloak — the products, the TypeScript SDK, the Rust local proxy, the MCP server, and the gateway relay path — to check they agree, since a customer may reach the same engine through any of them.

That exercise required two consecutive fully-clean runs before it counted as passed. Here is what actually happened, stated plainly:

  • Run 1 — clean on every surface: 0 valid-PII structured leaks, and 8/8 of the bare (uncued, no label like “ID:” preceding them) national-ID cases whose checksums validate were caught on every surface.
  • Run 2 — not clean on first pass. A fresh batch with different entity values found that one surface, the Rust local proxy, missed a single bare Korean resident-registration number that the WASM and native lanes both caught. The cause was a suppression rule: the value’s leading digit made it look like a payment card, the card check failed, and the failed card match shadowed the (valid) national-ID match over the same digits. Fixed by exempting checksum-validated detectors from that shadow, re-tested, and the full batch re-captured clean.
  • Only then was the gate recorded as met.

We publish the failed pass because the alternative — reporting run 2 as clean because it was clean after the fix — is the kind of accounting that makes the whole exercise worthless.

The defect this method caught

The strongest evidence for a verification process is that it finds things. Ours found a real one, in our own product, before launch.

What happened. The image-scan product reads an uploaded image with on-device OCR, then cloaks the extracted text before sending it. On a test image, the OCR engine split an email address across a line-wrap — a domain of the shape example- + newline + legal.test came back as two fragments rather than one token. The email detector requires a contiguous token, so it did not match. The address was therefore never detected, never given a surrogate, and its readable characters egressed in the clear. Reproducible 3 times out of 3.

This mattered beyond the one class: the receipt for that request would have attested a zero-raw-egress property that was, for that request, false.

How it was found. Not by review and not by inference — by the wire capture. Everything else about that path looked correct; six of the seven structured classes cloaked fine through OCR, because digit-runs survive OCR contiguously. Only the one class that carries internal dots and hyphens fragmented.

The fix, and why the re-test is meaningful. A repair step now collapses whitespace inside an email-shaped run — anchored on the top-level domain and with the @ outside the character class, so two adjacent addresses can never be merged into one — applied before the text reaches the cloaking step, leaving all other bytes byte-for-byte unchanged. Re-verified on the wire: 3 runs, 0 raw structured PII, while separately asserting that the OCR still reads the email. That second assertion is the point. A gate that passes because the input silently disappeared is a vacuous gate; the re-test proves the address was present and was cloaked.

Timeline honesty. Pre-launch, no customers, no user data — the only address ever exposed by this defect was a synthetic one we planted ourselves.

Honest limits

These are the parts we can not claim, stated as plainly as the results above.

Free-form names, organisations and places are recall-bounded

Detecting a person, company or place name in arbitrary text has no checksum and no fixed format to anchor on. It is dictionary- and model-bound, and therefore best-effort, not complete. In the measured runs — with the on-device name model deliberately off, i.e. the deterministic floor — roughly 24 of the 56 corpus texts still contained at least one raw person, organisation or place name in the outgoing body.

We do not claim a recall percentage for this class, because we have not measured one under conditions we would be willing to publish. We do not claim that “no PII ever escapes.” That statement would be false, and every other number on this page would deserve to be doubted because of it.

A bare, uncued phone-shaped number is a deliberate precision residual

A digit run that could be a phone number but carries no cue word, no country prefix and no formatting is not treated as a phone number. This is a chosen trade: treating every such digit run as a phone number produces constant false positives on order numbers, reference codes and quantities. Verified empirically in the same runs: the cued form (a “Phone:” label), the internationally-formatted form, and the conventionally-formatted national form all cloak; only the bare uncued form is left. The same reasoning applies to bare national-ID forms that do not carry a validating checksum.

Non-contiguous and obfuscated inputs

The OCR case above is one instance of a general class: detection operates on the text as the device sees it. Values fragmented by OCR, encoded (e.g. base64), or written with look-alike characters from another script can evade detectors that match on the literal form. Some of these have mitigations, some do not; the current state is documented per-technique on Detection coverage.

What the measurement itself does not cover

  • It covers the paths and surfaces listed above, at the engine build identified in each run. It is not a statement about any other build, or about a surface not in the list.
  • The corpus is 56 texts and 12 attachments. It is adversarial and multilingual, but it is finite. Absence of a leak in a finite corpus is evidence, not proof.
  • Meeting transcription was verified on the shared cloak-and-send leg using a pasted transcript rather than a full in-browser audio transcription, which is too heavy to run headless. The speech step produces the transcript text; the privacy gate lives on the leg that was measured. We flag this rather than imply the heavier path was exercised.

Nothing on this page is a statement about your regulatory obligations, your role or ours under any data-protection regime, or whether any particular deployment satisfies any particular law. Those are determinations for you and your advisers. What we publish are architectural facts and measurements with their method and scope attached — inputs to your assessment, not the assessment.

What a receipt proves — and what it does not

Every relay can carry a signed receipt. Read precisely, it proves a process, not an outcome.

A receipt does prove:

  • That the relayed body is the body your device produced. The hash committed by the device before egress matches what was relayed — so nobody altered it in between, and the surrogate body you can see is the one that was actually sent.
  • Engine identity. Which engine build and ruleset version produced it, checkable against a public allowlist you can rebuild from source (see Reproducibility). Note that engine genuineness is attested server-side against that allowlist, not accepted from the client’s self-report.
  • Tamper-evidence and ordering. The receipt is signed and chain-linked, so alteration, deletion or reordering within a chain is detectable.
  • Which detector categories fired — categories only, never matched values.

A receipt does not prove:

  • That the detector caught every piece of personal data in your text. It attests what the engine did, not that there was nothing left to do. The free-form-name limit above is exactly this gap: a receipt on a message containing an undetected name is a perfectly valid receipt.
  • That the surrogate text is harmless. Text can identify someone through context alone, with no identifier of any kind present. No detector addresses that, and no receipt claims to.
  • Anything about what the AI provider does after the relay. The receipt witnesses the relay, not the recipient.
  • That the tokenisation was correct — only that it happened, with a named engine, over a body whose hash is committed.

The distinction that matters: a receipt is process-proof (this specific pipeline ran, on these bytes, with this engine), not result-proof (all personal data was removed). Read as result-proof, it overclaims. Read as process-proof, it is exactly what a reader needs to check a provider’s story against what actually left the machine. The same distinction, stated for implementers of the open receipt format, is on signedreceipts.org.

Reproducing the shape of this yourself

You do not need our harness, and you should not have to trust our numbers. The measurement is simple enough to rebuild:

  1. Open a product and put text containing values you control into it — use reserved and documentation-example ranges, never real personal data belonging to anyone.
  2. Open your browser’s developer tools, Network tab, and send.
  3. Read the request payload of the outgoing call. Search it for each value you planted. Structured identifiers should be absent, replaced by a surrogate or a [CLK-…] token.
  4. Repeat the search after decoding: JSON-unescape the body and collapse whitespace, then search again. This is what catches a value that was split rather than removed — the exact check that caught our OCR defect.
  5. Check the engine the page loaded against the public allowlist, and rebuild the allowlist entries from source if you want the trust anchor to be yours rather than ours:
    Terminal window
    curl -s https://api.cloakapi.io/api/v1/engine/hashes | jq
  6. Try to break it. Bare uncued numbers, names in scripts we are weak in, values split across lines. If you find something that leaves the browser in the clear and is not one of the documented limits above, please report it — see Disclosure policy.