Skip to main content

Anonymize PII before it reaches an LLM - with consistent, reversible labels. 10 languages: JA/EN/ES/VI/ZH/KO/FR/DE/PT/IT.

Project description

English | 日本語 | Español | Tiếng Việt | 中文 | 한국어 | Français | Deutsch | Português | Italiano

Prompt Anonymizer

A buddy check for your prompts — before they reach an LLM. On-device, reversible anonymization that catches the PII you didn't mean to send.

CI Release Python License: MIT

Your team already has the rule: don't paste customer data, secrets, or personal info into ChatGPT / Claude / Gemini. But people are in a hurry, and a name or a phone number slips through. Prompt Anonymizer is the buddy check — it runs on your machine and catches that PII before the text leaves, so a slip doesn't become a leak. It doesn't replace the rule or your judgment; it backs them up.

Line of defense against a PII slip Catches it? What you trust
A written policy alone ✗ relies on memory everyone, every time
Your own last-second vigilance ~ when you remember your attention, mid-rush
+ Prompt Anonymizer ✓ automatic, on-device code you can read

It replaces PII with consistent labels (<人名_1>, <Name_1>, <Nombre_1>, <Tên_1>, …) before the text leaves your machine. Because the same value always gets the same label, the LLM's answer still makes sense — you're not trading away frontier intelligence to stay safe. When the reply comes back, the mapping — which never left your device — restores the real values.

For people, it's a second pair of eyes in the browser, desktop app, or Chrome extension. For pipelines, the same check runs automatically — the OpenAI-compatible proxy masks before egress, and the scan gate fails a commit or CI run when PII slips in.

Supported languages: English (en), Japanese (ja), Spanish (es), Vietnamese (vi), and — new — Chinese (zh), Korean (ko), French (fr), German (de), Portuguese (pt) and Italian (it). The default PromptAnonymizer(languages=…) remains ("en", "ja"); every other language is opt-in via languages=[...]. All UI language pickers and auto-detect cover all ten. Language support is registry-driven — adding a language is one registry entry (languages.py / languages.ts) plus one label file.

Detection runs on-device (WebGPU / WASM in the browser, spaCy or local transformers in Python). Don't take our word for it: open DevTools, watch the network tab, or read the source. It's MIT-licensed and small enough to audit in one sitting — docs/AUDIT.md is the step-by-step procedure.

Table of contents

Demo

Anonymize → the mapping stays local → the LLM reply keeps the labels → restore:

Browser app demo: anonymize, mapping, restore round-trip
CLI demo CLI demo
Chrome extension demo (side panel) Chrome extension demo

Try it

Target How Notes
Browser (WebGPU) akazah.github.io/prompt-anonymizer 100% on-device: NER runs in your browser via WebGPU (WASM fallback). Your text is never sent to a server — verify it in the network tab.
Desktop app Download from Releases (.dmg / .msi / .exe / .AppImage / .deb / .rpm) Tauri 2. Unsigned for now — your OS will warn on first launch.
Chrome extension prompt-anonymizer-extension-*.zip from Releases Unzip → chrome://extensions → enable Developer mode → "Load unpacked". Select text → right-click → Anonymize selection.
Python / CLI pip install prompt-anonymizer Presidio + spaCy. See Quickstart below.
Node CLI (npx) npx @prompt-anonymizer/cli Same commands and flags as the Python CLI; transformers.js NER, fully on-device.
Web Component @prompt-anonymizer/element Framework-agnostic <prompt-anonymizer> element: drop the full anonymize → restore panel into any site (plain HTML, Svelte, Angular, …).
React / Vue @prompt-anonymizer/react / @prompt-anonymizer/vue Drop-in <AnonymizerPanel /> component plus a useAnonymizer() hook / composable for custom UIs. See Quickstart below.
Local proxy + admin GUI npx @prompt-anonymizer/proxy OpenAI-compatible reverse proxy: point OPENAI_BASE_URL at it and PII is masked before leaving your machine, labels restored in responses (incl. streaming). Admin GUI on http://127.0.0.1:8787/admin/. See Quickstart below.
MCP server npx @prompt-anonymizer/mcp anonymize / deanonymize / scan tools for any MCP client (Claude Desktop, Claude Code, Cursor, …). The label mapping stays in server memory (mapping_id) and is never shown to the model unless explicitly requested. See Quickstart below.
Commit hook / CI gate prompt-anonymizer scan (both CLIs) + .pre-commit-hooks.yaml Exit-code PII gate for commit-time and CI checks: reports file:line:col and entity type, never the matched text. Offline and model-free by default. See below.

