← Back to list

Building GDPR-Compliant AI Agents: I Built a PII Detection Engine That Actually Validates What It…

The problem: Your AI customer support agent just forwarded a user’s Social Security Number, credit card, and email address to an LLM API…

Sheeban Wasi · 2026-02-18 00:23 · 2 claps · 16.5 min read
#security #ai-agent-security #aws #aws-strands #strands-agents
Open on Medium ↗
Wiki topics: LLM · Large Language Models AGT · AI Agents ☁️ · DevOps & Cloud 🔒 · Cybersecurity

Building GDPR-Compliant AI Agents: I Built a PII Detection Engine That Actually Validates What It Finds

The problem: Your AI customer support agent just forwarded a user’s Social Security Number, credit card, and email address to an LLM API without sanitization. GDPR Article 33 gives you 72 hours to notify every affected individual. Your agent processed 14,000 conversations today. This is not theoretical — this is the compliance gap that keeps CISOs up at night.

I spent weeks thinking about this problem while building Agent-Warden, an open-source security framework for AI agents. What started as a SQL injection guard (covered in my previous article) kept pulling me deeper: if I could structurally validate SQL before it hits a database, could I do the same for personal data before it hits an LLM API?

Turns out, the answer required rethinking what I needed from PII detection. There are good tools out there — commercial APIs, open-source libraries like Presidio, regex-based scanners. But I kept running into the same gap: most PII solutions focus on detection. They tell you “there’s an email here.” What I needed was a system that detects, validates, scores confidence, and then takes the right action for the context. It’s a validation problem, a scoring problem, and a strategy problem. You need to know not just what you found, but how confident you are, and what to do about it depending on whether you’re in a HIPAA environment or an analytics pipeline.

Agent-Warden integrates with AWS Strands, with a LangChain adapter in development, and works with any Python-based agentic framework. Here’s how I built a PII engine designed specifically for the agentic workflow — and what I learned along the way.

The Wake-Up Call

Here’s the moment that changed my approach. I was reviewing a customer support chatbot built on an agentic framework. The team had spent real engineering time on a “PII safety layer” that looked like this:

# DON'T DO THIS
pii_keywords = ['ssn', 'social security', 'credit card', '@']
if any(keyword in message.lower() for keyword in pii_keywords):
    raise SecurityError("PII detected")

This feels safe. It’s not.

I tested it with a typical support ticket:

“My account is linked to john.smith@company.com, my number is 123–45–6789, and the card ending in 4111–1111–1111–1111 isn’t working.”

The customer never typed “SSN” or “credit card.” They just wrote their data. The keyword filter saw nothing.

Then I tested it with a completely innocent message:

“Check the docs@ section for the API reference”

Blocked. Because the string contained “@”.

That’s when I realized: keyword matching for PII is like checking someone’s identity by asking “are you a spy?”

The Fundamental Problem With Naive PII Detection

PII isn’t a keyword — it’s structured data with rules. When you search for PII as text, you’re asking the wrong question.

Wrong question: “Does this text contain something that looks like an email?” Right question: “Does this text contain validated PII, and what’s the right way to handle it for this context?”

Here’s why that distinction matters:

False positives shut down your application. A PII detector that blocks every 10-digit number will reject order IDs, timestamps, and zip code combinations. Your support team will disable the filter within a week.

Not all patterns are PII. The number 4111111111111112 has 16 digits and starts with 4 — looks like a Visa card. But it fails the Luhn checksum. It’s not a real card number. Similarly, 000–45–6789 looks like a Social Security Number, but SSNs never start with 000. A naive regex can’t tell the difference.

Detection without strategy is useless. Sometimes you need to block the request entirely (HIPAA). Sometimes you need to redact the PII and continue (sending prompts to an LLM). Sometimes you need a deterministic hash (analytics). One mode doesn’t fit all.

Both directions matter. PII can exist in what you send TO the LLM and in what comes BACK. A user might paste their SSN. The LLM might hallucinate one in its response. You need to inspect both.

Enter Structured PII Detection

I needed a system that does four things: detect, validate, score, then act. Not just pattern matching — a pipeline.

First, define what you’re looking for:

