DEV Community

Cover image for I built a zero-dependency PII scanner for AI prompts in 270 lines of Python
Anil Prasad
Anil Prasad

Posted on • Originally published at open.substack.com

I built a zero-dependency PII scanner for AI prompts in 270 lines of Python

TL;DR — AgentMesh 0.3.2 ships a PII/PHI/PCI scanner that runs on every AI prompt before it reaches the model. 17 entity types. Under 2ms. No external API. No cloud service. Pure Python regex with Luhn validation and overlap deduplication. Three enforcement modes: mask, redact, block. pip install agentmesh-proxy

The problem

Your AI agents and tools are sending raw sensitive data to the LLM vendor.

Medical record numbers in clinical AI prompts. Credit card numbers in finance team workflows. AWS access keys in developer debug pastes. Social security numbers in HR automation.

The people doing this are not making bad decisions. They are using the tools available to them. The problem is that there is no layer between the prompt and the model that catches sensitive data first.

I built that layer into AgentMesh. Here is how it works.


What it catches

![17 entity types caught before the LLM]

17 entity types across four categories:

PII — personal identity

  • SSN: 567-89-0123[SSN]
  • Date of birth: 07/22/1985[DOB]
  • Email: sarah.johnson@gmail.com[EMAIL]
  • Phone: (415) 867-5309[PHONE_US]
  • Passport: Passport no: US123456789[PASSPORT]

PCI — payment card and financial data

  • Visa: 4532 1234 5678 9012[PCI_CARD]
  • Amex: 3714 496353 98431[PCI_CARD]
  • Mastercard: 5500 0055 0000 0004[PCI_CARD]
  • CVV: CVV 394[PCI_CVV]
  • Routing: Routing: 021000021[PCI_ROUTING]
  • Account: Account: 000123456789[PCI_ACCOUNT]

PHI — HIPAA-protected medical data

  • Medical record: MRN: P-987654[PHI_MRN]
  • ICD-10 diagnosis: E11.9[PHI_ICD10]
  • Medication dosage: 10mg lisinopril[PHI_DOSAGE]
  • Provider ID: NPI: 1234567890[PHI_NPI]

CII — cloud credentials and infrastructure

  • AWS key: AKIAIOSFODNN7EXAMPLE[CII_AWS_KEY]
  • JWT token: eyJhbGci...[CII_JWT]

Quick start

pip install agentmesh-proxy
Enter fullscreen mode Exit fullscreen mode
from agentmesh.security.pii_scanner import PIIScanner, ScanMode

scanner = PIIScanner(mode=ScanMode.MASK)

result = scanner.scan(
    "Patient MRN: P-987654, email: sarah@example.com, "
    "card: 4532 1234 5678 9012, key: AKIAIOSFODNN7EXAMPLE"
)

print(result.cleaned)
# Patient MRN: [PHI_MRN], email: [EMAIL],
# card: [PCI_CARD], key: [CII_AWS_KEY]

print(result.finding_types)
# ['CII_AWS_KEY', 'EMAIL', 'PCI_CARD', 'PHI_MRN']
Enter fullscreen mode Exit fullscreen mode

For scanning a list of {role, content} messages (OpenAI format):

messages = [
    {"role": "user", "content": "SSN 123-45-6789, card 4532 1234 5678 9012"}
]

cleaned_messages, findings = scanner.scan_messages(messages)
print(cleaned_messages[0]["content"])
# SSN [SSN], card [PCI_CARD]
Enter fullscreen mode Exit fullscreen mode

Three enforcement modes

from agentmesh.security.pii_scanner import PIIScanner, ScanMode, PIIDetectedError

# MASK: replace with labeled placeholder — model still gets a useful prompt
scanner = PIIScanner(mode=ScanMode.MASK)
result = scanner.scan("SSN 123-45-6789")
print(result.cleaned)  # "SSN [SSN]"

# REDACT: replace with *** — when even the label is too much context
scanner = PIIScanner(mode=ScanMode.REDACT)
result = scanner.scan("SSN 123-45-6789")
print(result.cleaned)  # "SSN ***"

# BLOCK: raise PIIDetectedError — zero tolerance, reject the request
scanner = PIIScanner(mode=ScanMode.BLOCK)
try:
    scanner.scan("SSN 123-45-6789")
except PIIDetectedError as e:
    print(e.findings)  # [Finding(entity_type='SSN', ...)]
    # Return HTTP 400 to the caller
Enter fullscreen mode Exit fullscreen mode

Engineering decisions worth explaining

Why regex over an NLP model?

Speed. The scan runs in under 2ms. An NLP-based entity recognizer adds 50ms to 200ms per call and requires a model download. For a proxy that sits in the path of every LLM call, 2ms is acceptable and 200ms is not.

The tradeoff is recall. Regex will miss creative obfuscation. For governance purposes — where the goal is catching accidental leakage, not adversarial attacks — regex is the right tool.

