MaskFlow PII proxy

السجل الرسمي الأمن

Mask PII in outbound MCP tool-call arguments, unmask the results. Indian identifiers included.

التثبيت

أضف هذا إلى إعدادات MCP في عميلك (Claude Desktop: claude_desktop_config.json، وCursor: ‏.cursor/mcp.json، ويستخدم VS Code وغيره الصيغة نفسها «mcpServers»).

PYPI package maskflow-mcp
Claude Code
claude mcp add maskflow-pii-proxy -- uvx maskflow-mcp==0.1.2
إعداد JSON (Claude Desktop وCursor وVS Code)
{
    "mcpServers": {
        "maskflow-pii-proxy": {
            "command": "uvx",
            "args": [
                "maskflow-mcp==0.1.2"
            ]
        }
    }
}

التوثيق

مأخوذ من ملف README للمشروع على GitHub ويُحدَّث أسبوعيًا. تفتح الروابط على GitHub.

المحتويات
  1. MaskFlow
  2. Why
  3. Quickstart
  4. How it works
  5. Protecting your own logs
  6. Auditing what already reached a provider
  7. Gateway: no code change at all
  8. LiteLLM: a guardrail on your existing proxy
  9. LangChain: a one-line import swap
  10. LlamaIndex: keep PII out of RAG
  11. Coding agents: keep customer data out of Claude Code, Copilot, Cursor, Codex, ...
  12. MCP: a masking proxy for agent tool calls
  13. Evidence Cloud (hosted, private preview)
  14. Configuration
  15. What it detects today
  16. Multi-language
  17. MaskFlow vs. alternatives
  18. Benchmark
  19. Roadmap
  20. Links

MaskFlow

Stop Indian PII from ever reaching an LLM.

Aadhaar, PAN, GSTIN, UPI, IFSC, ABHA, Indian names and addresses — detected and replaced with reversible, typed placeholders before a prompt leaves your process, restored in the response. 28 entity types, checksum-validated where a public checksum exists, MIT-licensed, runs entirely on your own infrastructure.

⭐ Star us on GitHub — if MaskFlow is useful to you, a star helps other teams facing the DPDP deadline find it.

Why

India's DPDP Act sets a compliance deadline of 13 May 2027, with penalties of up to ₹250 crore for a breach where the required safeguards weren't in place. Every prompt sent to an LLM provider is a potential data-sharing event — and general-purpose PII tools weren't built to recognize Aadhaar, PAN, GSTIN, UPI VPAs, IFSC codes, ABHA health IDs, or Indian names and addresses reliably. Presidio already owns generic PII and is more mature everywhere else; MaskFlow exists specifically to close that gap, with accuracy that's measured and published, not asserted. See MaskFlow vs. alternatives.

Quickstart

pip install maskflow-sdk
python -m spacy download en_core_web_sm
from maskflow import mask, unmask

# Synthetic Aadhaar number: passes the Verhoeff checksum, belongs to no one.
result = mask("My Aadhaar is 2346 8907 6549 and you can reach me at alice@example.com.")
result.masked_text
# "My Aadhaar is <AADHAAR_1> and you can reach me at <EMAIL_1>."
unmask(result.masked_text, result.mapping)  # original text, restored

For a one-line wrapper around your actual LLM call, or session-scoped masking across a multi-turn agent (same value → same token for as long as the session is open), see packages/maskflow-sdk/README.md.

How it works

  1. Tier-0 excision first. Deterministic regex/checksum matches (Aadhaar, PAN, GSTIN, email, credit card, ...) are found and locked in before the NER pass ever runs — spaCy parses each document at most once, only over what tier-0 didn't already claim.
  2. Every match is a Span. Start/end offsets, entity type, confidence, which recognizer produced it, whether a checksum validated it, and a human-readable explanation trail. Run maskflow explain "<text>" (from maskflow-cli) to see that trail for any input, span by span — including near-misses that fell just below threshold and what config change would catch them.
  3. Deterministic resolution on overlaps. Below-threshold spans are dropped; among what's left, a checksum-validated span always beats an overlapping unvalidated one, then higher confidence, then longer span, then earliest start wins — greedy, non-overlapping placement.
  4. Placeholders are typed, stable, and collision-proof. <AADHAAR_1>, <EMAIL_1>, ... — the same value gets the same token within a session, and if the input text already contains something that looks like a placeholder, a nonce suffix (<AADHAAR_1_a4f9>) is used instead so a real placeholder is never ambiguous with attacker-controlled input.
  5. Recognizers are pluggable. maskflow-pack-intl and maskflow-pack-india are just two "maskflow.recognizers" entry-point plugins sharing one memoised analysis context — write and register your own the same way. See docs/custom-recognizers.md.