Quickstart (Python)

pip install prompt-anonymizer
python -m spacy download ja_core_news_sm   # en: en_core_web_sm; es: es_core_news_sm
python -m spacy download xx_ent_wiki_sm    # vi: no official spaCy pipeline — WikiNER
# zh: zh_core_web_sm; ko: ko_core_news_sm; fr/de/pt/it: *_core_news_sm — or
# install every sm model at once: uv sync --group models (lg: --group models-lg)
from prompt_anonymizer import PromptAnonymizer

pa = PromptAnonymizer(languages=["ja"])
result = pa.anonymize("山田太郎の電話は090-1234-5678", language="ja")

result.text     # '<人名_1>の電話は<電話番号_1>'
result.mapping  # {'<人名_1>': '山田太郎', '<電話番号_1>': '090-1234-5678'}

pa_es = PromptAnonymizer(languages=["es"])
pa_es.anonymize(
    "El cliente es Javier Moreno, teléfono 612 345 678", language="es"
).text  # 'El cliente es <Nombre_1>, teléfono <Teléfono_1>'

# vi names need the transformer backend (see "Optional transformer NER backend")
pa_vi = PromptAnonymizer(languages=["vi"], ner_backend="hf")
pa_vi.anonymize(
    "Tôi tên là Nguyễn Văn An, số điện thoại 0912 345 678", language="vi"
).text  # 'Tôi tên là <Tên_1>, số điện thoại <SốĐiệnThoại_1>'

llm_output = call_your_llm(result.text)          # labels survive the round trip
pa.deanonymize(llm_output, result.mapping)       # real values restored, locally

CLI (-l ja|en|es|vi|zh|ko|fr|de|pt|it):

prompt-anonymizer anonymize -l ja --interactive --mapping-file mapping.json \
  -t "山田太郎の電話は090-1234-5678"
prompt-anonymizer anonymize -l es -t "El cliente es Javier Moreno, teléfono 612 345 678"
prompt-anonymizer anonymize -l fr -t "Le client est Pierre Durand, téléphone 06 12 34 56 78"
prompt-anonymizer deanonymize --mapping-file mapping.json -t "<人名_1>様 ..."

Quickstart (JavaScript / TypeScript)

The Node CLI mirrors the Python CLI (same commands, flags and JSON output), running the TypeScript core with transformers.js NER on-device:

npx @prompt-anonymizer/cli anonymize -t "山田太郎の電話は090-1234-5678"

To embed the ready-made anonymize → restore panel in any frontend, use the framework-agnostic web component:

<script type="module">
  import { definePromptAnonymizer } from "@prompt-anonymizer/element";
  definePromptAnonymizer();
</script>
<prompt-anonymizer language="auto"></prompt-anonymizer>

React (@prompt-anonymizer/react) and Vue 3 (@prompt-anonymizer/vue) ship a typed <AnonymizerPanel /> wrapping that element:

import { AnonymizerPanel } from "@prompt-anonymizer/react"; // or "@prompt-anonymizer/vue"

<AnonymizerPanel language="auto" denyList={["ProjectX"]}
  onAnonymize={(result) => console.log(result.text)} />

For custom UIs, both packages also expose the anonymize → LLM → restore session as a hook / composable:

import { useAnonymizer } from "@prompt-anonymizer/react"; // or "@prompt-anonymizer/vue"

const { anonymize, restore, mapping, busy, error } = useAnonymizer();
const result = await anonymize(input, { language: "ja" });
// send result.text to the LLM — the mapping never leaves the device — then:
const { text: restored, unresolved } = await restore(llmReply);

By default detection is regex-only (emails, phone numbers, …); pass a ner (e.g. new TransformersNerBackend() from @prompt-anonymizer/core) to also mask names and locations.

