pyveil

Python LLM security guide

PII redaction for Python LLM apps: protect the boundary.

Redact high-confidence PII and credentials before a prompt, tool call, tool result, memory write, log, or trace leaves your application.

Published July 11, 2026 · Uses pyveil 0.2.1 · Synthetic examples only

Redact before the provider call, not after

Once an LLM provider or model-controlled tool receives raw data, application-side masking is too late. The useful control point is the last local boundary where your code still owns the payload.

user / RAG / database local redaction LLM provider

pyveil keeps the operation local and replaces supported values with scoped HMAC placeholders such as [EMAIL:12hex]. Stable placeholders let the model refer to the same hidden value during a run without receiving the original value.

Install and prove it works

pip install pyveil
python -m pyveil demo

The demo uses synthetic values and needs no secret. Application redaction does require your own HMAC secret so placeholders cannot be correlated by an unknown global key.

Protect a provider-neutral LLM call

from pyveil import Channel, Veil

veil = Veil.high(
    secret=b"tenant-or-run-secret",
    scope="tenant-123/session-456",
)

messages = [
    {
        "role": "user",
        "content": "Email alice@example.com about account CUS-A1B2C3D4.",
    }
]

safe = veil.redact_data(messages, channel=Channel.PROMPT_INPUT)
response = call_llm(safe.data)  # OpenAI, Anthropic, Gemini, or your gateway

The list and dictionary shape is preserved. Only sensitive string values change. Use a CustomRule for identifiers or known values your application understands:

from pyveil import CustomRule

rules = [
    CustomRule("CUSTOMER_ID", r"\bCUS-[A-Z0-9]{8}\b"),
    CustomRule.exact("PERSON", ["Alice Kim", "Hong Gildong"]),
]

veil = Veil.high(secret=b"tenant-secret", rules=rules)

Use structured redaction for agent payloads

Do not stringify tool or model payloads just to mask them. redact_data recursively handles dictionaries, lists, tuples, and JSON while keeping booleans, numbers, and null values intact.

payload = {
    "user": "alice@example.com",
    "headers": {"Authorization": "Bearer DEMO_ONLY_CREDENTIAL_123456"},
    "debug": True,
}

safe = veil.redact_data(payload, channel=Channel.TOOL_CALL_RESULT)

assert safe.data["debug"] is True
assert safe.data["user"].startswith("[EMAIL:")
assert safe.data["headers"]["Authorization"].startswith("[AUTH_HEADER:")

Block credentials before model-controlled tools execute

Tool arguments are a higher-risk boundary than an ordinary prompt. The default policy blocks credential-like findings on tool.call.arguments rather than silently forwarding them.

from pyveil import BlockedSensitiveData, Channel

try:
    safe_args = veil.redact_data(
        tool_args,
        channel=Channel.TOOL_CALL_ARGUMENTS,
    ).data
except BlockedSensitiveData as exc:
    audit(exc.summary())  # counts only; no raw secret
else:
    run_tool(**safe_args)
Boundary rule

Redact model input, tool results, MCP resources, memory, logs, and traces. Block credentials before model-controlled tool execution.

Keep large redaction work off the event loop

pyveil is synchronous and CPU-bound. For large payloads in FastAPI or another async server, run it in the default executor:

import asyncio
from functools import partial

redact = partial(
    veil.redact_data,
    payload,
    channel=Channel.PROMPT_INPUT,
)
safe = await asyncio.get_running_loop().run_in_executor(None, redact)

For small request bodies the direct synchronous call is usually simpler. Keep max_input_chars enabled and enforce upstream request-size limits either way.

Test the boundary, not only the detector

def test_provider_never_receives_raw_email():
    captured = {}

    def fake_provider(messages):
        captured["messages"] = messages

    safe = veil.redact_data(
        [{"role": "user", "content": "Email alice@example.com"}],
        channel=Channel.PROMPT_INPUT,
    )
    fake_provider(safe.data)

    assert "alice@example.com" not in str(captured)
    assert "[EMAIL:" in str(captured)

The repository also ships a reproducible synthetic detector evaluation and runs it in CI.

Know what local regex redaction does not solve

NeedRecommended layer
Known emails, phones, cards, credentials, domain IDspyveil at the final application boundary
Unknown people, organizations, locations, addressesNER or Presidio upstream, then pyveil at the boundary
Documents, images, incident workflows, managed policyEnterprise DLP
Prompt injection and model behavior controlsA dedicated guardrail layer
No compliance guarantee

Redaction reduces exposure. It does not prove GDPR, HIPAA, PCI, or any other compliance regime, and no detector has perfect recall.