class PIIType(Enum):
    """Types of PII that can be detected."""
    EMAIL = "email"
    PHONE = "phone"
    SSN = "ssn"
    CREDIT_CARD = "credit_card"
    IP_ADDRESS = "ip_address"
    DATE_OF_BIRTH = "date_of_birth"
    PASSPORT = "passport"
    DRIVER_LICENSE = "driver_license"
    BANK_ACCOUNT = "bank_account"
    CUSTOM = "custom"

Then, define what to do about it:

class PIIStrategy(Enum):
    """How to handle detected PII."""
    BLOCK = "block"       # Block the request entirely
    REDACT = "redact"     # Replace with [TYPE REDACTED]
    MASK = "mask"         # Show last 4 characters only
    HASH = "hash"         # Replace with deterministic hash
    MONITOR = "monitor"   # Log only, don't block or modify

Five strategies. Because a healthcare app and an analytics pipeline have very different compliance needs.

Building the PII Inspector

Here’s the core implementation from Agent-Warden (https://github.com/Sheeban-Wasi/agent-warden). The interesting parts aren’t the regex patterns — it’s what happens after the match.

Validation That Goes Beyond Pattern Matching

Take Social Security Numbers. The regex alone isn’t enough:

# SSN: xxx-xx-xxxx format
PIIType.SSN: re.compile(
    r"\b(?!000|666|9\d{2})\d{3}"  # Area (not 000, 666, or 9xx)
    r"[-\s]?"
    r"(?!00)\d{2}"                 # Group (not 00)
    r"[-\s]?"
    r"(?!0000)\d{4}\b",            # Serial (not 0000)
),

The Social Security Administration never issues numbers starting with 000, 666, or 900–999. A naive regex would flag 000–12–3456 as an SSN. This pattern rejects it at the regex level. That’s the difference between “matches a pattern” and “could be real.”

The Luhn Algorithm: Why Regex Can’t Validate Credit Cards

This is where things get interesting. A regex can tell you that 4111111111111112 has 16 digits starting with 4. The Luhn algorithm tells you it’s not a real credit card number.

Every legitimate credit card number passes the Luhn check. It’s the same algorithm that payment processors use. Without it, your PII detector is just counting digits.

Confidence Scoring: Not All Matches Are Equal

Different PII types have different false positive rates. Phone numbers are the worst offenders — any 10-digit number could be a phone number or an order ID. So I score each match:

def _calculate_confidence(self, pii_type: PIIType, value: str) -> float:
    if pii_type == PIIType.CREDIT_CARD:
        if self._luhn_check(re.sub(r"[\s-]", "", value)):
            return 0.99  # Valid Luhn = almost certainly a card
        return 0.5       # Looks like a card, but invalid
    elif pii_type == PIIType.SSN:
        return 0.95      # Strong pattern + validation
    elif pii_type == PIIType.EMAIL:
        return 0.99      # Email patterns are distinctive
    elif pii_type == PIIType.PHONE:
        return 0.85      # Higher false positive risk
    elif pii_type == PIIType.IP_ADDRESS:
        return 0.90
    return 0.80

Phone numbers get 0.85 because random 10-digit numbers are common. Emails get 0.99 because the user@domain.tld pattern is distinctive. Credit cards with valid Luhn get 0.99. The configurable min_confidence threshold (default: 0.8) lets you tune sensitivity per deployment.

The Audit Trail That Never Stores PII

Here’s the part compliance teams love:

def to_audit_log(self) -> dict:
    """Convert to audit log format."""
    log = self.verdict.to_audit_log()
    log["pii_findings"] = [
        {
            "type": f.pii_type.value,
            "position": {"start": f.start, "end": f.end},
            "confidence": f.confidence,
        }
        for f in self.findings
    ]
    return log

Notice what’s NOT in the audit log: the actual PII values. We log the type, position, and confidence — never the data itself. This is metadata-only audit trailing. You can prove to an auditor that your system detected and handled an SSN at position 42–53 with 0.95 confidence, without storing the SSN in your logs. That’s how you stay GDPR-compliant even in your compliance infrastructure.

A Note on Development

I built Agent-Warden using modern development practices, including AI-assisted coding with tools like Claude Code. This allowed me to focus on architecture, testing, and developer experience rather than boilerplate. The PII detection engine combines well-established techniques (Luhn algorithm, regex-based pattern matching, confidence scoring) with the innovation of making them composable through a protocol-based architecture. The architectural decisions, testing strategy, and five-strategy system are the result of iterative design and real-world validation.

Testing the Edge Cases

I wrote 80+ test cases trying to break this (see the full test suite on GitHub: https://github.com/Sheeban-Wasi/agent-warden/blob/main/tests/test_pii_inspector.py). Here are the ones that kept me honest:

Test 1: Luhn validation rejects fake card numbers

def test_reject_invalid_luhn(self):
    """Reject card number that fails Luhn check."""
    # 4111111111111112 fails Luhn
    result = inspect_pii("Card: 4111111111111112", strategy="monitor")
    cc_findings = [f for f in result.findings if f.pii_type == PIIType.CREDIT_CARD]
    for f in cc_findings:
        assert f.confidence < 0.8  # Low confidence = filtered out

Test 2: SSN validation rejects impossible numbers

def test_reject_invalid_ssn_000(self):
    """Reject SSN starting with 000."""
    result = inspect_pii("SSN: 000-45-6789", strategy="monitor")
    ssn_findings = [f for f in result.findings if f.pii_type == PIIType.SSN]
    assert len(ssn_findings) == 0  # Invalid SSN not detected
def test_reject_invalid_ssn_666(self):
    """Reject SSN starting with 666."""
    result = inspect_pii("SSN: 666-45-6789", strategy="monitor")
    ssn_findings = [f for f in result.findings if f.pii_type == PIIType.SSN]
    assert len(ssn_findings) == 0  # Invalid SSN not detected

Test 3: Multi-PII redaction in one pass

def test_multiple_types_redacted(self):
    """All PII types are redacted."""
    text = "Email: john@example.com, Phone: 555-123-4567, SSN: 123-45-6789"
    result = inspect_pii(text, strategy="redact")
    assert "[EMAIL REDACTED]" in result.sanitized_text
    assert "[PHONE REDACTED]" in result.sanitized_text
    assert "[SSN REDACTED]" in result.sanitized_text

Test 4: PII hiding in JSON payloads

def test_pii_in_json(self):
    """PII embedded in JSON."""
    text = '{"email": "john@example.com", "phone": "555-123-4567"}'
    result = inspect_pii(text, strategy="redact")
    assert "[EMAIL REDACTED]" in result.sanitized_text
    assert "[PHONE REDACTED]" in result.sanitized_text

Test 5: Custom detectors alongside built-in detection

def test_inspector_custom_and_builtin_detectors(self):
    """Custom detectors work alongside built-in detection."""
    detector = RegexDetector(r"CUST-\d+", label="CUSTOMER_ID")
    inspector = PIIInspector(
        strategy="redact",
        custom_detectors=[detector],
    )
    text = "Customer CUST-999 email: john@example.com"
    result = inspector.inspect(text)
    assert "[CUSTOMER_ID REDACTED]" in result.sanitized_text
    assert "[EMAIL REDACTED]" in result.sanitized_text

The validation layer caught every edge case. Fake credit cards, impossible SSNs, PII buried in JSON — all handled.

Real-World Performance

I benchmarked this against realistic workloads:

  • Clean text (no PII): ~50μs detection time, None found, Pass
  • Single email address: ~80μs detection time, 1 match, Redacted
  • Support ticket (3 PII types): ~150μs detection time, 3 matches, Redacted
  • Long text (13,000 chars): ~200μs detection time, 1 match, Detected
  • JSON payload with PII: ~120μs detection time, 2 matches, Redacted

Average overhead: under 0.2ms per inspection. For context, a typical LLM API call takes 500–2000ms. The PII check adds less than 0.01% to your total latency.

The Five-Strategy System

Different compliance requirements demand different responses. Here’s how each strategy works:

1. BLOCK (strictest)

result = inspect_pii("Email: john@example.com", strategy="block")
assert result.verdict.blocked  # Request rejected entirely

Use case: Zero-tolerance environments. HIPAA-regulated systems where PII must never reach an external API.

2. REDACT

result = inspect_pii(
    "Email: john@example.com, SSN: 123-45-6789",
    strategy="redact",
)
# result.sanitized_text: "Email: [EMAIL REDACTED], SSN: [SSN REDACTED]"

Use case: Sending user messages to LLMs. The model still gets useful context (“there was an email here”) without seeing the actual data.

3. MASK

result = inspect_pii("SSN: 123-45-6789", strategy="mask")
# result.sanitized_text: "SSN: *******6789"

Use case: Customer support interfaces. The agent sees enough to confirm identity (“ending in 6789”) without full exposure.

4. HASH

result = inspect_pii("john@example.com", strategy="hash")
# result.sanitized_text: "[HASH:b4c9a289841f]"
# Same email always produces the same hash

Use case: Analytics and deduplication. You can count unique users, link records across sessions, and track patterns — without ever storing the original PII. This is pseudonymization under GDPR Article 4(5).

Need different hashes per environment? Use a salt:

inspector_prod = PIIInspector(strategy="hash", hash_salt="prod-secret-2026")
inspector_dev  = PIIInspector(strategy="hash", hash_salt="dev-local")
# Same email, different hashes per environment
result_prod = inspector_prod.inspect("john@example.com")
result_dev  = inspector_dev.inspect("john@example.com")
assert result_prod.sanitized_text != result_dev.sanitized_text

This prevents cross-environment data correlation — your staging analytics can’t be matched against production data, even if the same users exist in both.

5. MONITOR

result = inspect_pii("Email: john@example.com", strategy="monitor")
# result.sanitized_text: "Email: john@example.com"  (unchanged)
# result.has_pii: True  (still detected and logged)

Use case: Development and gradual rollout. See what your detector finds before enforcing. Measure your false positive rate before going live.

Here’s how to choose:

  • BLOCK — Does not modify text, blocks request. Best for: HIPAA, PCI-DSS strict compliance.
  • REDACT — Modifies text, does not block. Best for: LLM prompts, logging.
  • MASK — Modifies text, does not block. Best for: Customer support UIs.
  • HASH — Modifies text, does not block. Best for: Analytics, record linking.
  • MONITOR — Does not modify text, does not block. Best for: Development, gradual rollout.

Custom Detectors: Extend Without Forking

Every organization has domain-specific PII. Employee IDs, medical record numbers, internal account codes. I built a protocol-based extension system so you can add detection for anything without modifying the library.

The interface:

@runtime_checkable
class CustomDetector(Protocol):
    """Protocol for custom PII detectors."""
    def detect(self, text: str) -> list[DetectorMatch]: ...

For simple patterns, use RegexDetector:

# Detect internal employee IDs
emp_detector = RegexDetector(r"EMP-\d{6}", label="EMPLOYEE_ID")

# Detect medical record numbers
mrn_detector = RegexDetector(r"MRN-[A-Z]{2}\d{8}", label="MEDICAL_RECORD")
inspector = PIIInspector(
    strategy="redact",
    custom_detectors=[emp_detector, mrn_detector],
)
result = inspector.inspect("Employee EMP-123456, record MRN-AB12345678")
# "[EMPLOYEE_ID REDACTED], record [MEDICAL_RECORD REDACTED]"

For complex logic, use FunctionDetector:

def detect_credit_card_with_context(text: str) -> list[DetectorMatch]:
    """Only flag credit cards preceded by 'card:' or 'cc:'."""
    matches = []
    for m in re.finditer(
        r"(?:card:|cc:)\s*(\d{4}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4})",
        text.lower(),
    ):
        matches.append(DetectorMatch(
            value=m.group(1),
            start=m.start(1),
            end=m.end(1),
            label="CONTEXTUAL_CC",
            confidence=0.95,
        ))
    return matches

detector = FunctionDetector(detect_credit_card_with_context)
# Catches "card: 1234 5678 9012 3456" but ignores bare number sequences
detector = FunctionDete\ctor(detect_credit_card_with_context)
# Catches "card: 1234 5678 9012 3456" but ignores bare number sequences

For the simplest case — just a regex pattern you want to catch — there’s an even shorter path:

# Quick custom patterns (no class needed)
inspector = PIIInspector(
    strategy="redact",
    custom_patterns={
        "api_key": r"sk-[a-zA-Z0-9]{32}",
        "aws_key": r"AKIA[0-9A-Z]{16}",
    },
)

Custom detectors run alongside the built-in ones. If a buggy detector throws an exception, the inspection continues — built-in detection still works. Your custom logic can’t break the core pipeline. That error resilience was a deliberate design choice: a flawed custom detector should never disable your baseline PII protection.

Integration: The @guard Decorator

One decorator. One line. PII protection applied.

from warden import guard

@guard(
    pii=True,
    pii_strategy="redact",
    pii_apply_to="input",
)

def process_message(message: str) -> dict:
    """Process a user message with PII protection."""
    return {"processed": message, "length": len(message)}

Need both SQL and PII protection? Stack them:

@guard(
    sql=True,
    mode="read-only",
    pii=True,
    pii_strategy="redact",
    pii_apply_to="both",  # Check BOTH input and output
)
def secure_query(query: str) -> dict:
    """Execute a query with full protection."""
    return {"data": f"Results for: {query}"}

The pii_apply_to parameter controls the scope:

  • "input" — scan what goes to the function
  • "output" — scan what comes back
  • "both" — scan everything

AWS Strands Integration

Agent-Warden was designed to work with AWS Strands. The @guard decorator stacks with Strands' @tool decorator:

from strands import Agent, tool
from warden import guard

@tool
@guard(
    pii=True,
    pii_strategy="redact",
    pii_detect=["email", "ssn", "credit_card"],
    pii_apply_to="both",
)
def process_support_ticket(message: str) -> str:
    """Process a customer support message with PII protection.
    Args:
        message: The customer's message
    Returns:
        Agent's response with PII redacted
    """
    return llm.process(message)

agent = Agent(tools=[process_support_ticket])

What happens at runtime:

# Customer sends their details
agent("My email is john@company.com and my SSN is 123-45-6789")

# 1. Guard intercepts the input
# 2. PII detected: email (0.99 confidence), SSN (0.95 confidence)
# 3. Input redacted: "My email is [EMAIL REDACTED] and my SSN is [SSN REDACTED]"
# 4. LLM receives the redacted version -- never sees the raw data
# 5. LLM generates a helpful response
# 6. Guard scans the output too (pii_apply_to="both")
# 7. Clean response returned to user

The LLM never sees the original PII. The customer still gets a useful response. The audit log records what was detected and handled. Everyone’s happy — especially your compliance team.

Multi-layer protection with Strands:

@tool
@guard(
    sql=True,
    mode="read-only",
    pii=True,
    pii_strategy="redact",
    pii_apply_to="both",
    audit=True,
)
def query_customer_data(sql: str) -> str:
    """Query customer database with PII protection."""
    results = db.execute(sql)
    return json.dumps(results)

One decorator:

  • Validates SQL structure (no DROP/DELETE/UPDATE)
  • Redacts PII from query results (emails, SSNs, credit cards)
  • Logs everything for compliance audit trails

The @guard decorator uses functools.wraps to preserve function signatures, so Strands' tool introspection works as expected. Type hints, docstrings, and parameter metadata are all maintained.

PII Protection Across the Entire Stack

Here’s something I’m proud of: PII detection in Agent-Warden isn’t limited to the PII inspector. It’s woven through the entire security stack.

RAG document scanning: When your agent retrieves documents for context, the RAG inspector can scan those documents for PII before they’re injected into the prompt:

@guard(
    rag=True,
    rag_scan_pii=True,
    rag_pii_strategy="redact",
)
def answer_with_context(query: str, documents: list[str]) -> str:
    """Answer using retrieved documents -- PII in documents gets redacted."""
    return llm.process(query, context=documents)

Your agent never sees the SSN buried on page 47 of an HR document.

API call inspection: When your agent makes outbound API calls, the API inspector can scan request bodies for PII leakage:

@guard(
    api=True,
    api_scan_pii=True,
)
def call_external_service(payload: str) -> str:
    """Call an external API -- PII in payload is caught."""
    return requests.post("https://api.example.com", data=payload)

This catches PII leaking through API integrations, not just LLM calls.

The result: PII protection at every boundary — LLM inputs, LLM outputs, retrieved documents, and external API calls. One framework, consistent detection, unified audit trail.

What I Learned (And What Still Needs Work)

Building this taught me something I didn’t expect: the hardest part of PII detection isn’t finding PII — it’s deciding what to do with it.

The detection code took a few days. Getting the Luhn algorithm right, writing the SSN validation rules, tuning the regex patterns — that’s straightforward engineering. But the architecture question — how do you build a system that can block in one context, redact in another, and hash in a third, all with the same detection engine? — that took weeks of iteration.

The five-strategy system came from talking to real teams. A healthcare startup needed BLOCK. A fintech needed HASH for analytics. A SaaS support tool needed REDACT for their LLM calls and MASK for their UI. Every team had a different answer to “what should happen when you find PII?” The system had to accommodate all of them.

What works:

  • Validated detection eliminates the false positive problem (Luhn for cards, SSA rules for SSNs, octet validation for IPs)
  • Five strategies cover every real-world compliance scenario I’ve encountered
  • Custom detector protocol makes domain-specific PII trivial to add
  • Metadata-only audit trails satisfy GDPR without creating new PII exposure
  • 80+ test cases across every edge case I could think of

Current limitations (and where the project is heading):

  • No Named Entity Recognition for unstructured names and addresses — regex can’t reliably detect “John Smith” as a name versus a product name. NER integration with spaCy/Presidio is on the roadmap.
  • International formats need expansion (UK postcodes, EU phone numbers, IBAN). The custom detector protocol makes community contributions straightforward.
  • Confidence thresholds need per-deployment tuning — what works for a US healthcare app won’t be right for a European fintech.
  • No contextual detection by default (“is this 9-digit number an SSN or a zip+4?”)

These aren’t permanent gaps. The protocol-based architecture means every one of these can be solved by adding detectors, not rewriting the core engine. That was the whole point of building it this way.

Why This Matters for the Agentic Ecosystem

Let me paint the picture. It’s 2026, and every enterprise is racing to deploy AI agents. Support agents, analytics agents, internal tools, customer-facing bots. The frameworks make it easy — AWS Strands, LangChain, CrewAI — you can have an agent running in an afternoon.

But here’s what often gets overlooked in the rush to ship: every time your agent calls an LLM API, it’s a data transfer to a third party.

Under GDPR, sending personal data to OpenAI or Anthropic without proper handling is a violation. Under CCPA, it triggers right-to-know obligations. Under HIPAA, it’s a reportable breach. The fines start at 4% of annual revenue.

And the common pattern in many agentic tutorials?

response = llm.invoke(user_message)  # Is there PII in here?

This is similar to the early days of web security, when developers passed unsanitized input to databases before the ecosystem built shared validation tools. The difference: with LLMs, the data isn’t just stored — it’s transmitted to a third-party API, potentially across borders.

Agentic frameworks are designed to be flexible and unopinionated — that’s a feature, not a flaw. But it means the PII protection layer is up to developers. Some teams use commercial solutions. Some write custom filters. Some add “do not include personal information in your responses” to the system prompt. The gap I saw was a lightweight, open-source option designed specifically for the agentic workflow — something you can drop in with a decorator and configure for your compliance context.

Agent-Warden provides the deterministic layer between your users and the LLM. PII is detected, validated, scored, and handled — before the data ever leaves your server. No probabilistic guardrails. No prompt-based wishful thinking. Deterministic code that runs in under a millisecond and catches what needs catching.

Here’s the thing that surprised me: once I built this, I realized the same PII detection engine needed to run at every boundary — not just the LLM call. RAG document retrieval? PII in the documents. API calls? PII in the payload. Database queries? PII in the results. That’s why Agent-Warden’s PII detection isn’t a standalone tool — it’s woven through the entire security stack. One configuration, consistent protection, unified audit trail.

The Open Source Strategy

Agent-Warden is fully open source (MIT license) on GitHub. I’m building it like Pydantic — solve one problem extremely well, make it a dependency everyone trusts.

What makes this PII engine different from existing tools:

1. Five configurable strategies, not just detect-and-block. Many PII tools stop at detection — they tell you “there’s an email here.” Agent-Warden adds the next step: do you want to block, redact, mask, hash, or just monitor? That decision matters for compliance, and having all five strategies in a single API means one tool covers HIPAA, GDPR, analytics, and development workflows.

2. Validation beyond pattern matching. Luhn algorithm for credit cards. SSA rules for Social Security Numbers. Octet validation for IP addresses. This isn’t just regex — it’s structured validation that reduces false positives to near zero.

3. Protocol-based extensibility. Implement the CustomDetector protocol, pass it to the inspector, and your domain-specific PII is handled. No monkey-patching, no forks, no PRs required.

4. Zero external dependencies for PII detection. The entire engine runs on Python’s standard library. No spaCy, no Presidio, no ML models to download. Install and run in seconds.

5. Cross-stack protection. PII scanning built into the SQL guard, RAG inspector, API inspector, and the standalone PII inspector. One framework protects every boundary.

Check out the code on GitHub: https://github.com/Sheeban-Wasi/agent-warden

Current stats:

  • 701 tests across 7 inspectors (SQL, PII, File, Shell, RAG, API, Identity)
  • PII engine alone: 829 lines of detection code, 889 lines of tests
  • Zero external dependencies for PII detection (stdlib only)
  • Sub-millisecond detection time (~0.2ms average)
  • Integrates with AWS Strands, LangChain adapter in development
  • Designed for any Python-based agentic framework
  • MIT licensed, actively maintained

I’m not trying to build a platform. I’m building the plumbing — boring infrastructure that just works. The kind of library you install once, configure with a decorator, and forget about until an auditor asks “how do you handle PII?” Then you show them the audit logs and they sign off.

Try It Yourself

GitHub: https://github.com/Sheeban-Wasi/agent-warden

Install:

pip install agent-warden

Three levels of API, depending on how much control you need:

# Level 1: Quick check - is there PII?
from warden import check_pii

if check_pii("My email is john@example.com"):
    print("Contains PII!")

# Level 2: Full inspection - detect, validate, and handle

from warden import inspect_pii

result = inspect_pii("SSN: 123-45-6789", strategy="redact")
print(result.sanitized_text)  # "SSN: [SSN REDACTED]"
print(result.findings)        # List of PIIMatch objects
print(result.verdict.blocked) # False (redact doesn't block)

# Level 3: Decorator - one line, full protection

from warden import guard
@guard(pii=True, pii_strategy="redact")
def my_agent_tool(text: str):
    return llm.process(text)

Or the shortest path — just redact and go:

from warden import redact_pii

clean = redact_pii("Email: john@example.com, SSN: 123-45-6789")
# "Email: [EMAIL REDACTED], SSN: [SSN REDACTED]"

What’s Next

I’m actively working on:

  • NER-based name detection (integration with spaCy/Presidio for unstructured PII like names and addresses)
  • International PII formats (IBAN, UK postcodes, EU phone numbers, national ID formats)
  • Real-time PII dashboards (visualize what’s being caught across your agent fleet)
  • LangChain adapter (native LangChain tools integration)

Calling All AWS Strands Developers and Agent Builders

If you’re building customer-facing agents with AWS Strands — support bots, analytics tools, anything that touches user data — the @guard decorator gives you GDPR compliance with one line. No architectural changes. No new infrastructure. Just a decorator.

Get started:

Found a bug? Have a feature request? Open an issue on GitHub: https://github.com/Sheeban-Wasi/agent-warden/issues

The project is fully open source and welcoming contributors.

Resources

  • GitHub Repository: https://github.com/Sheeban-Wasi/agent-warden
  • Documentation: Full documentation with 10+ examples including PII protection
  • Previous article: Why Regex Can’t Save Your Database: AST-Based SQL Guard for AI Agents
  • Next article: Identity Management for AI Agents: Moving Beyond ‘God Mode’

About This Series

This is Article 3 in a series on building production-ready security infrastructure for AI agents. The series covers:

  1. The Crisis of Vibe Coding in AI Agent Security
  2. Why Regex Can’t Save Your Database: AST-Based SQL Guard
  3. Building GDPR-Compliant AI Agents: PII Detection at Scale (this article)
  4. Identity Management for AI Agents: Moving Beyond ‘God Mode’
  5. The Security Layer Stack: RAG, API Guards, and Data Exfiltration Prevention
  6. Production-Ready AI: Rate Limiting, Human-in-the-Loop, and Retry Strategies
  7. The @guard Decorator: One Line to Rule Them All
  8. Building the Pydantic of AI Security: Lessons from 700 Tests

Follow along as I document the technical decisions, architectural patterns, and lessons learned from building Agent-Warden in the open.


메타데이터
post_id
bf48fe5bcb18
slug
building-gdpr-compliant-ai-agents-i-built-a-pii-detection-engine-that-actually-validates-what-it-bf48fe5bcb18
url
https://medium.com/@sheebanw/building-gdpr-compliant-ai-agents-i-built-a-pii-detection-engine-that-actually-validates-what-it-bf48fe5bcb18
canonical_url
https://medium.com/@sheebanw/building-gdpr-compliant-ai-agents-i-built-a-pii-detection-engine-that-actually-validates-what-it-bf48fe5bcb18
author_url
https://medium.com/@sheebanw
status
ok
fetched_at
2026-06-09 15:37:30