Your own names and terms¶
Out of the box, Privyx finds values that have a recognizable shape: an email address, a phone number, an API key. A person's name, a customer's company, or your project's codename looks like any other word, so Privyx masks it only after you tell it to. This tutorial builds that configuration step by step and tests it on a sample before any request depends on it.
You need: Privyx installed. Time: fifteen minutes.
1. Start with a sample¶
Collect a few lines of the kind of text you send to a model, and save them as
sample.txt. This tutorial uses a support ticket:
Ticket from Bob Marsh (bob.marsh@initech.example), Initech:
"Ann promised the Bluebird beta by Friday. Our order ORD-2026-000123
still shows the old address. Call me on +1 415 555 0142."
Handled by EMP-004217.
Without configuration, Privyx finds the email address and the phone number, and nothing else:
$ privyx detect --transform --stdin < sample.txt
23:48 EMAIL 'bob.marsh@initech.example'
169:184 PHONE '+1 415 555 0142'
Ticket from Bob Marsh (<PRIVYX_EMAIL_1>), Initech:
"Ann promised the Bluebird beta by Friday. Our order ORD-2026-000123
still shows the old address. Call me on <PRIVYX_PHONE_2>."
Handled by EMP-004217.
The names, the company, the codename, and the two IDs are still there.
2. List the words you know¶
terms maps an entity name to plain words. Privyx escapes them, so no regex
knowledge is needed:
# my.yaml
detector:
type: regex # keeps the built-in patterns
terms:
PERSON: [ann, bob]
ORGANIZATION: [acme, initech]
PROJECT: [bluebird]
$ privyx detect -c my.yaml --transform "Bob at Initech wants Bluebird specs before Acme does"
0:3 PERSON 'Bob'
7:14 ORGANIZATION 'Initech'
21:29 PROJECT 'Bluebird'
43:47 ORGANIZATION 'Acme'
<PRIVYX_PERSON_1> at <PRIVYX_ORGANIZATION_2> wants <PRIVYX_PROJECT_3> specs before <PRIVYX_ORGANIZATION_4> does

