Skip to content

All posts

AI

How to stop PII and national IDs from reaching LLMs

Ritesh Patel

AI: How to stop PII and national IDs from reaching LLMs

To keep PII and national IDs out of an LLM, check each prompt and tool result before it leaves your network. Use a regex with a checksum for structured IDs like Aadhaar, a named-entity model for names, then redact, hold or block each match. No detector finds everything, so also limit which files an agent can read.

Where PII reaches the model

A coding agent sends text to the model from four places. A developer types a prompt or pastes text into one. An agent reads a file, such as a CSV export or a .env file, and the contents come back as a tool result. An MCP server or a shell command returns records from a database or an API. Each path needs its own control, because a check on one path does not cover the others.

Claude Code has a hook at each point. UserPromptSubmit runs before a typed prompt is processed, and pasted text arrives inside that same prompt. PreToolUse runs before a tool call, and PostToolUse can replace a tool’s output before Claude sees it. A gateway is the one place where a single rule can cover every tool, as long as all model traffic is routed through it.

Compare the detection methods

Each method finds a different kind of PII and fails in a different way. For national IDs, use a checksum. For names and addresses, use a model. For a policy that covers every tool and every developer, use the gateway.

Method Good at Misses Example from Presidio
Regex only Fixed shapes, such as a 12-digit number Any other number of the same shape, valid or not The IN_AADHAAR pattern alone scores 0.01
Regex plus checksum Separating real IDs from random digits IDs in a format the pattern does not cover, and about 1 in 10 random 12-digit strings still pass Verhoeff check digit and palindrome rule for Aadhaar
Named-entity model (NER) Names and locations in running text Short text, unusual names, and fixed-format IDs PERSON and LOCATION from a spaCy model
Gateway policy One rule for every tool, with hold, redact or block Traffic that skips the gateway, and detector coverage that varies by product Managed settings can pin ANTHROPIC_BASE_URL

Detect Aadhaar with Presidio

Presidio is an open-source library for detecting and anonymizing PII. It has a predefined recognizer for Aadhaar, InAadhaarRecognizer, which returns the entity IN_AADHAAR. According to its source, the pattern takes 12 digits, optionally grouped in fours with a space, dash or colon. A match counts only if the first digit is 2 or higher, the Verhoeff check digit is valid, and the number is not a palindrome. The base pattern scores 0.01. The recognizer’s tests expect a number that passes the checks to score 1, and in our run with Presidio 2.2.366, a number that passed the checks was returned with a score of 1.0.

The default config sets this recognizer to enabled: false, so it does not run until you add it. The docs’ country filter, countries=["in"], does not turn it on. load_predefined_recognizers passes a config with no recognizers key, so the loader falls back to the default file (recognizers_loader_utils.py, the defaults branch near line 789), and the loader skips any entry with enabled: false (line 412). In our test with Presidio 2.2.366, the filter loaded no Aadhaar recognizer, and adding InAadhaarRecognizer explicitly found the number.

A valid checksum is not a complete filter. We ran the same check on 100,000 random 12-digit numbers with a first digit from 2 to 9, and about 10% passed. Order numbers and reference codes of that length will still match, so route those matches to review and not straight to a block.

Set up a detection pass with Presidio

These steps were checked against Presidio 2.2.366, the analyzer and anonymizer version in the repository at commit 17d1946, fetched 2026-10-29. Its CHANGELOG.md dates that release 2026-10-27. The output below was re-run with the en_core_web_lg model, which the installation docs use by default.

  1. Install the two packages and the default English model. Presidio supports Python 3.11 to 3.14.
pip install presidio_analyzer presidio_anonymizer
python -m spacy download en_core_web_lg
  1. Add the Aadhaar recognizer, then analyze and redact the text. Use numbers that pass the checksum, but are not real IDs.
from presidio_analyzer import AnalyzerEngine, RecognizerRegistry
from presidio_analyzer.predefined_recognizers import InAadhaarRecognizer
from presidio_anonymizer import AnonymizerEngine
from presidio_anonymizer.entities import OperatorConfig

