Llm privacy
Skill patrikherak/llm-privacy
Agent Skill: reversible PII tokenization — masks personal data before the LLM sees it, restores it in the final answer (SKILL.md, works in Claude Code and any agent)
npx -y skills add patrikherak/llm-privacyAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- 23 days oldThe repository was created 23 days ago. New is not bad, but a brand new repository carrying a familiar-sounding name is the shape a typosquat arrives in, and there has been no time for anyone else to find a problem with it.
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Reversible PII tokenization for working with sensitive data. Masks personal data (emails, phones, IBANs, cards, national IDs, tagged names/addresses) behind stable placeholder tokens BEFORE text enters model context, and restores the real values in the final output — deterministic code, no ML, no network. Use when reading, querying, or processing data that contains (or may contain) personal information of real people — customer records, database query results, CRM exports, support tickets, log files — so the model never sees the actual PII but the user still gets a complete answer.
The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
5.9 KB, ~1.4k tokens by cl100k_base, as published. Nobody here has run it
llm-privacy — reversible PII tokenization
Personal data must not enter model context, but the user's answer must stay complete. This
skill gives you a deterministic mask/restore pipeline: pipe sensitive text through mask
before you read it, work with the placeholder tokens as if they were the real values, and
pipe your final answer through restore before presenting it. The mapping lives in a local
vault file (0600) and never enters context.
raw data Jan Novák · [email protected] · +420 601 111 222 · order 80-5550001234
you see Jan Novák · ⟦PII_EMAIL_1⟧ · ⟦PII_PHONE_2⟧ · order 80-5550001234
user gets Jan Novák · [email protected] · +420 601 111 222 · order 80-5550001234
Workflow
1. Never read raw sensitive data directly. When a file, database result, API response, or command output contains (or may contain) PII, route it through the masker instead of reading it raw:
# a file
python3 scripts/llm_privacy.py mask < customers.csv
# any command output (DB query, API call, log dump)
psql -c "SELECT name, email, phone FROM customers LIMIT 20" | python3 scripts/llm_privacy.py mask
Read and analyze the masked output. Treat every ⟦PII_…⟧ token as an opaque, stable
identifier: the same real value always maps to the same token, so you can group, count, join,
and compare tokens exactly as you would the real values.
2. Work normally. Aggregate, filter, reason, draft the answer — keeping tokens verbatim wherever the real value belongs. Never guess what is behind a token, never claim a value is missing because it is masked, and never alter a token's spelling (they restore by exact match).
3. Restore before the user sees it. Write your draft answer (with tokens) to a file or
pipe, run it through restore, and present the restored text:
python3 scripts/llm_privacy.py restore < draft_answer.md > final_answer.md
The restored file contains real PII — give it to the user (or write it where they asked), but do not read it back into context.
Commands
python3 scripts/llm_privacy.py mask [--vault PATH] [--locales cz,sk,…] [--entities email,…] [--fields name,customer,street]
python3 scripts/llm_privacy.py restore [--vault PATH]
python3 scripts/llm_privacy.py stats [--vault PATH] # entry count, no values
python3 scripts/llm_privacy.py clear [--vault PATH] # delete the vault
--fields — use it whenever the data is CSV or JSON. Regexes can't catch free-text names,
but structured data already labels its PII: --fields name,customer,street masks those CSV
columns / JSON keys whole (recursively for JSON), exactly and in any language. Look at the
header/keys of what you're about to read and list every person-related field (name, customer,
email, phone, street, address, note, …). Concurrent calls are safe — the vault is lock-protected.
- Vault default:
$LLM_PRIVACY_VAULT, else~/.llm-privacy/vault.json(created 0600). Use one vault per task/session;clearit when the task is done. - Requires Python 3.9+, stdlib only.
What gets masked
- Always (universal): emails, IBANs, international
+CCphone numbers, IPv4/IPv6, MAC, US SSN, ETH/BTC addresses. Credit cards are opt-in (--entitiesincl.credit_card) and Luhn-gated. - Per locale (opt-in,
--locales): national phone formats and national IDs for 48 countries — checksum-validated where a checksum exists (PESEL, CPF, TCKN, Aadhaar, DNI, …), so a random same-length number does not mask. Enable the locales matching the data's origin. Full table in SPECS.md. - Source tags (exact, any language): free-text names and addresses have no reliable
universal pattern. When YOU generate the data query, wrap known-PII columns so they mask
exactly — e.g. in SQL:
chr(57344) || 'NAME:' || customer_name || chr(57345) AS customer_name(57344/57345 = U+E000/U+E001; the helpertag("NAME", value)in the script does the same in Python). Tagged values mask regardless of language or script — 日本語, العربية, кирилиця all work. - Never masked: order/tracking numbers, UUIDs, hashes, dates, prices — the net is deliberately conservative so business identifiers stay usable.
Rules
- If output might contain PII, mask it — don't read first and decide after; raw values seen once are already in context.
- Prefer computing aggregates at the source (SQL
COUNT/SUM) so row-level PII never needs to flow at all; mask what must flow. - Tokens are opaque: you cannot paste a real value into a follow-up command. Either filter at
the source (e.g. a subquery on the original column), or use the restore→exec→mask chain:
write the command WITH the token, restore it, execute it, and re-mask the output — the real
value touches only the local shell, never your context:
echo "grep '⟦PII_EMAIL_1⟧' orders.log" | python3 scripts/llm_privacy.py restore | sh \ | python3 scripts/llm_privacy.py mask - The vault file is sensitive (it holds the real values). Never print it, never commit it,
clearit when finished. - Never print restored output back into your context — no
cat/diffof the restored file (that would pull the real values in and defeat the whole point). Verify a restore blindly:grep -c 'PII_' final_answer.mdmust print0.
What ships with it: 13 files
74.4 KB alongside SKILL.md, 3 of them executable
hooks/
- guard.pyruns2.3 KB
scripts/
tests/
- test_llm_privacy.pyruns16.0 KB
- .gitignore59 B
- LICENSE1.0 KB
- pyproject.toml1.1 KB
- README.md10.7 KB
- SPECS.md5.8 KB