Protecting your own logs

Regex/checksum-based recognizers can also scrub your application's own logging calls — not just text passed through mask() — closing the gap where a raw value gets logged before it's ever masked:

from maskflow_core import install_pii_filter

install_pii_filter()  # attaches to the root logger, once, at startup

Opt-in only; importing maskflow_core never touches global logging state on its own. It doesn't cover NER-only entity types (bare names/addresses) or exc_info tracebacks — see docs/logging.md for the exact boundary.

Auditing what already reached a provider

Going forward, mask() keeps PII out of your prompts. But the DPDP audit asks a backward-looking question first: what has this system already sent to a third-party LLM? maskflow scan answers it. It reads your historical LLM traffic — a JSONL/CSV export, a recursive directory, an S3 archive, a Postgres table, or the Langfuse / Helicone / LangSmith API — streams it through the same detection with bounded memory (parallel, resumable), and writes one self-contained HTML report: a single headline number, breakdowns by entity type / provider / model / time, a severity ranking with a plain-English "why this matters" per row, masked excerpts only (never a raw value), and a DPDP Rule 6 mapping appendix. Also --format json|csv. Runs entirely locally — the API sources only read from your own account, nothing is transmitted.

pipx install maskflow-cli   # or: docker run --rm -v "$PWD:/work" ghcr.io/maskflow/cli
maskflow scan jsonl requests.jsonl --field 'messages[].content' --deep -o exposure.html

Also ships as a standalone binary (mac/linux/windows, no Python — pattern pass only) and a GitHub Action that can fail a build over a PII-exposure threshold. A runnable 60-record synthetic example is in packages/maskflow-cli/examples/; full reference, including the Rule 6 mapping, in docs/scan.md.

Gateway: no code change at all

maskflow-gateway is a drop-in OpenAI/Anthropic-compatible proxy. Point your existing client's base URL at it and PII is masked before every request reaches the provider and restored in the response — streaming included (a <PERSON_NAME_1> split across SSE chunks is stitched back together; fuzz-tested at every byte boundary). Tool-call arguments are walked as JSON; multi-turn token identity is kept in Redis (AES-256-GCM at rest).

from openai import OpenAI
client = OpenAI(base_url="http://localhost:8000/v1", api_key="sk-...")  # your real key, passed through
pip install "maskflow-gateway[redis]"   # or: docker run -p 8000:8000 ghcr.io/maskflow/gateway

Full reference in packages/maskflow-gateway/README.md and docs/gateway.md.

LiteLLM: a guardrail on your existing proxy

Already running a LiteLLM proxy? maskflow-litellm is a custom guardrail — no separate service. It masks PII (Indian identifiers included) before a request leaves the proxy and restores it in the response, streaming and tool calls included.

pip install maskflow-litellm
guardrails:
  - guardrail_name: maskflow
    litellm_params:
      guardrail: maskflow_litellm.MaskflowGuardrail
      mode: [pre_call, post_call]

Full reference in packages/maskflow-litellm/README.md and docs/litellm-guardrail.md.

LangChain: a one-line import swap

maskflow-langchain is a drop-in for langchain-experimental's Presidio anonymizer — same .anonymize() / .deanonymize() / .deanonymizer_mapping — so an existing chain migrates by changing one import. The deanonymizer is a streaming-aware Runnable (a placeholder split across streamed chunks is stitched back), and there's an optional leak-guard callback that fails a call closed if PII reaches the model.