registry = RecognizerRegistry()
registry.load_predefined_recognizers(languages=["en"])
# IN_AADHAAR is enabled: false in the default config, so add it explicitly.
registry.add_recognizer(InAadhaarRecognizer())

analyzer = AnalyzerEngine(registry=registry)
anonymizer = AnonymizerEngine()

text = "Asha Rao's Aadhaar is 234567890124. Call her on 212-555-5555."
results = analyzer.analyze(
    text=text,
    entities=["PERSON", "IN_AADHAAR", "PHONE_NUMBER"],
    language="en",
)
redacted = anonymizer.anonymize(
    text=text,
    analyzer_results=results,
    operators={"DEFAULT": OperatorConfig("replace")},
)
print(redacted.text)
# Output from Presidio 2.2.366 with en_core_web_lg:
# <PERSON> Aadhaar is <IN_AADHAAR>. Call her on <PHONE_NUMBER>.
  1. Test both sides of the check. A number that passes should be replaced, and a near miss should be left alone. In our run, 234567890124 was replaced, and 1234 5678 9012 was not because its first digit is below 2. The recognizer’s own validate_result returns True for 234567890124, so it passes the Verhoeff check. Keep the test set in version control so you can rerun it after every upgrade.

Redact inside Claude Code with hooks

Hooks can block a prompt, and they can rewrite tool arguments and tool results. They cannot rewrite a typed prompt. The steps below follow the Claude Code hooks reference, checked as of 2026-10-29. The hooks reference says onFailure requires Claude Code v2.1.295 or later. For the blocking pattern on destructive commands, see how to block destructive commands in Claude Code.

  1. Block a matching prompt. The hook prints JSON with decision set to "block". Set suppressOriginalPrompt to true so the prompt text does not appear in the block message. A blocked prompt does not reach the model.
{
  "decision": "block",
  "reason": "This prompt contains a national ID. Remove it and try again.",
  "hookSpecificOutput": {
    "hookEventName": "UserPromptSubmit",
    "suppressOriginalPrompt": true
  }
}
  1. Fail closed. Command hooks on UserPromptSubmit default to a 30-second timeout. A hook that times out is cancelled, its output is discarded, and the prompt still goes to Claude. Set onFailure to "block" so a timeout blocks the prompt instead.
{
  "hooks": {
    "UserPromptSubmit": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "\"${CLAUDE_PROJECT_DIR}/.claude/hooks/pii-check.sh\"",
            "timeout": 10,
            "onFailure": "block"
          }
        ]
      }
    ]
  }
}
  1. Redact tool results. Use PostToolUse with updatedToolOutput to replace what Claude sees. The value must match the tool’s output shape. The tool has already run by then, so any file writes or network calls have happened. The hooks reference says telemetry, such as OpenTelemetry tool spans, captures the original output before the hook runs.

  2. Redact outbound tool inputs. Use PreToolUse with updatedInput under hookSpecificOutput to change a tool’s arguments before it runs.

A blocked prompt’s text can still be kept on disk, in the session transcript and your prompt history. suppressOriginalPrompt only changes the block message, so a blocking hook does not keep a secret off the disk.

Route all model traffic through a gateway

Claude Code sends model requests to the URL in ANTHROPIC_BASE_URL. Point that variable at a gateway, and every model request Claude Code makes goes through it. Requests that a script or MCP server makes with its own configuration do not use that variable, so they do not pass through the gateway. The gateway docs list credentials, usage tracking, cost controls and audit logging as gateway functions. They do not list redaction, so check whether a gateway can hold, redact or block PII before you rely on it. For the routing setup, see how to route Claude Code through an AI gateway.

A developer can still point Claude Code at their own proxy unless you stop it. Managed settings can do that. Setting allowedProviders to ["customEndpoint"] and putting the gateway URL in the env block makes Claude Code refuse any session pointed elsewhere. The docs say this needs Claude Code v2.1.285 or later.

{
  "allowedProviders": ["customEndpoint"],
  "env": {
    "ANTHROPIC_BASE_URL": "https://ai-gateway.example.com"
  }
}

Decision checklist

