Key takeaways
- ✓Personal data (PII) is anything that points to a person: a name, an email, a phone, a card or ID number, an address. Redaction replaces each value with a placeholder like [EMAIL] before the message is stored or sent. Logs and traces keep every prompt word for word, so this is where it has to happen.
- ✓Patterns find a value by its shape. On 876 planted values, they found every email, phone, card, social security number, IBAN and IP address, and none of the 146 names or 84 street addresses: a name has no shape.
- ✓A model (gpt-5.4-mini) reads the words. It found every name and address, and 95–96% of cards, social security numbers and IBANs. It left 8 of the 60 messages written to sound like real people unsafe: 4 cards and 4 SSNs, 7 of them right after words like “order number”, “reference number” or “ticket”. Its own instructions say order numbers aren't personal data.
- ✓Count by message, not by value. One missed value and the whole log line holds personal data. Patterns alone left 194 of the 323 messages that had any unsafe (60%), all names and addresses. The model left 12 (4 of 263 fill-in-the-blank, 8 of 60 written). Both together left none.
- ✓A card number checks itself. Double every second digit from the right, add everything up: a card's total ends in zero (the Luhn check). The cold open's “order number” totals 70; change one digit and it's 71. The patterns caught all 8 of the model's misses because they look at the digits, not the words.
- ✓Both together has a price. Patterns go by shape, so all 108 ten-digit invoice numbers were removed as phones and all 64 version numbers as IP addresses: 148 of the 300 fill-in-the-blank messages lost a number someone may need. And the model has to read the data to remove it: about 650 ms and $0.34 per 1,000 messages, sent to a provider.
Our assistant's logs keep every message learners send. Before a message is stored, its personal details should be replaced with placeholders. This chapter compares three ways to find them: patterns, a model, and both together. It counts what each one leaves in the log, and what it removes by mistake.
The source is Lab 15 (Safety), script 01_pii.py and its pii.py, from the lab's recorded run (October 2026); the video adds no new runs. Quill Academy, the course site, is made up, and so is every message: 300 come from fill-in-the-blank templates, and 60 were written by gpt-5.4-mini, the same model being tested, to sound like real people. The personal data was planted, so the position of every value is known. Chapter 44 is the first of five on safety and security; the next four cover prompt-injection detectors, a secret in the prompt, an agent that can send email, and the tools an agent is given.
Personal data, and redaction
Personal data (PII, personally identifiable information, in security) is anything that points to a person: a name, an email address, a phone number, a card or ID number, a street address, an IP address. It ends up in prompts, and so in logs and traces: chapter 40's traces kept every prompt word for word.
Redaction replaces each value with a placeholder, like a black marker on paper:
Hello! [PERSON_1] here. Reach me at [PHONE_1] or [EMAIL_1]. Invoice [PHONE_2] lists the wrong price: $650.43, not what I agreed.
Numbered placeholders (pseudonyms) keep the line readable: "send it to [EMAIL_1], not [EMAIL_2]". The question is what finds the values. (And yes, that invoice number shouldn't have been removed. More on that below.)
The test
The lab's test set: 360 support messages, with 876 planted values of eight kinds: emails, phones, cards, US social security numbers (SSNs), IBANs (bank account numbers), IP addresses, names and street addresses. It also plants look-alikes that aren't personal: 94 sixteen-digit order numbers, 108 ten-digit invoice numbers, 64 four-part version strings, ticket ids and prices.
Every card and SSN is made up but realistic: cards pass the card checksum, SSNs follow the issuing rules.
Tool 1: patterns
A pattern finds a value by its shape. An email has an at sign. A phone number is ten digits, maybe with a country code (43 of the 146 phones started with +). Where the format has a check, the pattern uses it:
- Cards: 13 to 19 digits that pass the Luhn check (below).
- IBANs: the mod-97 check every bank account number passes.
- SSNs: three-two-four digits that follow the issuing rules (never area 000, 666 or 900–999, group 00 or serial 0000).
The patterns found every value with a shape: all emails, phones, cards, SSNs, IBANs and IP addresses. And none of the 146 names or 84 street addresses. A name has no shape.
Tool 2: a model
The second tool sends each message to gpt-5.4-mini and asks for the personal data in it, as JSON. Its instructions name the eight kinds, and add one line:
Order numbers, tracking numbers, product codes, prices, dates and company names are NOT personal data.
Because it reads the words, it found every name and every street address. And most of the numbers: 86 of 90 cards, 82 of 86 SSNs, 90 of 94 IBANs, about 96 in a hundred. Counted that way, the model wins. It depends on how you count.
Count messages, not values
A log line is safe, or it isn't. One missed value and the whole line holds personal data. So the lab also counts messages that still hold personal data after redaction:
| Messages (with any personal data) | Patterns | The model | Both |
|---|---|---|---|
| fill-in-the-blank (263 of 300) | 157 (60%) | 4 (2%) | 0 |
| written to sound like real people (60) | 37 (62%) | 8 (13%) | 0 |
| all 323 | 194 (60%) | 12 (4%) | 0 |
Patterns alone left personal data in about six in ten messages: every one of them a name or an address. The model slipped only 4 times on the fill-in-the-blank sentences. But on the 60 written ones, which read more like real people, it left 8 of 60: 13% (with only 60 messages, anywhere from 7% to 24%).
Pick a message and a tool, and see what's left in the log:
Messages the model wrote to sound like real people (the model's 8 misses)
Fill-in-the-blank messages, with look-alikes
gpt-5.4-mini reads the message and lists the personal data in it.
Unsafe: a card number still in the log.
All 360 messages, counted by message: still holds personal data
323 of the 360 messages had personal data planted in them. Patterns left names and addresses in 157 of 263 fill-in-the-blank messages and 37 of the 60 written ones. The model slipped on 4 fill-in-the-blank messages and 8 of the 60 written ones (13%). Both together left none, but removed a look-alike from 157 of the 300 fill-in-the-blank messages: all 108 invoice numbers (as PHONE), all 64 version numbers (as IP), and 14 words the model took for names.
Why the model missed
All eight of the model's misses are a card number or an SSN. Seven came right after words like "order number", "order/reference number", "reference number" or "ticket":
The order number is 4805-3137-1544-1511, and the account name is Wei Schulz.
The model found the name and the SSN in that message, and left the card. Its recorded answer gives no reason. But its instructions say order numbers aren't personal data, and it seems to have taken the words at face value. Words like these often fooled it, though not always: of 17 cards and SSNs after such words in the written messages, it still found 10.
Where did the odd wording come from? The same model wrote these messages, and it was given only the bare values, not what they were. So it guessed, and called a card an "order number". Real messages may be less strange. But it shows what a tool that reads words can be talked into.
Digits, not words
The patterns caught all eight. They look at the digits, not the words. And for a card, they can be sure, because a card number checks itself:
- From the right, double every second digit. (If that gives 10 or more, add its two digits.)
- Add up all the digits.
- A card's total always ends in zero.
The cold open's card totals 70. Change its last digit and the total is 71: not a card. A book's ISBN uses the same kind of check digit. Whatever the message calls it, a number that passes is very likely a card.
Starting from the second digit from the right, double every second digit (highlighted). If doubling gives 10 or more, add its two digits (14 → 1 + 4 = 5). Then add everything up.
The number the message called an order number. Its total ends in zero: it's a card, whatever the words say.
One caveat on the order numbers: the lab built every one to fail the check, so 0 of 94 being flagged as cards is by construction. A random 16-digit number passes about one time in ten, so in real traffic some order numbers would be removed as cards.
Both together
So the lab combines them: patterns for everything with a shape, the model only for names and addresses. The model's other answers are ignored, so a card it calls an order number can't hide from the patterns.
# 01_pii.py
def combine(pattern_spans, llm_spans):
return pii.merge(pattern_spans + [s for s in llm_spans if s["type"] in ("PERSON", "ADDRESS")])
Together, no message still held personal data: 0 of 323.
The price
Using both has two costs.
Patterns go by shape, and some harmless numbers share it. A ten-digit invoice number looks like a phone number: all 108 were removed. A version number like 2.16.0.18 looks like an IP address: all 64 were removed. 148 of the 300 fill-in-the-blank messages lost a number like this, so the invoice number someone needed to fix a bill is gone from the log. Counting everything removed that wasn't personal, including 14 words the model took for names ("I", "Invoice", "Order", ticket ids like INC-10978), 157 of the 300 lost something (52%).
The model has to read the data to remove it. Every message goes to a model provider first. In the lab's run that took about 650 ms per message (943 ms at the 95th percentile) and cost $0.34 per 1,000 messages. Patterns took a few hundredths of a millisecond and cost nothing.
Also in the lab
These parts of script 01 aren't in the video. From the lab's README and its recorded run:
- Per type, with the right label. The README's table requires the right label: the model scores 96% on cards, 90% on SSNs, 95% on IBANs that way, because it removed some SSNs as PHONE and an IBAN as CARD. Its false positives: 22 version strings as IP addresses, 14 words as names, 6 phones, 4 cards.
- Overlaps. An IBAN contains digit runs that look like a phone number.
pii.merge()keeps the longest span, so the IBAN wins. - Redact or pseudonymize? Pseudonyms keep the log readable without the values. If you keep the mapping from
[EMAIL_1]back to the address, keep it somewhere else, with its own access control, or don't keep it.
What to do
- Run patterns with checksums on every log line, before it's stored: cards (Luhn), IBANs (mod-97), SSNs (issuing rules), emails, phones, IP addresses.
- Use a model only for what has no shape: names and addresses. Ideally one you run yourself, since it has to read the data.
- Score by message, not just by value: the share of log lines that still hold personal data.
- Count what you remove by mistake, and decide which look-alikes (invoice numbers, versions) your team needs to keep.
- Test on messages that read like real people, not only on fill-in-the-blank sentences: that's where the model slipped.
Caveats
- Made-up messages. All 360 are synthetic, and 60 were written by the model being tested; it chose the odd wording around its own misses. Real traffic will look different. Measure on a sample of yours.
- Small numbers. 8 of 60 is 13%, with a range of 7–24%. The direction (patterns miss names, the model misses some numbers, both together miss neither) is the robust part.
- Two ways to count a model's answer. This chapter, like the video, counts a value as removed if any placeholder covers it, whatever the label. The lab's README requires the right label, and so reports 20% of the written messages unsafe for the model alone (the 8 here, plus messages where an SSN was removed as PHONE or an IBAN as CARD). By coverage, it's 8 of 60.
- Order numbers were built to fail the card check. In real data, about 1 in 10 random 16-digit numbers passes it.
- Pattern speed. This run measured a median of 0.02 ms per message; the README says 0.06 ms. Either way, far below the model's 650 ms.
- One model, one prompt. A different model or instructions would miss different values.
The checklist
- List the personal data your logs and traces can hold, including what users type in free text.
- Redact before storing or sending: patterns with checksums for every value with a shape.
- Add a model for names and addresses if you need them removed, and decide where it runs.
- Ignore the model's answers for anything with a shape: the patterns decide those.
- Keep a test set with planted values and look-alikes, including natural-sounding messages.
- Report messages still holding personal data, and messages that lost something they needed.
- Decide whether to keep a pseudonym mapping, where, and who can read it.
Practice: patterns, checksums and counting by message
Write the parts of the lab's pii.py that decide what gets removed: the Luhn check, the SSN rules, merging overlaps, combining patterns with the model's answer, redaction with pseudonyms, and counting what's left by message. The patterns and detect() are given. The tests use the 8 written messages the model left unsafe and four fill-in-the-blank messages with look-alikes, with the model's recorded answers, and expect the lab's results: the model leaves all 8 unsafe, patterns leave the names and addresses, and both together leave nothing.
Quiz
1.Patterns found every email, phone, card and IP address in the test. What did they miss?
2.The model found 95–96% of card, ID and bank numbers. Why did the chapter count messages instead?
3.Seven of the model's eight misses came right after which kind of words?
4.Why did the patterns catch the card the message called an order number?
5.Patterns and the model together left no message with personal data. What did that cost?
6.The model removed a social security number but labeled it PHONE. Is it a leak?
Check your understanding
Try each question before opening its answer.
What's the difference between finding values and making a log line safe?
A log line is safe only if every personal value in it is removed. One miss leaves the whole line unsafe. So a tool that finds 95–96% of numbers can still leave 13% of messages unsafe, as the model did on the 60 written ones (8 of 60).
Why can't patterns find names and addresses?
A pattern matches a value by its shape: an at sign, a count of digits, a checksum. Names and street addresses have no fixed shape, so the patterns found 0 of 146 names and 0 of 84 addresses.
A message says "The order number is 4805-3137-1544-1511." How do the patterns know it's a card?
They ignore the words and check the digits. Double every second digit from the right, add up all the digits: the total is 70, which ends in zero, so it passes the Luhn check. Change one digit and it's 71.
Why did the lab use the model only for names and addresses when combining?
Patterns are exact for everything with a shape, and the model can be misled by the words around a number. Taking only its names and addresses means a card it calls an order number is still caught by the patterns. Together, 0 of 323 messages kept personal data.
What do you lose by running patterns on every log line?
Look-alikes with the same shape: every 10-digit invoice number was removed as a phone and every version number as an IP address. 148 of 300 fill-in-the-blank messages lost a number like that. Measure it, and decide which ones your team needs.
What's the privacy cost of using a model to remove personal data?
The model has to read the data to find it, so every message goes to the model provider first (plus about 650 ms and $0.34 per 1,000 messages in the lab). Use it only where patterns can't help, and prefer a model you run yourself.
Next up: Chapter 45 tests tools that should catch prompt-injection attacks on the assistant: four detectors on public attacks, and on harmless questions that use the attackers' words. The one that scored best on the public test blocked the most harmless questions.