> ## Documentation Index
> Fetch the complete documentation index at: https://docs.agentmark.co/llms.txt
> Use this file to discover all available pages before exploring further.

# PII masking

> Redact sensitive data from traces before they leave your application

AgentMark PII masking strips sensitive data from span attributes before traces leave your application. Masking runs in your application process, so it redacts configured attributes before the OTel exporter sends them over the network.

You can use a custom mask function, the built-in PII masker, environment variable suppression, or any combination of these approaches. Coverage depends on your configuration: masking covers only the attributes you target.

***

## How it works

AgentMark implements PII masking as an OpenTelemetry `SpanProcessor` that wraps the export pipeline. When a span finishes, the masking processor intercepts it before the exporter sends data over the network.

<Steps>
  <Step title="Span finishes">
    Your application completes an LLM call or tool invocation. The span is ready for export.
  </Step>

  <Step title="MaskingSpanProcessor intercepts">
    If you configure masking, the processor runs env var suppression first, then your mask function on each sensitive attribute.
  </Step>

  <Step title="Redacted span exported (or dropped)">
    If masking succeeds, the processor forwards the redacted span to the exporter. If the mask function throws, the processor **drops the span entirely** (fail-closed) and logs a warning.
  </Step>
</Steps>

This means:

* **The processor redacts configured attributes before export.** The processor runs in-memory, in your application, before any network call, so attributes you mask never leave the process in their raw form.
* **Zero overhead when masking is off.** The SDK only adds the processor to the pipeline when you configure a `mask` function or set env vars.
* **Standard OTel pattern.** The `MaskingSpanProcessor` wraps your existing `BatchSpanProcessor` or `SimpleSpanProcessor`, with no forking or patching required.

### Before and after

With `createPiiMasker()` enabled, PII tokens like `[EMAIL]`, `[SSN]`, and `[PHONE]` replace sensitive data in the trace viewer:

<Frame>
  <img src="https://mintcdn.com/puzzlet-9ba7bb98/5xsP1AqgWmw7fWzd/images/pii-masking/trace-masked.png?fit=max&auto=format&n=5xsP1AqgWmw7fWzd&q=85&s=b2af35f969a957fa2a6a007a5246f15a" alt="Trace with PII masking enabled, sensitive data replaced with tokens like [EMAIL], [SSN], [PHONE]" width="1280" height="720" data-path="images/pii-masking/trace-masked.png" />
</Frame>

With `AGENTMARK_HIDE_INPUTS=true`, all input attributes show `[REDACTED]` while outputs remain visible:

<Frame>
  <img src="https://mintcdn.com/puzzlet-9ba7bb98/5xsP1AqgWmw7fWzd/images/pii-masking/trace-redacted.png?fit=max&auto=format&n=5xsP1AqgWmw7fWzd&q=85&s=45f0ddef9e1c12de8c15395a1d421280" alt="Trace with input suppression, all inputs show [REDACTED]" width="1280" height="720" data-path="images/pii-masking/trace-redacted.png" />
</Frame>

Here's what the raw span attributes look like with each approach:

**Without masking:**

```json theme={null}
{
  "gen_ai.request.input": "My SSN is 123-45-6789 and email is user@example.com",
  "gen_ai.response.output": "I found your account linked to user@example.com",
  "gen_ai.request.model": "gpt-5",
  "gen_ai.usage.total_tokens": 150
}
```

**With `createPiiMasker({ email: true, ssn: true })`:**

```json theme={null}
{
  "gen_ai.request.input": "My SSN is [SSN] and email is [EMAIL]",
  "gen_ai.response.output": "I found your account linked to [EMAIL]",
  "gen_ai.request.model": "gpt-5",
  "gen_ai.usage.total_tokens": 150
}
```

**With `AGENTMARK_HIDE_INPUTS=true`:**

```json theme={null}
{
  "gen_ai.request.input": "[REDACTED]",
  "gen_ai.response.output": "I found your account linked to user@example.com",
  "gen_ai.request.model": "gpt-5",
  "gen_ai.usage.total_tokens": 150
}
```