The credit card validation decision

Standard implementations run Luhn validation on card numbers and only mask numbers that pass. We run in strict_pci=True mode by default:

# In PIIScanner.__init__:
# strict_pci=True (default): mask any card-shaped number (13-19 digits)
# even if it fails the Luhn check.
# Rationale: governance proxies should over-mask rather than under-mask.
# A false positive costs one masked token.
# A false negative sends a real card number to the vendor.
self.strict_pci = strict_pci
Enter fullscreen mode Exit fullscreen mode

If you prefer Luhn validation only:

scanner = PIIScanner(mode=ScanMode.MASK, strict_pci=False)
Enter fullscreen mode Exit fullscreen mode

The overlap deduplication problem

This one took a few iterations to get right.

Consider a prompt containing MRN: A1234567. The PHI_MRN pattern matches the whole span. The PASSPORT pattern (before it required a passport: prefix) would also match the A1234567 part.

If you apply replacements in reverse order by start position — which is the standard approach to keep earlier offsets valid — and the inner match gets processed first, it replaces 8 characters with 10 ([PASSPORT]). The outer match then tries to cut at the original end offset, which now points into the middle of [PASSPORT], producing [PHI_MRN]T].

The fix:

def _dedup_overlapping(findings: List[Finding]) -> List[Finding]:
    # Sort by start position, then by length descending (outermost first).
    # Walk forward and drop any finding whose start is inside the
    # previous kept finding's range.
    sorted_f = sorted(findings, key=lambda f: (f.start, -(f.end - f.start)))
    result: List[Finding] = []
    last_end = -1
    for f in sorted_f:
        if f.start >= last_end:
            result.append(f)
            last_end = f.end
    return result
Enter fullscreen mode Exit fullscreen mode

Keep the outermost match. Drop everything whose start position falls inside it. Apply replacements in reverse order on the deduplicated list. No artifacts.


Wiring it into the proxy

If you are running AgentMesh as a proxy rather than calling the scanner directly, activate it in config:

# agentmesh.yaml
pii_mode: mask           # mask | redact | block
block_injections: true   # prompt injection detection (14 rules)
anomaly_detection: true  # runaway loop + burn rate monitoring
slack_webhook: ""        # optional: alert destination
Enter fullscreen mode Exit fullscreen mode
agentmesh serve --config agentmesh.yaml --port 8080
Enter fullscreen mode Exit fullscreen mode

Point your agents at it:

export OPENAI_BASE_URL=http://localhost:8080/v1
export ANTHROPIC_BASE_URL=http://localhost:8080
Enter fullscreen mode Exit fullscreen mode

Every call going through the proxy now gets scanned. The response includes a header showing what was found:

X-AgentMesh-PII-Findings: 4
X-AgentMesh-Cache: miss
X-AgentMesh-Cost-USD: 0.000420
Enter fullscreen mode Exit fullscreen mode

The Chrome extension

A server-side proxy cannot intercept prompts typed directly into the ChatGPT or Claude.ai browser tab. For that there is a Chrome extension — same scanner, running locally in the browser process before the request leaves the tab.

Google approved it last weekend.

Install from the Chrome Web Store (link in the repo readme) or build from source. Works with ChatGPT, Claude.ai, Gemini, Perplexity, and Cursor. No server required for standalone use.


HIPAA in production

If your team uses AI in a clinical setting, the PHI scanner is the piece that matters most. ICD-10 codes are two to five characters but identify specific diagnoses. Combined with a medical record number and a provider NPI, they reconstruct a patient record from a prompt.

AgentMesh also generates HIPAA readiness reports:

from agentmesh.compliance.pdf_report import ComplianceReporter, Framework

reporter = ComplianceReporter()
markdown = reporter.generate_markdown(Framework.HIPAA)
# or
reporter.generate_pdf(Framework.HIPAA, output_path="hipaa_report.pdf")
Enter fullscreen mode Exit fullscreen mode

Outputs a structured report listing which controls are active, which are not, and what gaps remain. Useful for security reviews before a compliance audit.


Try it

pip install agentmesh-proxy
Enter fullscreen mode Exit fullscreen mode
from agentmesh.security.pii_scanner import PIIScanner, ScanMode

scanner = PIIScanner(mode=ScanMode.MASK)
result = scanner.scan("your prompt here")
print(result.cleaned)
print(result.finding_types)
Enter fullscreen mode Exit fullscreen mode

The scanner is in agentmesh/security/pii_scanner.py. About 270 lines. No external dependencies beyond the Python standard library.

Repo: https://github.com/anilatambharii/agentmesh
PyPI: agentmesh-proxy
Docker: docker pull anilsprasad/agentmesh:latest
Apache 2.0.

What entity types would you add? What patterns are you seeing in your team's prompts that are not covered here?


Find me: anilsprasad.com · X @anilsprasad · LinkedIn

Top comments (0)