pip install maskflow-langchain
# from langchain_experimental.data_anonymizer import PresidioReversibleAnonymizer
from maskflow_langchain import MaskflowReversibleAnonymizer as PresidioReversibleAnonymizer

Full reference in packages/maskflow-langchain/README.md and docs/langchain.md.

LlamaIndex: keep PII out of RAG

maskflow-llamaindex gives a LlamaIndex RAG pipeline two components and an unmask helper. MaskflowNodePostprocessor is a drop-in for llama_index.core.postprocessor.PIINodePostprocessor (same __pii_node_info__ contract) that masks retrieved context before the synthesizer, with no LLM call. MaskflowIngestionTransform masks node text at ingestion so raw PII never reaches the vector store. unmask_response() / MaskflowQueryEngine restore the real values in the answer.

pip install maskflow-llamaindex
from maskflow_llamaindex import MaskflowNodePostprocessor, unmask_response

engine = index.as_query_engine(node_postprocessors=[MaskflowNodePostprocessor()])
response = engine.query("What is Ramesh's PAN?")
answer = unmask_response(str(response), response.source_nodes)

Full reference in packages/maskflow-llamaindex/README.md and docs/llamaindex.md.

Coding agents: keep customer data out of Claude Code, Copilot, Cursor, Codex, ...

maskflow-agent installs Claude Code hooks that mask PII in every tool result (shell output, file reads, searches, MCP) before the model sees it. Real values are restored only inside allowlisted database/kubectl commands, at run time, in a separate process. The agent can still fix that customer's row; the model never sees their Aadhaar, PAN or mobile.

pip install maskflow-agent
maskflow-agent install     # hooks in .claude/settings.json
maskflow-agent doctor      # proves it end to end with synthetic canaries

Or install it as a Claude Code plugin: /plugin marketplace add maskflow/maskflow, then /plugin install maskflow-agent@maskflow.

Other agents (experimental): GitHub Copilot (CLI, cloud agent, VS Code), Cursor, OpenAI Codex, Google Antigravity, Gemini CLI, OpenCode, Cline and Devin Desktop. Install with maskflow-agent install --agent <name>. Coverage differs per agent, because some agents' hooks can't replace a tool's result. The table is in docs/agent.md.

It fails closed, never grants permissions you didn't already have, and keeps the transcript on disk free of real values. Recorded from real sessions on synthetic data; reproduce it with packages/maskflow-agent/examples/demo. Threat model and limits are in docs/agent.md.

MCP: a masking proxy for agent tool calls

maskflow-mcp is a Model Context Protocol proxy. Put it in front of any MCP server and PII in outbound tools/call arguments is masked before it reaches the tool, results are unmasked on the way back, and placeholders stay consistent for the whole agent run. Agent tooling is where PII leakage is least examined; this is a drop-in shim that stops the real values at the boundary.

{
  "mcpServers": {
    "github": {
      "command": "maskflow-mcp",
      "args": ["stdio", "--backend", "npx -y @modelcontextprotocol/server-github",
               "--pass-env", "GITHUB_TOKEN"]
    }
  }
}

Full reference in packages/maskflow-mcp/README.md and docs/mcp.md.

Evidence — a record of what was masked

maskflow-evidence emits a metadata-only record of what was masked — entity type, count, recognizer, action, versions — and never a value, a placeholder, or the mapping. Off by default; one line in .maskflowrc turns it on, to a self-hosted sink (stdout / file / syslog / webhook / OTLP). The gateway emits automatically when enabled; maskflow explain --evidence does it for a single run.

[evidence]
enabled = true
sink    = "file"
path    = "evidence.log"

That no event field can carry free text is enforced in CI. Full reference in docs/evidence.md. maskflow-attest produces a reproducible, Ed25519-signed accuracy attestation for a pinned pack version — see docs/attestations.md. Compliance-control mapping to DPDP / ISO 27001 and the recommended retention period are still being validated with practitioners and are not yet part of this layer.

Evidence Cloud (hosted, private preview)