Notice that `gen_ai.request.model` and `gen_ai.usage.total_tokens` are never masked. These operational attributes contain no user data.

***

## Basic usage

Pass a `mask` function to `AgentMarkSDK`. The function receives each string attribute value and returns the redacted version.

<CodeGroup>
  ```typescript TypeScript theme={null}
  import { AgentMarkSDK } from '@agentmark-ai/sdk';

  const sdk = new AgentMarkSDK({
    apiKey: 'am_...',
    appId: 'app-123',
    mask: (data) => data.replace(/\b\d{3}-\d{2}-\d{4}\b/g, '[SSN]'),
  });
  sdk.initTracing();
  ```

  ```python Python theme={null}
  import re
  from agentmark_sdk import AgentMarkSDK

  sdk = AgentMarkSDK(
      api_key="am_...",
      app_id="app-123",
      mask=lambda data: re.sub(r"\b\d{3}-\d{2}-\d{4}\b", "[SSN]", data),
  )
  sdk.init_tracing()
  ```
</CodeGroup>

AgentMark calls the `mask` function on every maskable span attribute before it hands the span to the exporter. The function must be synchronous. You have full control over the replacement logic.

***

## Built-in PII masker

AgentMark ships a built-in PII masker that covers common patterns. Enable the patterns you need:

<CodeGroup>
  ```typescript TypeScript theme={null}
  import { AgentMarkSDK, createPiiMasker } from '@agentmark-ai/sdk';

  const sdk = new AgentMarkSDK({
    apiKey: 'am_...',
    appId: 'app-123',
    mask: createPiiMasker({
      email: true,
      phone: true,
      ssn: true,
      creditCard: true,
      ipAddress: true,
    }),
  });
  sdk.initTracing();
  ```

  ```python Python theme={null}
  from agentmark_sdk import AgentMarkSDK, create_pii_masker, PiiMaskerConfig

  sdk = AgentMarkSDK(
      api_key="am_...",
      app_id="app-123",
      mask=create_pii_masker(PiiMaskerConfig(
          email=True,
          phone=True,
          ssn=True,
          credit_card=True,
          ip_address=True,
      )),
  )
  sdk.init_tracing()
  ```
</CodeGroup>

### Built-in patterns

* **`email`**: Matches email addresses like `user@example.com`. Replaced with `[EMAIL]`.
* **`phone`**: Matches phone numbers like `(555) 123-4567`. Replaced with `[PHONE]`.
* **`ssn`**: Matches Social Security numbers like `123-45-6789`. Replaced with `[SSN]`.
* **`creditCard`** / **`credit_card`**: Matches credit card numbers like `4111 1111 1111 1111`. Replaced with `[CREDIT_CARD]`.
* **`ipAddress`** / **`ip_address`**: Matches IP addresses like `192.168.1.100`. Replaced with `[IP_ADDRESS]`.

All patterns default to `false`. AgentMark applies only the patterns you explicitly enable.

<Note>
  TypeScript uses camelCase (`creditCard`, `ipAddress`) while Python uses snake\_case (`credit_card`, `ip_address`) following each language's conventions.
</Note>

***

## Custom patterns

You can add custom patterns alongside the built-in ones. Each entry needs a regex pattern and a replacement string.

<CodeGroup>
  ```typescript TypeScript theme={null}
  import { AgentMarkSDK, createPiiMasker } from '@agentmark-ai/sdk';

  const sdk = new AgentMarkSDK({
    apiKey: 'am_...',
    appId: 'app-123',
    mask: createPiiMasker({
      email: true,
      custom: [
        { pattern: /MRN-\d+/g, replacement: '[MEDICAL_RECORD]' },
        { pattern: /ACCT-[A-Z0-9]+/g, replacement: '[ACCOUNT_ID]' },
      ],
    }),
  });
  sdk.initTracing();
  ```

  ```python Python theme={null}
  import re
  from agentmark_sdk import AgentMarkSDK, create_pii_masker, PiiMaskerConfig, CustomPattern

  sdk = AgentMarkSDK(
      api_key="am_...",
      app_id="app-123",
      mask=create_pii_masker(PiiMaskerConfig(
          email=True,
          custom=[
              CustomPattern(pattern=re.compile(r"MRN-\d+"), replacement="[MEDICAL_RECORD]"),
              CustomPattern(pattern=re.compile(r"ACCT-[A-Z0-9]+"), replacement="[ACCOUNT_ID]"),
          ],
      )),
  )
  sdk.init_tracing()
  ```