Work through these before you turn on blocking. A false positive stops a developer’s work, so start with redaction and review, then move to blocking once the match rate is known.

  • List every path: typed prompts, pasted text, file reads, MCP results and shell output.
  • For each ID type, use a checksum-backed recognizer where one exists, and a model for names.
  • Choose the action per match: redact, hold for approval, or block.
  • Set the hook’s failure mode. Use onFailure: "block" wherever a timeout must not let data through.
  • Set suppressOriginalPrompt and find out which local files still keep prompt text.
  • Pin ANTHROPIC_BASE_URL in managed settings so traffic goes through the gateway.
  • Test with synthetic numbers that pass the checks, and with near misses that fail them.
  • Log the decision and the entity type, not the matched text. For the audit side, see how to audit MCP tool calls.

Fine print that breaks these controls

  • The Presidio README says detection is not guaranteed and that other protections are needed.
  • IN_AADHAAR is disabled in the default config, and the documented country filter does not enable it.
  • About 1 in 10 random 12-digit numbers passes the Verhoeff check, so checksum matches still need review.
  • The entity table describes IN_PAN as 12 characters, but the recognizer’s docstring says ten. Test PAN on your own samples before you rely on it.
  • The Presidio hash operator uses a random salt by default since version 2.2.361 (anonymizer docs). The same value gets a different hash each time unless you pass a fixed salt.
  • A UserPromptSubmit hook that times out lets the prompt through unless onFailure is set to "block".
  • PostToolUse redaction happens after the tool runs, and the hooks reference says telemetry captures the original output first.

Enforcing this with AIControls

AIControls from Nirmata supports Claude Code through its gateway, for both the CLI and VS Code. For PII, it holds a matching request for approval, or it redacts or blocks the request. It also provides per-user tokens and per-user and per-session cost and budget limits. Ask any vendor, including us, which ID types its detectors cover and how those detectors are tested, and run the synthetic-number test above against it before you rely on the answer. For the wider control set, see governance for AI coding agents and AIControls.

Frequently asked questions

Can a Claude Code hook redact PII from a prompt before the model sees it?

Not by rewriting the prompt. The hooks reference says UserPromptSubmit can block a prompt or add context, but it cannot replace the prompt text. A blocking hook keeps the prompt from the model, but the text can still sit in the session transcript and prompt history on disk. To redact tool inputs or tool results, use PreToolUse with updatedInput or PostToolUse with updatedToolOutput.

Does Presidio detect Aadhaar numbers out of the box?

Presidio has a recognizer for them, InAadhaarRecognizer, which returns IN_AADHAAR. Its entry in the default recognizer config is set to enabled: false, so you have to add it yourself. In our test with Presidio 2.2.366, the documented country filter did not load it, but adding the recognizer explicitly did. The recognizer checks 12 digits, the first digit, a Verhoeff checksum and palindromes.

Is a regex enough to find national IDs in prompts?

A regex finds the shape of a number, not whether it is an ID. A checksum removes most random digit strings, but not all. In our run of 100,000 random 12-digit numbers, about 10% passed the Aadhaar check, so order numbers and reference codes still match. Add review for those matches, and use a named-entity model for names, which a pattern cannot find.

Does a Claude Code gateway see PII before the model provider does?

Yes, for traffic that goes through it. With ANTHROPIC_BASE_URL set to a gateway, each model request Claude Code makes passes through that gateway first. The gateway docs list credentials, usage tracking, cost controls and audit logging as its functions. They do not list redaction, so ask any gateway vendor whether it can hold, redact or block a request by content.

Ritesh Patel, Founder & VP Product, Nirmata

Ritesh Patel is a founder of Nirmata and leads its products. He writes about governing AI coding agents, MCP security and Kubernetes policy as code.

LinkedIn

Govern AI agents with AIControlsControl what AI agents do, and prove it.
ShareXLinkedIn

Stay up-to-date with the latest in AI, governance, and infrastructure

Subscribe to our free monthly newsletter to get industry news, DevOps tips, and articles right to your inbox on Kyverno, containerization, Kubernetes deployment & management and more.