JOURNAL

Claude Code mods: a CPR filter that tripped over a Danish letter

Claude Code just got mods. A filter with 37 lines of code swaps CPR numbers, emails and phone numbers out before Claude reads them. The first version let an email with a Danish letter through and made Claude believe the file was masked. The fixed one let none of 26 through on a mock customer export. The filter is free.

2 October 2026·11 min read·Claude · Claude Code · GDPR · Personal data · claude news

TL;DR: Claude Code just got mods, small pieces of code that can change what Claude Code does. I had Claude Code build a filter that swaps CPR numbers (the Danish personal ID number), emails and phone numbers for placeholders before Claude reads them, and test it on a mock customer export with 26 of them planted. The first version let one email through, because its domain had a Danish letter in it, and made Claude believe the file was masked. The fixed version let none through. Claude still read every name and every street address. The filter is free: download the zip.

The test in 37 seconds: the same file and the same request with no filter, the first try and the fixed version. Reconstructed from the recorded runs; Claude's lines are its own, translated from Danish.Film: Brinvik
Switch between the three modes, or paste in a line of your own. Nothing leaves the page, and every customer is invented.

It might be a developer or someone in operations who sets Claude Code to clean up an export from the CRM. In Denmark, that export can have CPR numbers in it.

Claude Code mods arrived on 1 October. A mod is a small function that Claude Code calls whenever something specific happens, such as Claude reading a file. The function can run before, after or instead of what Claude Code would otherwise do. Anthropic names this as one of its own examples in the launch post: "Redact secrets from tool output before Claude reads it." Anthropic describes mods in Claude Code: in the terminal, the desktop app and VS Code. The regular Claude chat and Cowork aren't mentioned.

A silent filter made Claude get it wrong

The filter is a mod with 37 lines of code. Every time a tool runs, for example when Claude reads a file, the result passes through the filter before Claude gets it. It swaps CPR numbers for [CPR], emails for [EMAIL] and Danish phone numbers for [TELEFON]. It doesn't touch the file on disk.

A drawing of the path one tool result takes, in four steps from left to right. First kunder.csv on disk, marked untouched. Then the Read tool, which reads the file. Then cpr-filter, which swaps CPR numbers, emails and phone numbers for [CPR], [EMAIL] and [TELEFON] and adds one line of notice. Last, Claude, which gets the placeholders and the notice. Below them is the filter's line, with 10 CPR numbers, 8 emails and 9 phone numbers. The 10 are the 9 planted plus the order number.
The path from the file to Claude. The filter sits in the middle, and the file on disk is not changed.

The test file looks like a CRM export: eight invented customers with name, CPR number, email, phone, street address, city, revenue and a free-text note. 26 of the values the filter should catch are planted in it. Two of them sit inside the notes: the CPR number of a new owner and the phone number of a customer's daughter. The same request ran eight times: with no filter, the first version, the fixed one and a control, on a Danish and an English prompt. In two more runs Claude was asked for the revenue in each city.

With the first version, Claude told the user, in Danish, that the file didn't contain the CPR numbers, the phone numbers or most of the emails. That wasn't true. The file was untouched. Claude got the placeholders without knowing where they came from. Claude assumed the file was masked, and would have sent you off looking for another one.

The first version had a second fault too. One value got through: an email address whose domain contained the Danish letter o with a stroke through it. The email pattern only knew the letters a to z, so one Danish letter was enough for the filter not to see an email at all.

The fixed version does two things. The pattern matches any letter, and Claude gets one line of notice: how many values the filter swapped out, and that the real values are still in the file on disk. To see whether the line or the pattern made the difference, Claude Code also ran the fixed filter without the line. Claude then repeated the mistake on both the Danish and the English prompt. On the Danish one it said, translated: "So there are no real values to give." With the line, Claude said a CPR filter had replaced them with placeholders before it read the file, and that the real values were still in kunder.csv.

An animation of Claude's answer in a terminal window, translated from Danish. First with no notice from the filter: the answer says the CPR numbers, emails and phones appear as placeholders in the file, so there are no real values to give, and the words in the file turn oxblood. Then the filter's one line arrives, and the answer changes to: a CPR filter replaced them with placeholders before I read the file, and the real values are still in kunder.csv.
Same filter, same file. Without the filter's one line, Claude blames the file. With it, Claude says what happened. Both answers are Claude's own, translated.

