{"id":"13bbde97-8424-4e7d-a7ea-95b4fac1d493","slug":"kavach-pii","kind":"mcp","name":"kavach-pii","summary":"detect, mask and restore Indian PII (Aadhaar, PAN, UPI, bank details) before it reaches an LLM. Local, deterministic, reversible.","owner":"harshita1615","repoUrl":"https://github.com/harshita1615/kavach-pii","license":"MIT","licenseTier":"permissive","trust":"community","stars":0,"match":null,"fewStars":true,"format":"mcp","createdAt":"2026-10-08T14:13:44.000Z","addedAt":"2026-10-21T05:16:08.176Z","logoUrl":"https://avatars.githubusercontent.com/u/228217144?u=d695ba532eb0e5563462c1fd34526a95c4c00f82&v=4","linkOnly":false,"collection":null,"skillPath":null,"categories":["cloud-infra","data-analytics"],"tags":["aadhaar","data-masking","dpdp","india","llm","pii","privacy","python"],"runtime":"python","local":true,"cloudCapable":false,"cloudCapability":"unknown","cloudReadyOnPlatform":false,"needsApiKey":false,"reviewed":false,"syncedAt":"2026-10-26T06:57:21.036Z","sample":false,"url":"https://conduits.dev/tools/kavach-pii","language":"Python","transports":["stdio"],"install":{"pypi":"kavach-pii"},"requirements":{"env":[],"upstreamMayCharge":false},"tools":null,"readme":"# Kavach\n\n**Keep Indian personal data out of LLM prompts.**\n\nKavach (कवच, \"shield\") detects Aadhaar, PAN, UPI IDs, bank details, phone numbers and more,\nreplaces them with placeholders before your prompt reaches an LLM, and restores them in the\nreply. It runs locally, has no runtime dependencies, and uses deterministic rules (patterns,\nchecksums and context words) rather than another model.\n\n**Website:** [kavach.bigforge.tech](https://kavach.bigforge.tech/) · **[Try it in your browser](https://kavach.bigforge.tech/demo/)**\n\n```python\nfrom kavach import Kavach\n\nsession = Kavach().session()\n\nsafe = session.mask(\"My PAN is ABCPE1234F, refund to rahul@okaxis\")\n# 'My PAN is [PAN_1], refund to [UPI_1]'\n\nreply = call_llm(safe)  # the LLM only ever sees placeholders\nprint(session.unmask(reply))  # 'Refund for ABCPE1234F has been sent to rahul@okaxis'\n```\n\n> **Status:** alpha (v0.2.0). The API may change before 1.0. Measured accuracy on a synthetic\n> benchmark is in [Accuracy](#accuracy).\n\n## Why\n\nSecurity reviews often block LLM features in Indian fintech, insurance and health apps because\ncustomer data would be sent to a third-party model. Generic PII tools cover some Indian IDs\n([Microsoft Presidio](https://microsoft.github.io/presidio/) detects Aadhaar and PAN, for\nexample) but miss the identifiers these apps handle most: UPI IDs, IFSC codes, bank account\nnumbers, GSTINs and Indian phone formats, often written in Hinglish. Kavach fills those gaps and\nmakes masking reversible, so your app keeps working after the LLM responds.\n\n## Install\n\n```bash\npip install kavach-pii\n```\n\nThe package is `kavach-pii`; the import is `kavach`. Requires Python 3.10+ and has no\ndependencies. For the [MCP server](#mcp-server), install `kavach-pii[mcp]`. (An unrelated PyPI package named `kavach` also uses the `kavach` import name; do\nnot install both in the same environment.)\n\nTo try unreleased changes, install from source:\n\n```bash\npip install git+https://github.com/harshita1615/kavach-pii.git\n```\n\n## What it detects\n\n| Entity | Placeholder | How it is validated |\n| --- | --- | --- |\n| Aadhaar | `[AADHAAR_1]` | 12 digits, first digit 2-9, Verhoeff checksum. Spaces, dashes and line breaks allowed |\n| PAN | `[PAN_1]` | 5 letters, 4 digits, 1 letter; 4th letter must be a valid holder type (P, C, H, F, A, T, B, L, J, G) |\n| UPI ID | `[UPI_1]` | `name@handle`; known handles (okaxis, ybl, paytm, ...) score high, unknown handles need context |\n| IFSC | `[IFSC_1]` | 4 letters, `0`, 6 letters or digits; known bank codes score higher |\n| Bank account | `[BANK_ACCOUNT_1]` | 9-18 digits **and** a nearby context word (\"account\", \"a/c\", \"khata\", \"खाता\") |\n| Mobile number | `[PHONE_1]` | 10 digits starting 6-9, optional `+91`, `0091`, `91` or `0`; contiguous, 5-5 or 3-3-4 |\n| GSTIN | `[GSTIN_1]` | State code, embedded PAN, entity number, `Z`, base-36 check character |\n| Email | `[EMAIL_1]` | Standard address format |\n\nContext words in English, Hinglish and Devanagari (\"mera aadhaar\", \"khata number\", \"मोबाइल\")\nraise confidence. When two matches overlap, Kavach keeps the higher-confidence, longer and more\nspecific one, so `9876543210@paytm` is one UPI ID, not a phone number.\n\nPlanned next: ABHA health ID, PIN code, voter ID, passport and vehicle numbers (v1.1); names and\naddresses (v2).\n\n## Usage\n\n### Choose entities and mode\n\n```python\nk = Kavach(entities=[\"AADHAAR\", \"PAN\", \"UPI\"], mode=\"mask\")\n```\n\n| Mode | `session.mask()` returns | `session.unmask()` |\n| --- | --- | --- |\n| `mask` (default) | Text with placeholders | Restores the original values |\n| `redact` | Text with placeholders | Cannot restore: no mapping is kept |\n| `scan` | The text unchanged | Nothing to restore; use `session.counts` |\n\nSwitching mode is a config change, not a code change. `k.redact(text)` always redacts, whatever\nthe mode.\n\n### Sessions\n\nUse one session per conversation. Within a session the same value always gets the same\nplaceholder, across messages and formatting variants (`2345 6789 0123` and `234567890123` are\nboth `[AADHAAR_1]`). The mapping lives only in the session's memory: it is never logged, written\nto disk or shown in `repr()`.\n\n```python\nsession = k.session()\nsession.mask(\"PAN ABCPE1234F\")  # 'PAN [PAN_1]'\nsession.mask(\"Is ABCPE1234F verified?\")  # 'Is [PAN_1] verified?'\nsession.counts  # {'PAN': 2}, safe to log\nsession.clear()  # forget everything\n```\n\nTell the model to keep placeholders intact:\n\n```python\nfrom kavach import SYSTEM_PROMPT  # \"Keep every placeholder exactly as written ...\"\n```\n\n`unmask()` tolerates small edits such as `[pan_1]` or `[ PAN_1 ]`, and leaves placeholders it\ndoes not know unchanged rather than guessing.\n\n### Streaming\n\nLLM responses arrive in chunks, and a placeholder can be split across them (`[PA` + `N_1]`).\nThe stream unmasker holds back a possible partial placeholder until it is complete:\n\n```python\nstream = session.stream()\nfor chunk in llm_stream(safe):\n    print(stream.feed(chunk), end=\"\")\nprint(stream.flush())\n```\n\n### Scan only\n\n```python\nfor d in Kavach().scan(text):\n    print(d.entity_type, d.start, d.end, d.score)  # d.text_in(text) gives the value\n```\n\n### Command line\n\n```bash\nkavach scan chat.txt                  # types, line/column and confidence; never the values\nkavach scan --json *.txt              # machine-readable\nkavach scan --entities PAN,UPI -      # read from stdin\nkavach redact chat.txt -o safe.txt    # replace PII with placeholders\n```\n\n`kavach scan` exits with 1 when it finds anything, so it works as a pre-commit or CI check.\nThere is no `mask` command: restoring later would require saving the mapping to disk, which\nKavach never does. Use the Python API for reversible masking.\n\n### MCP server\n\n`kavach-mcp` lets an AI assistant (Claude Desktop, Claude Code, Cursor and other\n[MCP](https://modelcontextprotocol.io/) clients) check and redact files without reading the\npersonal data in them.\n\n```bash\npip install \"kavach-pii[mcp]\"\n```\n\nThe extra installs the official MCP Python SDK; plain `pip install kavach-pii` stays\ndependency-free. The server runs locally over stdio and makes no network calls.\n\n| Tool | What the assistant gets back |\n| --- | --- |\n| `scan_file(path)` | Types, line and column numbers and counts. Never the values |\n| `redact_file(path, output_path?, overwrite?)` | Writes `<name>.redacted.<ext>` with placeholders; returns its path and counts |\n| `scan_text(text)` | Types, positions and counts |\n| `redact_text(text)` | The text with placeholders, for sending on to a message, email, ticket or log |\n\nAsk it to \"summarise customers.csv using Kavach\" and it scans the file, writes\n`customers.redacted.csv` and reads only that copy, so the real values never enter the\nconversation. The server's instructions tell the assistant to work this way.\n\n**Connect it.** `--root` sets the folders the file tools may use (default: the current folder);\nrepeat it for more than one.\n\n- Claude Code: `claude mcp add kavach -- kavach-mcp --root /path/to/data`, or a `.mcp.json` in the\n  project:\n\n  ```json\n  {\n    \"mcpServers\": {\n      \"kavach\": { \"command\": \"kavach-mcp\", \"args\": [\"--root\", \"/path/to/data\"] }\n    }\n  }\n  ```\n\n- Claude Desktop: add the same `\"kavach\"` entry under `mcpServers` in\n  `claude_desktop_config.json`. On Windows, give the full path to `kavach-mcp.exe` (or to\n  `python.exe` with `\"args\": [\"-m\", \"kavach.mcp_server\", \"--root\", ...]`).\n- Without installing: `uvx --from \"kavach-pii[mcp]\" kavach-mcp --root /path/to/data`.\n\n`--entities` and `--threshold` work as in the command-line tool.\n\n**Safety.** The file tools refuse paths outside `--root`, including through `..` or symlinks.\nThey never overwrite the input file, and replace an existing redacted copy only with\n`overwrite=true`. Files must be UTF-8 text, up to 10 MB. Redaction keeps no mapping, so nothing\ncan be restored through the server.\n\n**Limits.** Any text the assistant passes to `scan_text` or `redact_text`, it has already read;\nthose tools protect where the text goes next, not the conversation itself. Kavach also cannot\nstop an assistant from opening the original file with its own file tools. To rule that out in\nClaude Code, keep the originals in a folder it may not read and write the redacted copies\nelsewhere, for example in `.claude/settings.json`:\n\n```json\n{ \"permissions\": { \"deny\": [\"Read(./data/**)\"] } }\n```\n\nwith `redact_file(path=\"data/customers.csv\", output_path=\"safe/customers.csv\")`.\n\n## Accuracy\n\nMeasured on the **holdout split** of Kavach's\n[public synthetic benchmark](https://github.com/harshita1615/kavach-pii/tree/main/benchmark): 600\nsentences in English, Hinglish and Hindi, 581 labelled values, and 141 sentences with no PII but\nplenty of look-alikes (order IDs, UTRs, PNRs, timestamps). The holdout templates were written after\nKavach was tuned and have never been used to tune it.\n\n| Entity | Precision | Recall | Masked¹ |\n| --- | ---: | ---: | ---: |\n| Aadhaar | 94.9% | 100.0% | 100.0% |\n| PAN | 100.0% | 100.0% | 100.0% |\n| UPI ID | 100.0% | 93.6% | 93.6% |\n| IFSC | 100.0% | 100.0% | 100.0% |\n| Bank account | 100.0% | 83.7% | 84.5% |\n| Mobile number | 74.6% | 100.0% | 100.0% |\n| GSTIN | 100.0% | 100.0% | 100.0% |\n| Email | 100.0% | 100.0% | 100.0% |\n\n¹ The value was hidden by a detection of any type, so it would not reach the LLM.\n\nCompared with Microsoft Presidio 2.2.364 on the same data:\n\n| | Kavach | Presidio, default | Presidio + its Indian recognizers |\n| --- | ---: | ---: | ---: |\n| Values masked (all types) | **95.7%** | 55.9% | 79.3% |\n| Clean sentences wrongly flagged | **32 / 141** | 44 / 141 | 65 / 141 |\n| Latency, ms per 1,000 chars² | **0.8** | 93 | 92 |\n\n² On one Windows laptop, Python 3.14. Presidio's time is mostly spaCy NER, which also finds names\nand places that Kavach does not.\n\nWhere Kavach is weaker:\n\n- **Phone false alarms.** A bare 10-digit number starting 6-9 (a coupon code or an order number\n  with no label) looks exactly like a mobile number. Kavach errs towards masking.\n- **Bank accounts without a keyword.** Kavach needs a context word nearby (\"account\", \"a/c\",\n  \"khata\"), so a line like \"Copying from my passbook: 50100…\" is missed. Presidio masks more bank\n  numbers here (91.5% vs 84.5%), mostly as other entity types.\n- **UPI IDs with handles Kavach does not know**, when no \"UPI\" keyword is nearby.\n\nBrackets-intact round trips (mask, then echo, reorder, change case or spacing, or stream in\nchunks, then unmask) restored every value in all 467 holdout sentences Kavach masked. If the LLM\ndrops the brackets (`PAN_1`), nothing is restored.\n\nFull tables, the tuning (\"dev\") split, and how to reproduce everything:\n[benchmark/README.md](https://github.com/harshita1615/kavach-pii/blob/main/benchmark/README.md).\nResults are written by `python -m benchmark.run_eval`\nand never edited by hand.\n\n## Privacy principles\n\n- Everything runs in-process. Kavach makes no network calls.\n- Raw values are never logged, printed, stored or included in errors. A test fails the build if\n  the package starts logging or printing.\n- Audit output is limited to entity types and counts.\n- All test and benchmark data is synthetic. Never file an issue with real personal data; use\n  made-up values in the same format.\n\n## Limitations\n\nKavach reduces the chance of PII reaching an LLM; it cannot guarantee that none will. It is a\nrisk-reduction tool, not legal advice, and using it does not by itself make an application\ncompliant with the DPDP Act or any other law.\n\nKnown gaps today:\n\n- Names, addresses and free-text personal details are not detected (planned for v2).\n- Landline numbers and special GSTIN formats (TDS/TCS, non-resident) are not covered.\n- IDs written with Devanagari digits (२३४५…) are not detected; only ASCII digits are.\n- About 1 in 10 random 12-digit numbers starting 2-9 pass the Aadhaar checksum, so some\n  unlabelled IDs may be masked as Aadhaar. A label right before the number (\"Order\", \"UTR\",\n  \"PNR\", \"Invoice\") lowers the score.\n- Numbers can get the wrong placeholder type (for example a phone number as\n  `[BANK_ACCOUNT_1]`), though the value is still masked.\n- If the LLM rewrites a placeholder beyond small case and spacing changes, it cannot be\n  restored.\n\n## Development\n\n```bash\npython -m venv .venv\n.venv\\Scripts\\activate          # Windows; use source .venv/bin/activate elsewhere\npip install -e \".[dev]\"\npytest\nruff check . && ruff format --check .\n```\n\n## Roadmap\n\n1. **Python package** (this repo): core detection and masking ✅, benchmark ✅, PyPI release ✅\n2. **Chrome extension** ([extension/](https://github.com/harshita1615/kavach-pii/tree/main/extension)):\n   warns before PII is pasted or sent into ChatGPT, Claude or Gemini; runs fully on-device.\n   Built and tested; in Chrome Web Store review\n3. **Integrations**: [MCP server](#mcp-server) ✅, [website](https://kavach.bigforge.tech/) ✅,\n   [web demo](https://kavach.bigforge.tech/demo/) ✅; next: OpenAI-compatible proxy, Presidio\n   recognizer pack, n8n node\n\n## Related work\n\n- [Microsoft Presidio](https://github.com/microsoft/presidio): general PII detection and\n  anonymization. It ships recognizers for Aadhaar, PAN, GSTIN, Indian passport, voter ID and\n  vehicle registration, though they are not enabled in its default English setup (as of\n  2.2.364). Kavach aims to complement it, and a Presidio recognizer pack is planned.\n\n## License\n\n[MIT](https://github.com/harshita1615/kavach-pii/blob/main/LICENSE) © 2026 Harshita Chaudhary\n"}