Quickstart (local proxy)

Run the OpenAI-compatible proxy and point any client at it — PII is masked before the request leaves your machine and labels are restored in the response (streaming included). Mappings stay in proxy memory, per request:

npx @prompt-anonymizer/proxy            # listens on http://127.0.0.1:8787

# In your app / shell:
export OPENAI_BASE_URL=http://127.0.0.1:8787/v1

The admin GUI at http://127.0.0.1:8787/admin/ shows live status and redaction events (labels and counts only), edits the proxy config (upstream, NER, deny/allow lists) and offers a local-only anonymization playground. The proxy binds to 127.0.0.1 by default; original values are only revealable in the GUI when you explicitly enable --record-mappings.

Quickstart (MCP server)

Give any MCP client — Claude Desktop, Claude Code, Cursor, … — on-device anonymization tools:

# Claude Code:
claude mcp add prompt-anonymizer -- npx -y @prompt-anonymizer/mcp

Three tools, all designed so PII stays out of the model context: anonymize returns the masked text plus a mapping_id (the mapping stays in server memory unless you explicitly ask for it), deanonymize restores by mapping_id — optionally straight to a file — and scan checks files for PII, reporting file:line:col and entity type but never the matched text. Pass --ner in the server args to also mask names/locations (one-time model download on first use).

Commit-time / CI gate (scan)

Both CLIs ship a scan subcommand designed for git hooks and CI: it exits 0 when the inputs are clean, 1 when PII is found and 2 on errors. It reports file:line:col and the entity type only — the matched text is never printed, so hook output and CI logs stay PII-free. By default it is offline, deterministic and model-free (structured PII: emails, phone numbers, JP postal codes, My Number, credit cards — plus --deny terms); --ner opts into name/location detection where models are available.