A filter that hides things without saying so makes Claude misread your data. Tell the AI what you removed.

What Claude saw, with and without the filter

With no filter, Claude read 26 of 26 and repeated 10 in its answer: the CPR number, email and phone of the three largest customers, plus the daughter's number from a note. That's also what the request asked for. With the fixed filter, Claude read 0 of the 26 and repeated 0.

The work still got done. With the filter on, Claude was asked how much revenue sat in each city. The answer was right: Herning was the largest at 2,480,000 kroner, and the eight cities came to 9,910,000 kroner, without Claude seeing a single CPR number.

Claude still read all eight names and all eight street addresses in every single run, because the filter isn't built for them. And an order number in a note, 3112994455, was swapped for [CPR] in every run with the filter. It looks like a CPR number, 31 December 1999 plus four digits, and the pattern can't tell the difference.

A table headed What Claude read about one customer. Rows: name, CPR number, email, phone, street address, the daughter's phone in the note and revenue. Columns: No filter, First try and Fixed. With no filter, CPR, email and phone numbers reach Claude. In First try the CPR number and phones are swapped, but the email gets through. In Fixed all four are swapped. Name, address and revenue reach Claude in all three.
What the filter stops and what it lets through, for one customer from the test file.

How to test a filter before you trust it

What the test showed applies to any filter, including one you buy: a tool that's meant to hide personal data, a vendor that promises to anonymise, or an internal rule to clean exports before they go anywhere.

Make a test file of invented customers that looks like your own export. Plant the values the filter should catch, and add the traps on purpose: a letter outside a to z in an email address, an ID number in the middle of a note, another person's phone number, and a number that only looks like an ID number. Send the file through the filter, and ask the AI for exactly the details the filter should hide. Then count how many of the planted values turn up in the answer.

Two rows of the test file shown as a card, with each trap marked. The email address is marked a Danish letter, with the o with a stroke underlined. The note, wants contact via the daughter on 61 44 32 10, is marked another person's number in free text. An order number, 3112994455, is marked CPR-shaped number. The name and the address are each marked not the filter's job, count separately.
Two test rows with the traps planted on purpose. Every detail is invented.

Finally, ask the AI why the details are missing. If it says the file is masked, it doesn't know a filter is in place, and sooner or later it'll give a colleague a wrong answer.

If you're buying a filter, ask the vendor two questions: have you tested it on letters outside a to z, and what is the AI told when something has been removed?

What the filter can't do

The filter knows formats, not meaning. That leaves five limits:

  • Names, addresses and whatever the notes say about the customer get through.
  • A number that looks like a CPR number gets swapped, even when it's an order number.
  • What you type into the prompt yourself isn't filtered. Only what the tools fetch.
  • It reduces what Claude sees. The data is still on the machine and the names still reach Claude, so the file isn't anonymised.
  • Reshaped text gets through. The pattern looks for 123456-7890. If a command spaces out the characters as 1 2 3 4 5 6 - 7 8 9 0, the pattern no longer matches.

Mods also run with the same access to the machine as Claude Code itself. Anthropic says they aren't sandboxed, and that you should only install mods from sources you trust. A mod meant to protect your data can read it too. The filter is 53 lines in all, 37 of them code, and you should read them before you install it. Anthropic's own guide says of a different mod, one that looks for dangerous commands: "It's a safety net, not a permission system." The same goes here.

Added 2 October. A reader asked whether the filter only guards Read, so Claude could see the raw values through grep or Bash instead. Claude Code ran six more tests on the same file. Grep for a name, grep for the CPR number itself, and printing the file through Bash: 0 of 26 reached Claude each time, because the filter checks the result of every tool, not just Read. With no filter, 26 of 26 got through. Asked to find a way around the filter, Claude refused and called it a deliberate privacy control. But when it was told to run a command that spaces out the characters, 25 of 26 got through. So the filter stops accidents, not someone who wants the data.

Whether Claude Code touches the customer export is your call