A few rules:
- The entity name is yours to choose. It becomes part of the token, so the
model still knows that
<PRIVYX_PROJECT_3>is a project. - Matching ignores case and only hits whole words:
anndoes not match insideannual. - A term can hold spaces or punctuation:
bob marsh,git.internal.example. Longer terms are tried first.
3. Add patterns for IDs¶
Values that follow a format are better described by a regular expression.
patterns maps an entity name to one:
# my.yaml
detector:
type: regex
terms:
PERSON: [ann, bob]
ORGANIZATION: [acme, initech]
PROJECT: [bluebird]
patterns:
EMPLOYEE_ID: '\bEMP-\d{6}\b'
ORDER_ID: '\bORD-\d{4}-\d{6}\b'
LICENSE_KEY: '(?i)license[ _-]?key\s*[:=]\s*(?P<value>[A-Z0-9-]{10,})'
$ privyx detect -c my.yaml --transform "Ann (EMP-004217) refunded ORD-2026-000123. License-Key: QX7T-22LM-90PZ"
0:3 PERSON 'Ann'
5:15 EMPLOYEE_ID 'EMP-004217'
26:41 ORDER_ID 'ORD-2026-000123'
56:70 LICENSE_KEY 'QX7T-22LM-90PZ'
<PRIVYX_PERSON_1> (<PRIVYX_EMPLOYEE_ID_2>) refunded <PRIVYX_ORDER_ID_3>. License-Key: <PRIVYX_LICENSE_KEY_4>
- Write patterns in single quotes, so YAML leaves the backslashes alone.
- Patterns are case-sensitive; start one with
(?i)to ignore case. - A group named
valuemasks only that part. The last pattern hides the key and leavesLicense-Key:for the model to read. - A pattern named like a built-in entity, such as
PHONE, replaces the built-in one.
A misspelled key or an invalid regex stops Privyx at startup, with the setting named in the message, rather than silently not applying.
4. Test it on the sample¶
$ privyx detect -c my.yaml --transform --stdin < sample.txt
12:15 PERSON 'Bob'
23:48 EMAIL 'bob.marsh@initech.example'
51:58 ORGANIZATION 'Initech'
61:64 PERSON 'Ann'
78:86 PROJECT 'Bluebird'
113:128 ORDER_ID 'ORD-2026-000123'
169:184 PHONE '+1 415 555 0142'
198:208 EMPLOYEE_ID 'EMP-004217'
Ticket from <PRIVYX_PERSON_1> Marsh (<PRIVYX_EMAIL_2>), <PRIVYX_ORGANIZATION_3>:
"<PRIVYX_PERSON_4> promised the <PRIVYX_PROJECT_5> beta by Friday. Our order <PRIVYX_ORDER_ID_6>
still shows the old address. Call me on <PRIVYX_PHONE_7>."
Handled by <PRIVYX_EMPLOYEE_ID_8>.
Much better, and it shows the limit of a list: Marsh is still there, because
only bob was listed. You can add bob marsh, and you will keep adding names
for as long as new people appear in your text.
5. Find names you cannot list¶
For names nobody listed in advance, add a detector that reads language. A
detector list runs several detectors and pools what they find.
Presidio recognizes names and places with a language model that runs locally:
pip install 'privyx[presidio]'
python -m spacy download en_core_web_sm
# my.yaml
detector:
- type: regex # built-in patterns, your terms and patterns
terms:
ORGANIZATION: [acme, initech]
PROJECT: [bluebird]
patterns:
EMPLOYEE_ID: '\bEMP-\d{6}\b'
ORDER_ID: '\bORD-\d{4}-\d{6}\b'
- type: presidio # needs privyx[presidio] and a spaCy model
language: en
entities: [PERSON, LOCATION]
$ privyx detect -c my.yaml --transform --stdin < sample.txt
12:21 PERSON 'Bob Marsh'
23:48 EMAIL 'bob.marsh@initech.example'
51:58 ORGANIZATION 'Initech'
61:64 PERSON 'Ann'
78:86 PROJECT 'Bluebird'
113:128 ORDER_ID 'ORD-2026-000123'
169:184 PHONE '+1 415 555 0142'
198:208 EMPLOYEE_ID 'EMP-004217'
Ticket from <PRIVYX_PERSON_1> (<PRIVYX_EMAIL_2>), <PRIVYX_ORGANIZATION_3>:
"<PRIVYX_PERSON_4> promised the <PRIVYX_PROJECT_5> beta by Friday. Our order <PRIVYX_ORDER_ID_6>
still shows the old address. Call me on <PRIVYX_PHONE_7>."
Handled by <PRIVYX_EMPLOYEE_ID_8>.
Bob Marsh is now one PERSON, and neither name was listed.
entities limits what Presidio reports. Left empty, it uses every
recognizer, which here would also mask Friday as a DATE_TIME. A small
model also mislabels now and then, a company as a LOCATION for
instance. The value is masked either way; keep your word list for the
names that must never slip through.
The llm detector asks a language model to list the sensitive values in
each text. It understands context better than any pattern, and it costs a
model call per request:
pip install 'privyx[providers]'
# my.yaml
detector:
- type: regex
terms:
ORGANIZATION: [acme, initech]
PROJECT: [bluebird]
- type: llm # needs privyx[providers] and a provider key
llm_provider: openai
llm_model: gpt-4o-mini
Three things to know before you choose it:
- The detector's model sees the text unmasked. That is how it finds
what to mask. Use a provider you already trust with this data, or a
model you host: the SDK reads
OPENAI_BASE_URLfrom the proxy's environment, so the detector can call a local, OpenAI-compatible server while the chat itself goes to another provider. Do not point it at Privyx itself. -
It fails closed. If the scan fails or times out, the request is not forwarded, and the client gets a
503:{"error": {"type": "privyx_scan_failed", "message": "privyx could not scan this request for sensitive data, so it was not forwarded; see the privyx logs"}}llm_fallback_on_error: truescans with the built-in patterns instead and lets the request through, without whatever only the model would have caught. -
It is counted. The audit trail records the calls and an estimate of the tokens they used:
detector_counts=llm_calls:1,llm_input_tokens:87,llm_output_tokens:7.
The key comes from llm_api_key or from the SDK's own variable
(OPENAI_API_KEY, ANTHROPIC_API_KEY). Set llm_model to a model your
account can use.
Whichever you pick, run privyx detect on your sample again. It uses the
same detectors as the proxy.
6. Put it to work¶
Give the same file to whatever you run:
privyx proxy -c my.yaml --upstream https://api.openai.com
privyx run -c my.yaml claude
privyx mask -c my.yaml --map map.json -i ticket.txt
or set PRIVYX_CONFIG=/path/to/my.yaml once. While you are still tuning the
lists, --reload restarts the proxy whenever the file changes:
privyx proxy -c my.yaml --reload --upstream https://api.openai.com
Mask only some entity types¶
By default everything a detector finds is masked. The strict policy masks
only the types you allow, and leaves the rest in the text:
# strict.yaml
detector:
type: regex
terms:
PROJECT: [bluebird]
policy:
type: strict
allowed: [EMAIL, PHONE]
With strict, an entity of your own that is missing from allowed is
detected and then left alone, as PROJECT is here. --no-policy shows what
the policy filtered out:
$ privyx detect -c strict.yaml --transform "bluebird mail a@b.io"
14:20 EMAIL 'a@b.io'
bluebird mail <PRIVYX_EMAIL_1>
$ privyx detect -c strict.yaml --transform --no-policy "bluebird mail a@b.io"
0:8 PROJECT 'bluebird'
14:20 EMAIL 'a@b.io'
<PRIVYX_PROJECT_1> mail <PRIVYX_EMAIL_2>
What to keep in mind¶
- A word list is exact and fast, and it only knows what you wrote down. Review it when people and projects change.
- Common words make poor terms: listing
mayas a name masks every "may". - Detection results are cached by text, so a long conversation is not rescanned on every turn.
- Entity names must start with a letter and hold only letters, digits, and underscores.
Next steps¶
- Detection: every detector option, the built-in patterns, and the cache.
- Masking: what a detected value becomes, from tokens to realistic fakes.
- A detector plugin: when a format needs code, such as a checksum.