prompt-anonymizer scan src/prompt.txt docs/*.md      # files (e.g. staged)
git diff --cached -U0 | prompt-anonymizer scan       # or pipe a diff
prompt-anonymizer scan --deny ProjectX --json -t "..."

With the pre-commit framework (hook definition: .pre-commit-hooks.yaml):

repos:
  - repo: https://github.com/akazah/prompt-anonymizer
    rev: v0.3.1
    hooks:
      - id: prompt-anonymizer-scan
        # args: [--deny, ProjectX, --allow, support@example.com]

Node projects can wire the same gate through husky + lint-staged (npx @prompt-anonymizer/cli scan):

{ "lint-staged": { "*": "prompt-anonymizer scan" } }

Like everything else here, detection is best-effort: treat scan as a safety net for obvious leaks, not a guarantee.

Why not …?

Why not just use Presidio? Use Microsoft Presidio directly if you need a general-purpose PII detection / anonymization framework. Prompt Anonymizer uses Presidio as the engine of its Python core and adds the LLM round-trip workflow on top: consistent placeholders, anonymized prompt out, local restore after the response — plus browser, extension and desktop surfaces that need no Python at all.

Why not LLM Guard? LLM Guard is a solid Python-side guardrail suite with its own Anonymize/Deanonymize. Prompt Anonymizer differs in three ways: multilingual detection across ten languages with locale-specific structured PII (checksum-validated national IDs such as My Number, per-region phone formats), non-developer surfaces (paste text in a browser page — no Python setup), and a codebase small enough to actually read.

Why not a "100% local" Chrome extension? Several closed-source extensions claim local processing. Claims are not audits. This project is MIT-licensed: open the network tab, or read the source. (Malicious "AI privacy" extensions that exfiltrate conversations have been documented — the category has earned the skepticism.)

How it works

  1. Detection — Presidio + spaCy NER (Python) or transformers.js NER + regex recognizers (browser/desktop/extension), extended with registry-driven, locale-specific phone patterns (JP, US/NANP, ES, VN, CN, KR, FR, DE, PT, IT) and Japanese-specific recognizers (〒 postal codes, My Number with check-digit validation). Emails and credit cards are language-agnostic; JP_POSTAL_CODE and JP_MY_NUMBER are detected in every language mode.
  2. Consistent labeling — spans are merged (score-first) and replaced offset-based from the end; identical values share one label.
  3. Reversal — deanonymize(text, mapping) restores originals, longest label first. The mapping is returned to you and never persisted by the library; storing it safely is your responsibility.

Supported entities

Entity en label ja label es label vi label Engine
PERSON Name 人名 Nombre Tên NER
EMAIL_ADDRESS Email メールアドレス Correo Email pattern
LOCATION Location 住所 Dirección ĐịaChỉ NER
PHONE_NUMBER Phone 電話番号 Teléfono SốĐiệnThoại registry-driven per-language patterns + libphonenumber regions (JP/US/ES/VN/CN/KR/FR/DE/PT/IT)
JP_POSTAL_CODE PostalCode 郵便番号 CódigoPostal MãBưuĐiện pattern (custom)
JP_MY_NUMBER MyNumber マイナンバー MyNumber MyNumber pattern + check digit (custom)
CREDIT_CARD CreditCard クレジットカード Tarjeta ThẻTínDụng pattern + Luhn check (both cores, all languages)
US_SSN (opt-in) SSN 社会保障番号 SSN SSN pattern + invalidation rules (both cores, all languages)
IBAN_CODE (opt-in) IBAN IBAN IBAN IBAN pattern + mod-97 check (both cores, all languages)
CUSTOM (deny list) Custom 秘匿情報 Personalizado TùyChỉnh exact match

Labels for the six new languages (zh, ko, fr, de, pt, it) ship in src/prompt_anonymizer/labels/*.yaml (Python) and in LABELS in web/packages/core/src/labeling.ts (TS).

deny_list forces masking of specific strings; allow_list exempts them. Opt-in entities are not detected by default — request them explicitly: PromptAnonymizer(entities=[...]), new Anonymizer({ entities }), or --entities PERSON,EMAIL_ADDRESS,US_SSN,IBAN_CODE on either CLI.

Optional transformer NER backend (Python)

The default NER is spaCy, with the per-language model resolved from the central registry (see the table below; install every sm model with uv sync --group models, the lg ones with --group models-lg, or use python -m spacy download <model>). Vietnamese has no official spaCy pipeline — both model sizes use the multi-language WikiNER model xx_ent_wiki_sm for tokenization and baseline PER/LOC NER. For good Vietnamese name/location recall, use the transformer backend instead (see below).

For markedly better PERSON/LOCATION recall (especially ja and vi), install the hf extra and switch the backend — per-language Hugging Face models, fully locally:

Language spaCy (sm / lg) HF NER (ner_backend="hf")
ja ja_core_news_sm / ja_core_news_lg tsmatz/xlm-roberta-ner-japanese
en en_core_web_sm / en_core_web_lg dslim/bert-base-NER
es es_core_news_sm / es_core_news_lg Davlan/bert-base-multilingual-cased-ner-hrl
vi xx_ent_wiki_sm (both sizes) NlpHUST/ner-vietnamese-electra-base
zh zh_core_web_sm / zh_core_web_lg Davlan/bert-base-multilingual-cased-ner-hrl
ko ko_core_news_sm / ko_core_news_lg Davlan/bert-base-multilingual-cased-ner-hrl
fr fr_core_news_sm / fr_core_news_lg Davlan/bert-base-multilingual-cased-ner-hrl
de de_core_news_sm / de_core_news_lg Davlan/bert-base-multilingual-cased-ner-hrl
pt pt_core_news_sm / pt_core_news_lg Davlan/bert-base-multilingual-cased-ner-hrl
it it_core_news_sm / it_core_news_lg Davlan/bert-base-multilingual-cased-ner-hrl

The multilingual HRL model covers de/es/fr/it/pt/zh natively; Korean has no dedicated checkpoint in this family and relies on mBERT's cross-lingual transfer.

The TypeScript core (browser / extension / desktop / Node CLI) runs transformers.js ONNX models: ja and en use the same families as above; es, vi, zh, ko, fr, de, pt and it all use Xenova/bert-base-multilingual-cased-ner-hrl (no ONNX export of a dedicated Vietnamese NER model exists; the multilingual model transfers well to Vietnamese, and the same transfer caveat applies to Korean).

pip install "prompt-anonymizer[hf]"
pa = PromptAnonymizer(languages=["ja"], ner_backend="hf")  # CLI: --ner-backend hf
pa_vi = PromptAnonymizer(languages=["vi"], ner_backend="hf")  # recommended for vi names

Batch processing is also available and much faster than a loop:

results = pa.anonymize_batch(texts, language="ja", batch_size=16)

Accuracy

Measured span-level on a seeded synthetic golden set (200 documents each for all ten languages in tests/golden/golden_{lang}.json) — see docs/EVAL.md for the full table and uv run python -m prompt_anonymizer.evals to reproduce (defaults to all ten languages). Highlights (Python core, sm models): ja PHONE_NUMBER / EMAIL_ADDRESS / JP_POSTAL_CODE / CREDIT_CARD recall 1.00; ja PERSON recall 0.82 with spaCy, 1.00 with ner_backend="hf". es/vi PHONE_NUMBER recall is also 1.00; vi PERSON/LOCATION benefit strongly from ner_backend="hf". Structured-PII recall (phone / email / card) is 1.00 for the six new languages (zh, ko, fr, de, pt, it) on the golden set — docs/EVAL.md has the TS-core table; Python NER numbers are produced by the weekly eval.

These numbers exist to catch regressions, not to promise recall on real-world text.

Limitations

  • Detection is best-effort and not guaranteed. False negatives happen; always review the anonymized text before sending it anywhere (--interactive, and the mapping tables in the UIs, exist for this).
  • Anonymization hides identifiers, not context. Quasi-identifying details in the surrounding text (a rare job title, a specific event) can still narrow down who or what you're writing about.
  • LOCATION recall is the weakest entity, especially for partial Japanese addresses.
  • The browser NER model is a one-time ~100–300 MB download (cached afterwards).
  • Desktop and extension builds are unsigned for now.

Roadmap

See open issues and IMPLEMENTATION_PLAN.md. Highlights: PyPI / npm registry publication (enable trusted publishing — packages ship with GitHub Releases today), Chrome Web Store listing, code signing, smaller Japanese NER models, multi-region structured PII (more phone / national-ID formats via checksum validation), opt-in granular redaction (locale-aware policies that preserve coarse-grained context — e.g. surname-only hints, phone area/exchange codes, addresses down to prefecture/municipality — with per-country legal/privacy scope defined before implementation; preserved fragments may still be quasi-identifying).

Contributing / Security / License

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

prompt_anonymizer-0.3.1.tar.gz (30.2 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

prompt_anonymizer-0.3.1-py3-none-any.whl (54.1 kB view details)

Uploaded Python 3

File details

Details for the file prompt_anonymizer-0.3.1.tar.gz.

File metadata

  • Download URL: prompt_anonymizer-0.3.1.tar.gz
  • Upload date:
  • Size: 30.2 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for prompt_anonymizer-0.3.1.tar.gz
Algorithm Hash digest
SHA256 c20413df271f73b056891156f136d8f09a42c91ffca8d73a5aceb856cd7d3318
MD5 cb4e9295fa2e369948d2d104403063ae
BLAKE2b-256 32cc1487d53528c4e8fd27ecb3546df7872d45e294e2e9a22d12ff05f4c53f21

See more details on using hashes here.

Provenance

The following attestation bundles were made for prompt_anonymizer-0.3.1.tar.gz:

Publisher: release.yml on akazah/prompt-anonymizer

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file prompt_anonymizer-0.3.1-py3-none-any.whl.

File metadata

File hashes

Hashes for prompt_anonymizer-0.3.1-py3-none-any.whl
Algorithm Hash digest
SHA256 ea378860646506adcd32410c1b5f1d5e53dab45ff8314fd5b6f3b588cd139260
MD5 9fa1d1140703ea51f85d77cad7c26405
BLAKE2b-256 f767c3789c1b4dcf3820c3192edf0739f312a8467b83a2de6b2940e176caf843

See more details on using hashes here.

Provenance

The following attestation bundles were made for prompt_anonymizer-0.3.1-py3-none-any.whl:

Publisher: release.yml on akazah/prompt-anonymizer

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page