If GDPR is your responsibility, you decide whether your people may use Claude Code on the customer export. The filter decides whether CPR numbers, emails and phone numbers reach Claude, and so what Claude can repeat in an answer and what gets sent to the model. It doesn't change your data processing agreement, and it doesn't change how long Anthropic keeps what's sent. I covered that in the article on zero data retention: the retention arrangement decides what Anthropic keeps. The filter decides what Anthropic gets.

If your company is on Claude Team or Enterprise, an admin can make sure the filter loads for everyone, ahead of any mods people install themselves. Then it isn't up to each person to remember. Anthropic has a built-in mod of its own, sec-default, which loads first on those plans and on any machine with managed settings, and stops user-installed mods from doing risky things. Put sec-default on that list next to the filter, or you lose its restrictions.

For whoever sets it up: The filter is free as a zip file with instructions. Unzip it, start Claude Code with claude --plugin-dir ./cpr-filter, and run claude plugin test ./cpr-filter before you trust it. Adapt the patterns to your own numbers, such as customer or employee numbers. It needs Claude Code 2.1.287 or later.

Internal AI tools with the filter in place from day one

As an AI implementation consultant, I set Claude up in companies where customer data is part of the work. The hard part is finding out which numbers and names sit in your own exports, building a test file with the right traps, and rolling the filter out centrally to everyone, so nobody has to remember it. I do that as part of internal AI tools, or as your AI consultant before a rollout. It can start with the column headings from your export. You can send those without a single customer in them, and I can see what the filter needs to catch.

I'm also building a product of my own, PrivaThor: a Windows desktop app that finds and replaces personal data in documents fully offline, so you can share them with third parties or AI without the content leaving your machine. It's built for finance, insurance and healthcare in the Nordics, and it launches soon. You can join the waitlist now.

FAQ

Frequently asked questions

A small TypeScript function that Claude Code calls when something happens, for example when Claude uses a tool. A mod can watch the event, change it, or answer instead of Claude Code. Mods arrived on 1 October 2026 and need Claude Code 2.1.287 or later.

That depends on your data processing agreement, your plan and your own rules for personal data. A filter like this one reduces what Claude reads and can repeat, but names and addresses get through, and your agreement stays the same. If you're unsure, use a test file of invented customers until it's settled.

What Claude reads in a file is sent to the model as part of the conversation, CPR numbers included. With the filter, CPR numbers, emails and Danish phone numbers are swapped for placeholders before that happens. How long Anthropic keeps data depends on your plan and your agreement.

Not according to Anthropic's own description. Anthropic only describes mods in Claude Code: in the terminal, the desktop app's Code tab, the VS Code extension and with claude -p. Neither the launch post nor the documentation mentions the regular Claude chat or Cowork.

A mod runs with the same access as Claude Code: it can read files, start programs and use the network. Only install mods from sources you trust, and read the code first. The command claude plugin validate lists which events a mod listens to and what it asks Claude Code to do, before you install it.

Make a test file of invented customers and plant the details the filter should catch. Add traps: a letter outside a to z in an email address, an ID number inside a note, and a number that only looks like an ID number. Ask the AI for the details, and count how many turn up in the answer. Ask it why the details are missing, too.

Sources

How this article was made. This work was produced together with AI. Overall: AI about 83 percent, Kim about 17 percent. Counting production alone, AI did about 94 percent. Claude Code built the filter and the test file, ran the ten recorded runs, two of them control runs, found the bug with the Danish letter and tested the demo in a browser before a word of the article was written. Three Claude agents acting as critical reviewers sharpened the angle and asked for a more realistic test file and for the control runs. Kim wrote the rules, the voice and the method in advance. The numbers are a qualified estimate, not a measured log.

Portrait banner on a light ground. The headline "Who did the work?", below it two numbers for AI and Kim, a bar divided in the same proportion, and eight phase bars with their own labels underneath.
The split is a qualified estimate over the finished task, not a measured log.

Get new essays by email.

Roughly twice a month. Same voice. No list rental, no retargeting.

Protected by Cloudflare Turnstile. No challenge, no CAPTCHA. Brinvik never shares your address.