A customer asks for a payment update. Their message includes an IBAN and an email address. Bowden-PII lets an application replace those values with [IBAN_1] and [EMAIL_1] before sending the request to an AI service for summarisation.

That is the starting point for this project. Run detection locally, keep the original values there, and send the text with placeholders. The first public release, 0.0.1, gives developers a Python API and a command-line tool to try this with their own text.

Why name it after Caspar Bowden?

Caspar Bowden studied who could access data held by cloud services, including through government surveillance. He co-founded the Foundation for Information Policy Research and was Microsoft's chief privacy adviser for Europe, the Middle East and Africa. EDRi's remembrance describes his work and independence.

His work is a reason to ask what an application should send to a cloud service in the first place. Bowden-PII addresses one part of that question by removing supported identifiers before transmission. The name is an independent tribute, without affiliation or endorsement from his estate.

What happens to the text

The rules engine checks Swiss AHV/AVS, UID/CHE and VAT identifiers, IBAN and QR-IBAN, and payment-card numbers. It validates checksums where the format provides one. It also detects email addresses, conservative Swiss phone patterns, URLs, IP addresses and MAC addresses.

from bowden_pii import redact

result = redact("Contact mia@example.ch", policy="strict")
print(result.redacted)
# Contact [EMAIL_1]

If the same normalized value occurs twice, it gets the same placeholder within that result. A separate map holds the originals. Keep that map local. The CLI's full --json output also includes it, so forward result.redacted when you want the masked text. The audit summary omits the original values.

The policy determines which identifiers the engine replaces. strict includes every supported rule class. balanced leaves URLs unchanged. permissive also leaves IP addresses, MAC addresses and standalone UID values unchanged.

An early model flagged harmless text

Names, addresses and OCR errors need more than format rules. The experimental MiniLM classifier predicts which parts of a text contain identifiers. The hybrid engine combines those predictions with rule detections and gives validated rules priority when they overlap.

One early model flagged identifiers in 38 of 40 test texts labelled as containing no identifiers. Adding negative examples to training reduced that to zero in the next run on the same held-out set. These were small synthetic sets, but they exposed a problem we needed to fix before training on more data.

The larger minilm-large-v1 run completed three epochs on 12,000 synthetic training rows. The data includes German, French, Italian and English examples. Evaluation used separate generated splits and a curated stress set of 120 rows.

The larger run passed the project's recorded acceptance checks. There is still a gap in that evidence. The hybrid report compares counts of identifier labels, so a correct count can hide an incorrect text boundary. We need broader tests on independently labelled real documents before claiming reliable detection across languages and document types.

What the 0.0.1 download includes

The rules runtime, 36 focused tests, a small synthetic benchmark and experimental hybrid interfaces. Trained weights and training datasets are not included. The MiniLM adapter currently stops at 256 tokens by default and does not split long inputs into chunks.

Try the rules engine today

Download the source or wheel and install it with Python 3.11 or newer. The default engine needs no model, runtime dependencies or network connection. You can pass your own text to the API or command line. The website only shows precomputed examples.

The next work is better evaluation on public documents and OCR text, wider identifier coverage, and long-text processing. Distributing trained weights will also need a documented evaluation of the model we ship.

If an identifier slips through, or harmless text gets masked, open a GitHub issue with a synthetic example and the output you expected. Keep personal and confidential data out of the report.