</CodeGroup>

Custom patterns run after built-in patterns. You can use custom patterns on their own without enabling any built-in patterns.

***

## Environment variable suppression

For a zero-code option, set environment variables to suppress all inputs, all outputs, or both:

```bash theme={null}
AGENTMARK_HIDE_INPUTS=true
AGENTMARK_HIDE_OUTPUTS=true
```

When enabled, these replace ALL input or output attribute values with `[REDACTED]`. This requires no code changes.

<Note>
  If you configure both environment variable suppression and a `mask` function, suppression runs first. The mask function then receives the already-suppressed values.
</Note>

***

## Masked attributes reference

AgentMark masks specific span attributes depending on their category.

**Input attributes** (suppressed by `AGENTMARK_HIDE_INPUTS`):

* `gen_ai.request.input`: the prompt or messages sent to the model
* `agentmark.request.input`: vendor-namespaced input written by `observe()` / `setInput()` (dual-emitted with `gen_ai.request.input`)
* `gen_ai.request.tool_calls`: tool call arguments included in the request
* `agentmark.dataset_input`: the dataset row input attached during experiment runs
* `ai.prompt`, `ai.prompt.messages`, `ai.prompt.tools`, `ai.prompt.toolChoice`, `ai.toolCall.args`: prompt, message, and tool-call content emitted by the Vercel AI SDK (`experimental_telemetry`)
* `gen_ai.input.messages`, `gen_ai.system_instructions`, `gen_ai.tool.definitions`, `gen_ai.tool.call.arguments`: input content per the OpenTelemetry GenAI semantic conventions
* `gen_ai.prompt`: legacy OpenTelemetry GenAI prompt attribute
* `gen_ai.tool.input`: tool input recorded by the Claude Agent SDK integration

**Output attributes** (suppressed by `AGENTMARK_HIDE_OUTPUTS`):

* `gen_ai.response.output`: the model's text response
* `agentmark.response.output`: vendor-namespaced output written by `observe()` / `setOutput()` (dual-emitted with `gen_ai.response.output`)
* `gen_ai.response.output_object`: structured output from the model
* `ai.response.text`, `ai.response.toolCalls`, `ai.response.object`, `ai.result.text`, `ai.result.toolCalls`, `ai.result.object`, `ai.toolCall.result`: response and tool-result content emitted by the Vercel AI SDK (`experimental_telemetry`)
* `gen_ai.output.messages`, `gen_ai.tool.call.result`: output content per the OpenTelemetry GenAI semantic conventions
* `gen_ai.completion`: legacy OpenTelemetry GenAI completion attribute
* `gen_ai.tool.output`: tool output recorded by the Claude Agent SDK integration

**Metadata attributes** (mask function only, not affected by env vars):

* `agentmark.metadata.*`: custom metadata attached to spans

Operational attributes such as trace IDs, model names, and token counts are never masked. These contain no user data, and observability needs them to function.

***

## Error handling

PII masking uses fail-closed behavior. If your `mask` function throws an error, the processor drops the span entirely and never exports it. This ensures that unmasked data never reaches the trace backend.

Tracing continues normally for subsequent spans after a mask failure. The dropped span doesn't affect the rest of the trace pipeline.

<Warning>
  Test your mask function thoroughly before deploying to production. A mask function that throws on unexpected input causes spans to be silently dropped.
</Warning>

***

## Recipes

### Microsoft Presidio (Python)