Optional and paid; everything in this repo stays MIT and free. Evidence Cloud keeps a retained, tamper-evident record that masking happened and turns it into signed DPDP evidence reports that your auditor can verify offline on their own computer, without an account and without trusting us.

  • Metadata only, opt-in. Detection stays on your infrastructure. It receives which type of data was masked, how many, and when. It never receives a value.
  • Signed evidence reports mapped to DPDP Rule 6 safeguards, including where a safeguard is honestly out of scope. Useful for a DPIA or a 72-hour breach notice.
  • Signed accuracy attestations, team access, and an append-only audit log.
  • Free 14-day trial. Billed annually after that.

How Evidence Cloud works · Verify a sample signed report, no sign-up · Start your free trial

Configuration

Drop a .maskflowrc (TOML/YAML/JSON) in your project to adjust entity thresholds, disable an entity, add a custom regex-based entity, exclude specific values, or change the substitution strategy per entity (replace / redact / mask / hash / surrogate — the last swaps in a plausible fake value drawn from reserved/test-only ranges, e.g. RFC 2606 example domains or publicly documented payment-industry test card numbers, instead of a placeholder token). mask()/mask_and_call()/session() all pick it up automatically; no .maskflowrc anywhere behaves exactly as before this existed:

[entities.PHONE]
strategy = "mask"          # "415-555-0132" -> "XXX-XXX-0132" instead of "<PHONE_1>"

[custom.EMPLOYEE_ID]
pattern = '\bEMP-\d{6}\b'
score = 0.9
pip install maskflow-cli   # maskflow config validate / maskflow config show --resolved

See docs/configuration.md for the full schema and precedence rules.

What it detects today

maskflow-sdk and maskflow-cli both bundle maskflow-pack-intl and maskflow-pack-india — installing either gets you all 28 types below with no extra install step.

International (12 types) — maskflow-pack-intl:

TypeHow
EmailRegex
PhoneRegex
SSNRegex + area-code validation
Credit cardRegex + Luhn checksum
IP address (v4/v6)Regex
AWS access keyRegex
API key / generic secretRegex
JWTRegex
IBANRegex + mod-97 checksum
Street addressRegex
Person namespaCy NER
Date of birthspaCy NER + keyword context

Indian (17 types, the moat) — maskflow-pack-india:

TypeHow
Aadhaar (UID + VID)Regex + Verhoeff checksum
Aadhaar (masked display form, e.g. XXXX XXXX 9012)Regex, unvalidated, needs context
PANRegex + holder-category structural check (no public final-letter checksum)
GSTINRegex + state-code range + embedded-PAN check + base-36 checksum
IFSCRegex + bank code against a bundled RBI code list
UPI VPARegex + PSP handle against a bundled NPCI handle list
Indian mobile numberRegex (also the common 5+5 split, e.g. 98765 43210), full confidence with a +91/0 prefix, needs context otherwise
PIN codeRegex, unvalidated, needs context (pin/pincode/state name/address)
Voter ID (EPIC number)Regex, structural only (no public checksum)
Indian passport numberRegex, structural only (no public checksum)
Indian passport MRZ blockRegex + 4 ICAO 9303 check digits
Driving licenceRegex + state RTO code against a bundled code list
Vehicle registrationRegex + state RTO code against a bundled code list
ABHA number (health ID)Regex, unvalidated, needs context
ABHA addressRegex + domain (abdm/sbx) check
Bank account number (India)Regex, unvalidated, needs context (account/a/c/acct)
Person name (Indian)Gazetteer (name corpus) + structural (honorifics, relational markers, initials, form fields; in Devanagari: name labels and honorifics such as नाम: / श्री) + spaCy NER agreement boost. A Devanagari name with no such cue is not detected yet
Indian addressGazetteer (554+ Indian cities/places) + structural (unit markers, landmark-relative phrasing, locality patterns)

