Search by

Masks personal data in Dutch text on its way into an AI system, and puts it back on the way out. Offline, no dependency.

v0.1.0 2026-10-08 08:23 UTC

This package is not auto-updated.

Last update: 2026-10-23 07:02:51 UTC


README

Masks the people in Dutch text before an AI reads it, and puts them back in the answer. A PHP library and a command. Offline, no dependencies, EUPL-1.2.

Hey Karel, mijn boot kost 250 euro, te bezichtigen aan de Maanstraat 123 in Haasterdam.
ik ben te bereiken op 0612345678. Mvg, Bob

                                    ↓  mask

Hey voornaam1mask, mijn boot kost 250 euro, te bezichtigen aan de adres1mask in stad1mask.
ik ben te bereiken op telefoon1mask. Mvg, voornaam2mask

                                    ↓  the AI rewrites it, and never saw a person

Beste voornaam1mask, de boot op adres1mask in stad1mask is te bezichtigen. Bel telefoon1mask.

                                    ↓  unmask

Beste Karel, de boot op Maanstraat 123 in Haasterdam is te bezichtigen. Bel 0612345678.

Installation

composer require andronewille/sluis

PHP 8.5 or newer with mbstring and sodium. Nothing else comes with it.

Usage

Try it from the command line first:

echo "Hey Karel, bel 0612345678. Mvg, Bob" | vendor/bin/sluis --json
{"text":"Hey voornaam1mask, bel telefoon1mask. Mvg, voornaam2mask", "found":{"voornaam":2,"telefoon":1}, "vault":{…}}

In PHP:

use Sluis\Sluis;

$sluis = Sluis::nederlands();

$masked = $sluis->mask($mail);                    // $masked->text has no people in it
$answer = $yourAi->rewrite($masked->text);
$back = $sluis->unmask($answer, $masked->vault);

$back->text;        // the answer, with the people put back
$back->stray;       // masks that could not be put back — never ignore these
$masked->found;     // ['voornaam' => 2, 'telefoon' => 1], safe to log

As a pipe:

vendor/bin/sluis < mail.txt > masked.txt          # what was taken out goes to sluis-vault.json
cat masked.txt | your-ai | vendor/bin/sluis unmask

The vault is the one thing that holds what was taken out. You keep it; Sluis stores nothing.

What it finds

How
e-mail address, url, postcode by their format
telephone number ten digits, in every grouping people write them
iban, bsn by format, then the check digit
kvk number eight digits behind the word that announces them
street and house number Maanstraat 123, Waterlooplein 12a
names the greeting and the signature of a mail, and after heer or mevrouw
towns after a cue such as in, and from a word list

Amounts are left alone on purpose. Money is not personal data, and an answer without it is useless. To leave more alone, say so: $sluis->without(PiiType::Url), or $sluis->only(PiiType::Iban, PiiType::Bsn).

What it promises

  • Offline. Nothing opens a connection, and a test fails the build if anything ever does.
  • It checks its own work. Every run, Sluis refuses to return text in which something it masked is still readable.
  • Nothing is logged. No error message, log line or dump ever holds the words it was about.
  • Putting back only substitutes. It is safe to run over whatever the AI sent back.

What it misses

Rules read formats and the frame of a letter. They do not read prose, so on their own they miss:

  • a name in the middle of a sentence: Bel Sanne de Vries keeps de Vries
  • a town with no cue in front of it: Haasterdam is mooi
  • company names, licence plates and dates of birth

When Sluis is wrong it is built to mask too much rather than too little. For prose there is an optional local model, andronewille/sluis-onnx: 178 MB of weights on your own disk, still offline, and it finds the names, towns and companies the rules leave standing.

More