[Microsoft Presidio](https://microsoft.github.io/presidio/) uses NLP to detect unstructured PII like person names, addresses, and passport numbers that regex patterns miss. Since Presidio is a Python library, this recipe applies to the Python SDK.

```bash theme={null}
pip install presidio-analyzer presidio-anonymizer
python -m spacy download en_core_web_lg
```

```python Python theme={null}
from presidio_analyzer import AnalyzerEngine
from presidio_anonymizer import AnonymizerEngine
from agentmark_sdk import AgentMarkSDK

analyzer = AnalyzerEngine()
anonymizer = AnonymizerEngine()

def presidio_mask(data: str) -> str:
    results = analyzer.analyze(text=data, language="en")
    anonymized = anonymizer.anonymize(text=data, analyzer_results=results)
    return anonymized.text

sdk = AgentMarkSDK(
    api_key="am_...",
    app_id="app-123",
    mask=presidio_mask,
)
sdk.init_tracing()
```

Presidio detects 15+ entity types including `PERSON`, `LOCATION`, `US_PASSPORT`, `IBAN_CODE`, and `CRYPTO`. See the [Presidio supported entities](https://microsoft.github.io/presidio/supported_entities/) for the full list.

<Tip>
  For TypeScript applications, use `createPiiMasker()` with custom regex patterns for common PII types. Presidio requires a Python runtime and NLP models (\~500MB) which makes it better suited for Python services.
</Tip>

### Healthcare (HIPAA)

Combine built-in patterns with custom patterns for Protected Health Information:

```python Python theme={null}
import re
from agentmark_sdk import AgentMarkSDK, create_pii_masker, PiiMaskerConfig, CustomPattern

sdk = AgentMarkSDK(
    api_key="am_...",
    app_id="app-123",
    mask=create_pii_masker(PiiMaskerConfig(
        email=True,
        phone=True,
        ssn=True,
        ip_address=True,
        custom=[
            CustomPattern(pattern=re.compile(r"MRN[-\s]?\d{6,10}"), replacement="[MRN]"),
            # DEA numbers are 2 letters followed by 7 digits (e.g. AB1234567).
            CustomPattern(pattern=re.compile(r"\b[A-Z]{2}\d{7}\b"), replacement="[DEA_NUMBER]"),
            CustomPattern(pattern=re.compile(r"\b(?:DOB|dob)[:\s]+\d{1,2}/\d{1,2}/\d{2,4}"), replacement="[DOB]"),
        ],
    )),
)
sdk.init_tracing()
```

### Financial services

For PCI-DSS compliance, enable credit card masking and add patterns for financial identifiers:

```typescript TypeScript theme={null}
import { AgentMarkSDK, createPiiMasker } from '@agentmark-ai/sdk';

const sdk = new AgentMarkSDK({
  apiKey: 'am_...',
  appId: 'app-123',
  mask: createPiiMasker({
    creditCard: true,
    ssn: true,
    custom: [
      { pattern: /\b\d{9}\b/g, replacement: '[ROUTING_NUMBER]' },
      { pattern: /\b[A-Z]{2}\d{2}[A-Z0-9]{4}\d{7}([A-Z0-9]?){0,16}\b/g, replacement: '[IBAN]' },
    ],
  }),
});
sdk.initTracing();
```

<Note>
  For maximum compliance assurance, combine a `mask` function with `AGENTMARK_HIDE_INPUTS=true` as a defense-in-depth strategy. The env var acts as a safety net in case a new input attribute appears that the mask function doesn't cover.
</Note>

## Next steps

Verify your masking in the trace detail view: see [Traces and logs](/observe/traces-and-logs) for where span attributes appear.

<div className="mt-8 rounded-lg bg-blue-50 p-6 dark:bg-blue-900/30">
  <h3 className="font-semibold mb-3">Have questions?</h3>
  <p className="mb-4">Reach out any time:</p>

  <ul>
    <li>
      Email the team at <a href="mailto:hello@agentmark.co" className="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-200">[hello@agentmark.co](mailto:hello@agentmark.co)</a> for support
    </li>

    <li>
      Schedule an <a href="https://cal.com/ryan-randall/enterprise" className="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-200">Enterprise Demo</a> to learn about AgentMark's business solutions
    </li>
  </ul>
</div>