(PERSON_NAME is one shared entity type produced by both packs' layers, so 12 + 17 − 1 shared = 28 unique types total.)

Multi-language

@maskflow/detection on npm is a TypeScript port of the 10 pure regex/structural intl types (email, phone, SSN, credit card, IP, AWS key, API key, JWT, IBAN, street address) — same API shape as the Python SDK, tested against the same fixtures so both stay accuracy-matched. PERSON_NAME/DATE_OF_BIRTH (need spaCy NER) and the India pack's checksum-validated types are Python-only — @maskflow/detection is a deliberately narrow browser/Node helper, not a second full engine. See packages/maskflow-js/README.md.

import { mask, unmask } from "@maskflow/detection";

const result = mask("Email me at alice@example.com or call 415-555-0132.");
unmask(result.maskedText, result.mapping); // original text, restored

MaskFlow vs. alternatives

MaskFlowPresidiomask-privacy
Indian identifiers with checksumsAadhaar, PAN, GSTIN, IFSC, UPI (in maskflow-sdk)NoNo
Session-consistent tokens (unmask later)YesVia custom anonymizer configYes, today
NERspaCyspaCy, Stanza, transformersRegex-based, no NER
Breadth / maturityNarrow, early (28 types)Broad, mature (Microsoft-backed, years of production use)Narrow, early
LicenseMITMITVaries by package
LanguagesPython + TypeScript (regex layer)Python, multi-language via configurable NLP modelsJS/TS

Presidio is ahead on breadth and maturity. If you need broad, battle-tested coverage today, use it. MaskFlow's bet is Indian-identifier accuracy and a reversible mask/unmask flow that's simpler to drop into a single, provider-agnostic call.

Benchmark

Real numbers, not vendor claims — including results where competitors beat us. Scored on indiapii-v1.0, 2000 synthetic, checksum-valid documents (Aadhaar/PAN/GSTIN pass the same validity math the real formats use), against stock Presidio, Presidio with two hand-added Aadhaar/PAN recognizers, and mask-privacy. F1 below is partial-overlap matching (exact-character matching is too strict for multi-token spans like addresses — see the full report for both).

EntityMaskFlowPresidio (stock)Presidio + custommask-privacy
GSTIN / IFSC / UPI VPA100%not supportednot supportednot supported
AADHAAR98.4%not supported96.6%not supported
PAN100%not supported100%not supported
Indian mobile number99.0%94.9%94.9%42.2%
Person name47.3%30.4%30.4%30.4%
Indian address43.3%48.2%48.2%50.5%

Indian address is the one row above where a competitor is ahead — our gazetteer still has room to grow, and we're not hiding that. Full per-entity breakdown (all 17 types), strict-vs-partial matching, and latency/memory numbers: bench/reports/indiapii-v1.0/results.md. Reproduce with uv sync --group bench && uv run python -m bench.indiapii.harness run; harness source in bench/indiapii/harness/.

International / US-shaped types

Generic PII is not a surface MaskFlow is built to win — Presidio and mask-privacy own it — but "measured, not asserted" applies to the intl pack too. Scored on intl-pii-v1.0, 1800 synthetic documents (Luhn-valid cards, mod-97-valid IBANs, SSNs in real-but-unassigned area ranges), same harness, partial-overlap F1:

EntityMaskFlowPresidio (stock)mask-privacy
Email / IP address100%100%100%
Phone100%86.7%64.1%
Credit card99.4%100%78.2%
IBAN99.4%76.3%100%
AWS key / API key / JWT100%not supportednot supported
SSN100%100%not detected at defaults
Street address99.8%6.5%¹81.1%
Person name74.0%84.1%86.1%
Date of birth63.6%29.5%¹79.8%

Competitors are ahead on person name (MaskFlow's NER recognizer over-trusts spaCy's PERSON tag on sentence-initial words) and, for mask-privacy, on date of birth (it ships a dedicated birth-date regex; MaskFlow's spaCy-only DATE pass misses numeric formats and the pack's context-keyword list omits "birth date"). Both are tracked follow-ups. ¹ LOCATION / DATE_TIME are mapped generously to ADDRESS / DATE_OF_BIRTH so those engines score non-zero at all — see bench/intlpii/harness/labels.py. Full table (all 12 types, strict + partial, latency/memory): bench/reports/intl-pii-v1.0/results.md. Reproduce with uv sync --group bench && uv run python -m bench.intlpii.harness run.

Is it accurate on your documents?

The numbers above are measured on a synthetic corpus, necessarily — but it's still someone else's documents. maskflow bench --my-data <path> runs the same scoring against your own labelled JSONL file and prints per-entity precision/recall/F1, so "is it accurate on my documents?" is a command, not an argument. See docs/bench.md for the file format.

Does masking hurt the LLM's answer?

bench/indiapii/quality is a 200-task, LLM-judged benchmark that runs each task unmasked, with placeholder masking, and with surrogate masking, then measures the masked-minus-unmasked delta in task completion, fluency, factual consistency, and field-extraction accuracy (plus a hard leak check — a <PAN_1> token must never survive unmask() into the final answer). Method and scoring are built and unit-tested; a published run is pending an ANTHROPIC_API_KEY (make quality-bench — ~1200 disk-cached calls, ~$1.50). Once run, bench/reports/indiapii-quality-v1.0/results.md.

maskflow scan on log-shaped input

The detection benches above are prose; maskflow scan runs over access-log lines, JSON app logs, stack traces, and request dumps. scan-log-v1.0 is 2000 synthetic log records with the PII-lookalike noise real logs carry (trace ids, UUIDs, git SHAs, internal IPs, File.java:142 frames), scoring detection and — the metric that matters for an audit — the false-positive rate. --deep, partial-overlap F1:

EntityMaskFlowPresidio (stock)Naive regex
Aadhaar / PAN / GSTIN / IFSC / UPI100%not supported67–100%
Email / Indian mobile100%79–85%95–100%
Credit card97.8%100%86.2%
Person name87.9%57.6%not supported
IP address66.7%¹66.7%¹59.6%¹

False positives / 1000 log records: MaskFlow --deep 428, its patterns-only pass 298, Presidio 544, naive regex 433. Checksum-validated identifiers essentially never false-positive on log noise (naive regex flags 337 fake Aadhaars; MaskFlow flags 0); the --deep NER pass produces ~2.5× the false PERSON_NAME hits that the patterns pass does. ¹ every internal 10./172.16./192.168. IP is reported as IP_ADDRESS — a known gap (no private-IP suppression). A plumbing check verifies the real scan pipeline surfaces exactly what detect() finds. Full report: bench/reports/scan-log-v1.0/results.md.

Framework integrations match the core engine

The LiteLLM / LangChain / LlamaIndex / MCP wrappers each adapt the masking engine to a framework's data shapes. bench/integrations routes 300 indiapii-v1.0 documents through every wrapper's masking layer and asserts the masked (entity_type, value) set, the byte-exact round-trip, and streamed-response reassembly (chunks split at every byte) are identical to maskflow.mask() / maskflow.unmask() — 100% across all four (bench/reports/integration-parity-v1/results.md). A divergence would be a wrapper bug; it's a CI gate.

Roadmap

Openly not done yet, so you know what you're signing up for:

  • maskflow-gateway hardening: more provider schemas, a Redis-cluster session backend, and first-class OpenTelemetry traces.

الأسئلة الشائعة

ما هو MaskFlow PII proxy؟

Mask PII in outbound MCP tool-call arguments, unmask the results. Indian identifiers included.

كيف أثبّت MaskFlow PII proxy؟

يعمل على جهازك. في Claude Code: claude mcp add maskflow-pii-proxy -- uvx maskflow-mcp==0.1.2. وفي العملاء الآخرين، الصق إعداد JSON من هذه الصفحة في إعدادات MCP الخاصة بهم.

هل يحتاج MaskFlow PII proxy إلى مفتاح API؟

لا يذكر إدراجه في السجل أي مفتاح أو سر إلزامي. قد تتطلب بعض الميزات حسابًا لدى الخدمة التي يتصل بها.

هل MaskFlow PII proxy مفتوح المصدر؟

نعم. شيفرته متاحة على GitHub بترخيص MIT.

من يصون MaskFlow PII proxy؟

ينشره maskflow. يدرجه AI.tn لكنه لا يصونه ولا يوصي به.

من سجل MCP الرسمي. الإدراج ليس توصية: راجع شيفرة الخادم وصلاحياته قبل منحه الوصول إلى بياناتك.