Skip to content

PII Masking Examples

Basic PII Masking

from agent_airlock import Airlock, AirlockConfig

config = AirlockConfig(
    sanitize_output=True,
    mask_pii=True,
)

@Airlock(config=config)
def get_customer(id: int) -> dict:
    return {
        "id": id,
        "name": "John Doe",
        "email": "john@example.com",
        "phone": "555-123-4567",
        "ssn": "123-45-6789",
    }

result = get_customer(id=123)
# {
#     "id": 123,
#     "name": "John Doe",
#     "email": "j***@example.com",
#     "phone": "555***567",
#     "ssn": "[REDACTED]"
# }

Secret Masking

from agent_airlock import Airlock, AirlockConfig

config = AirlockConfig(
    sanitize_output=True,
    mask_secrets=True,
)

@Airlock(config=config)
def get_config() -> dict:
    return {
        "api_key": "sk-1234567890abcdefghijklmnop",
        "aws_key": "AKIAIOSFODNN7EXAMPLE",
        "db_url": "postgres://user:pass@localhost/db",
        "jwt": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ4In0.abc",
    }

result = get_config()
# {
#     "api_key": "sk-1234...mnop",
#     "aws_key": "AKIAIOS...MPLE",
#     "db_url": "[REDACTED]",
#     "jwt": "eyJhbGciOi...[JWT]"
# }

Secrets are matched by shape: a value that does not fit a known key format (for example an sk- key with fewer than 20 characters after the prefix) is returned unmasked.

Masking Strategies

@Airlock uses each type's default strategy (PARTIAL for email, as above); AirlockConfig has no strategy setting. To pick one, call sanitize_output with a mask_config mapping. A type left out of the mapping is masked FULL.

from agent_airlock import MaskingStrategy, SensitiveDataType, sanitize_output

def mask_email(strategy: MaskingStrategy) -> str:
    config = {SensitiveDataType.EMAIL: strategy}
    return sanitize_output("john@example.com", mask_config=config).content

# Full redaction
mask_email(MaskingStrategy.FULL)       # [REDACTED]

# Partial masking
mask_email(MaskingStrategy.PARTIAL)    # j***@example.com

# Type only
mask_email(MaskingStrategy.TYPE_ONLY)  # [EMAIL]

# Hash (for correlation)
mask_email(MaskingStrategy.HASH)       # [HMAC:3f9a8c2e1b7d4065]

Selective Type Masking

AirlockConfig has no per-type switch, only mask_pii, mask_secrets and pii_locales. Select types with the standalone functions:

from agent_airlock import (
    SensitiveDataType, WorkspacePIIConfig, mask_sensitive_data, sanitize_with_workspace_config,
)

text = "john@example.com, SSN 123-45-6789, card 4111111111111111"

# Only mask specific types
masked, detections = mask_sensitive_data(
    text,
    types=[
        SensitiveDataType.SSN,
        SensitiveDataType.CREDIT_CARD,
    ],
)
# john@example.com, SSN [REDACTED], card **** **** **** 1111
# (email NOT masked)

# Disable specific types
config = WorkspacePIIConfig(
    workspace_id="ops",
    disabled_types=[
        SensitiveDataType.IP_ADDRESS,   # Don't mask IPs
    ],
)
sanitize_with_workspace_config("host 10.0.0.12, owner ops@example.com", config).content
# host 10.0.0.12, owner o***@example.com

Workspace-Specific Rules

from agent_airlock import (
    WorkspacePIIConfig,
    sanitize_with_workspace_config,
)

# Enterprise workspace - allow internal emails
enterprise = WorkspacePIIConfig(
    workspace_id="acme-corp",
    allow_email_domains=["acme.com", "acme.internal"],
)

content = "Contact alice@acme.com or bob@gmail.com"
result = sanitize_with_workspace_config(content, enterprise)
# result.content == "Contact alice@acme.com or b***@gmail.com"

# Sales workspace - mask competitor emails
sales = WorkspacePIIConfig(
    workspace_id="sales",
    mask_email_domains=["competitor1.com", "competitor2.com"],
)

content = "Lead: prospect@company.com, Spy: mole@competitor1.com"
result = sanitize_with_workspace_config(content, sales)
# result.content == "Lead: prospect@company.com, Spy: m***@competitor1.com"

Phone Number Filtering

from agent_airlock import WorkspacePIIConfig, sanitize_with_workspace_config

config = WorkspacePIIConfig(
    workspace_id="support",
    allow_phone_prefixes=["+1800", "1800", "+1888", "1888"],
)

content = """
Toll-free: 1-800-555-1234 (keep visible)
Personal: 555-123-4567 (mask this)
Support: +1888-555-9999 (keep visible)
"""

result = sanitize_with_workspace_config(content, config)
# result.content: toll-free numbers preserved, personal masked as 555***567

Custom Patterns

from agent_airlock import MaskingStrategy, WorkspacePIIConfig, sanitize_with_workspace_config

config = WorkspacePIIConfig(
    workspace_id="hr-department",
    custom_patterns={
        "employee_id": r"EMP-\d{6}",
        "badge_number": r"BADGE-[A-Z]{2}\d{4}",
        "salary": r"\$\d{1,3}(?:,\d{3})*(?:\.\d{2})?",
    },
    custom_strategies={
        "employee_id": MaskingStrategy.TYPE_ONLY,
        "salary": MaskingStrategy.FULL,
    },
)

content = """
Employee: EMP-123456
Badge: BADGE-AB1234
Salary: $85,000.00
"""

result = sanitize_with_workspace_config(content, config)
# result.content:
# Employee: [EMPLOYEE_ID]
# Badge: [REDACTED]
# Salary: [REDACTED]

A custom pattern with no strategy is masked FULL. Only TYPE_ONLY shows the pattern's name; every other strategy gives [REDACTED].

Streaming Sanitization

from agent_airlock import StreamingAirlock, AirlockConfig

config = AirlockConfig(
    sanitize_output=True,
    mask_pii=True,
    max_output_chars=1000,
)

streaming = StreamingAirlock(config)

def generate_report():
    yield "Customer: john@example.com\n"
    yield "Phone: 555-123-4567\n"
    yield "SSN: 123-45-6789\n"

for chunk in streaming.wrap_generator(generate_report()):
    print(chunk, end="")
# Customer: j***@example.com
# Phone: 555***567
# SSN: [REDACTED]

print(f"\nTruncated: {streaming.state.truncated}")
# Truncated: False

Direct Sanitization

from agent_airlock import sanitize_output

content = """
User data:
- Email: john@example.com
- Phone: (555) 123-4567
- SSN: 123-45-6789
- API Key: sk-abcdef123456
"""

result = sanitize_output(
    content,
    mask_pii=True,
    mask_secrets=True,
)

print(result.content)
print(f"Detections: {result.detection_count}")

# Each detection is a dict: type, value, start, end, full_match, masked_as
for detection in result.detections:
    print(f"  {detection['type']}: {detection['value']} → {detection['masked_as']}")

Output:

User data:
- Email: j***@example.com
- Phone: (555***567
- SSN: [REDACTED]
- API Key: sk-abcdef123456

Detections: 3
  email: john@example.com → j***@example.com
  phone: 555) 123-4567 → 555***567
  ssn: 123-45-6789 → [REDACTED]

The API key is left as it is: the sk- pattern needs at least 20 characters after the prefix. The phone match starts after the opening parenthesis, which stays in the output.