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

# PII redaction — protect sensitive data in traces

> Configure Zespan's built-in PII redaction — key-based and pattern-based — to remove sensitive values from captured LLM data before it's sent to the ingest endpoint.

Zespan's PII redaction removes sensitive values from the data captured during LLM calls — message content, tool call arguments, system prompts, and stored prompt/completion text. When a field's key matches a configured list, its value is replaced with `"[REDACTED]"` — the key is preserved so you can still see which field contained sensitive data, but the value never reaches Zespan servers.

<Note>
  Key-based redaction (`redactKeys` / `redact_keys`) matches **field names**, not free-form text. It's applied to the structured data captured from LLM calls — message objects, tool call arguments, and similar JSON-shaped fields. It runs identically, locally, and always-on in both SDKs. Pattern-based PII detection (scanning prose for emails, phone numbers, etc.) is a separate feature with very different behavior per language — see [Pattern-based PII detection](#pattern-based-pii-detection) below before relying on it.
</Note>

## When redaction runs

Key-based redaction runs inside the SDK, in your process, before an event is added to the send queue — it never depends on a network round trip. Both wrappers apply it to the same fields:

* `prompt_text` / `completion_text` / `system_prompt` — only when `storePrompts` (`store_prompts`) is `true` (the default)
* `tool_call_args` — always, regardless of `storePrompts`

<CodeGroup>
  ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  // packages/sdk/src/client.ts — ZespanClient's method delegates to the
  // imported redactForStorage() from redact.ts (not part of @zespan/sdk's
  // public exports — see the Manual redaction note below)
  redactForStorage(value: unknown): string {
    return redactForStorage(value, this.redactKeys);
  }
  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  # packages/sdk-python/zespan/client.py — inside ZespanClient
  def redact_for_storage(self, value: Any) -> str:
      """Key-based redaction. Always-on."""
      from .redact import redact_for_storage as _redact
      return _redact(value, self.redact_keys)
  ```
</CodeGroup>

Every provider wrapper (`wrapOpenAI`/`patch_openai`, `wrapAnthropic`/`patch_anthropic`, and so on) calls this before the event is queued for the batch flush to `/v1/ingest` — so a matching key's value is already `"[REDACTED]"` by the time it leaves your application.

## Configuring redaction on init

Key-based redaction is always on — there's no flag to disable it — but you control the key list via `redactKeys` (TypeScript) / `redact_keys` (Python) at initialization.

<CodeGroup>
  ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  import { zespan } from "@zespan/sdk";

  zespan.init({
    apiKey: process.env.ZESPAN_API_KEY!,
    redactKeys: ["password", "secret", "token", "api_key", "email", "phone", "address", "ip_address", "dob"],
  });
  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  import zespan

  zespan.init(
      api_key="zsp_your_api_key_here",
      redact_keys=["password", "secret", "token", "api_key", "email", "phone", "address", "ip_address", "dob"],
  )
  ```
</CodeGroup>

### Default redacted keys

If you don't set `redactKeys` / `redact_keys`, both SDKs fall back to the identical built-in list:

```
password, secret, token, api_key
```

<Warning>
  Setting `redactKeys` / `redact_keys` **replaces** the default list — it does not merge with it. If you only pass `["email", "phone"]`, `password` and the other built-in keys are no longer redacted. To keep the built-in protection, list the defaults alongside your own keys, as shown above.
</Warning>

## How key matching works

Both SDKs implement the same normalization and traversal logic — `redact.ts` and `redact.py` are functionally identical:

<CodeGroup>
  ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  // packages/sdk/src/redact.ts
  const normalizeKey = (key: string): string =>
    key.toLowerCase().replace(/[^a-z0-9]/g, "");
  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  # packages/sdk-python/zespan/redact.py
  def _normalize_key(key: str) -> str:
      """Lowercase and remove non-alphanumeric characters."""
      return re.sub(r"[^a-z0-9]", "", key.lower())
  ```
</CodeGroup>

Matching is **case-insensitive** and strips every non-alphanumeric character before comparing, so it catches common naming variants against a single configured key:

* `"password"` matches `password`, `Password`, `PASSWORD`
* `"api_key"` matches `api_key`, `apiKey`, `API_KEY`, `api-key`

### Nested objects, depth limit, and cyclic safety

Matching walks nested objects and arrays recursively, not just the top level, so a matching key several levels deep inside `tool_call_args` or a serialized message array is still redacted.

Two safety limits are built in, and are identical between the two SDKs:

| Limit             | Behavior                                                                                                                                                                                                          |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Max depth: `20`   | Recursion beyond 20 levels returns the string `"[REDACTED_DEPTH_LIMIT]"` instead of continuing to descend                                                                                                         |
| Cyclic references | An object or array already visited on the current path (tracked via `WeakSet` in TypeScript, `id()` tracking in Python) is replaced with `"[Circular]"` instead of causing infinite recursion or a stack overflow |

<CodeGroup>
  ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  const MAX_DEPTH = 20;

  if (depth > MAX_DEPTH) {
    return "[REDACTED_DEPTH_LIMIT]";
  }
  // ...
  if (seen.has(value)) {
    return "[Circular]";
  }
  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  _MAX_DEPTH = 20

  if depth > _MAX_DEPTH:
      return "[REDACTED_DEPTH_LIMIT]"
  # ...
  if obj_id in seen:
      return "[Circular]"
  ```
</CodeGroup>

## Manual redaction (Python)

For cases where you want to redact a value yourself — before logging it or passing it somewhere outside the SDK's automatic capture path — Python exposes the same key-based redaction as a standalone function from the package root.

<CodeGroup>
  ```python Python theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  from zespan import redact_for_storage

  redact_for_storage(value: Any, keys: List[str]) -> str
  ```
</CodeGroup>

<ParamField path="value" required>
  The value to redact. Dicts and lists are redacted recursively; a JSON-encoded string is parsed, redacted, and re-serialized; any other string is returned unchanged. `None` returns `""`.
</ParamField>

<ParamField path="keys" type="List[str]" required>
  Field names to redact, matched with the same case-insensitive, punctuation-stripped normalization described above. This is not automatically the client's configured `redact_keys` — pass it explicitly.
</ParamField>

It returns a JSON string with matching keys replaced by `"[REDACTED]"`:

<CodeGroup>
  ```python Python theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  from zespan import redact_for_storage

  redact_for_storage(
      {"user": "abhi", "password": "hunter2"},
      keys=["password", "secret", "token", "api_key"],
  )
  # '{"user": "abhi", "password": "[REDACTED]"}'
  ```
</CodeGroup>

<Note>
  This helper is public in both SDKs: `redact_for_storage` is re-exported from the Python package root (`zespan/__init__.py`), and `redactForStorage` is exported from `@zespan/sdk`. You can call it standalone to redact a value yourself, or rely on the automatic capture pipeline that applies it for you.
</Note>

## Pattern-based PII detection

Key-based redaction only catches values stored under a matching field name. It won't catch an email address or phone number typed into a user's message, since that's free-form content, not a keyed field. Both SDKs expose a `redactPii` / `redact_pii` option for this, and both run **locally, in your process, before the event is transmitted**, applying key-based redaction first and PII detection second.

<Note>
  The two SDKs detect PII with different technology. **TypeScript** uses the `openredaction` package — ML-based, confidence-scored, with broad compliance presets. **Python** uses a fixed set of standard-library regular expressions for a focused category list (email, phone, credit card, SSN, IPv4); there is no confidence score, so `pii_confidence_threshold` has no effect in Python. Read the per-language sections below for exactly what each catches.
</Note>

### TypeScript: local pattern detection

<CodeGroup>
  ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  zespan.init({
    apiKey: process.env.ZESPAN_API_KEY!,
    redactPii: true,
    piiPreset: "gdpr",
  });
  ```
</CodeGroup>

`redactPii` is **off by default**. When enabled, the TypeScript SDK lazily loads the `openredaction` package and scans the same captured content as key-based redaction — message content, tool call arguments, system prompts, and stored prompt/completion text — for common PII patterns, applying key-based redaction first and PII pattern detection second. Detection failures fail open (the original text passes through) rather than crashing your app.

#### Presets and categories

`piiPreset` selects a bundled set of categories tuned for a compliance context: `gdpr`, `hipaa`, `ccpa`, `pci-dss`, `soc2`, `finance`, `education`, `transportation`.

You can instead set `piiCategories` directly to choose from: `personal`, `financial`, `government_ids`, `healthcare`, `digital_identity`. If both are set, `piiCategories` takes precedence over `piiPreset`.

#### Redaction mode

`piiRedactionMode` controls how a matched value is replaced:

| Mode                    | Behavior                                                |
| ----------------------- | ------------------------------------------------------- |
| `placeholder` (default) | Replace the whole value with `[REDACTED]`               |
| `mask-middle`           | Keep the first and last few characters, mask the middle |
| `mask-all`              | Replace every character with a mask character           |

#### Confidence and exclusions

`piiConfidenceThreshold` sets the minimum confidence, from `0` to `1`, required before a match is redacted — it defaults to `0.7`. `piiWhitelist` lists values that should never be redacted even if they match a pattern.

#### Additional TypeScript-only controls

* `piiIncludeNames`, `piiIncludeEmails`, `piiIncludePhones`, `piiIncludeAddresses` — booleans to toggle individual detectors on, independent of a preset or category list
* `piiCustomPatterns` — an array of additional regular expressions to treat as PII

### Python: regex pattern detection

Enabling `redact_pii=True` runs a set of standard-library regex detectors over the same captured content as key-based redaction — locally, in your process, before the event is sent. Key-based redaction runs first, then PII patterns.

<CodeGroup>
  ```python Python theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  zespan.init(
      api_key="zsp_your_api_key_here",
      redact_pii=True,
      pii_redaction_mode="placeholder",          # placeholder | mask-all | mask-middle
      pii_categories=["email", "phone", "ssn"],  # optional; all supported categories by default
      pii_whitelist=["test@example.com"],        # optional exact-match exclusions
  )
  ```
</CodeGroup>

**Supported categories:** `email`, `phone`, `credit_card` (Luhn-checked to cut false positives), `ssn` (US `###-##-####`), `ip` (IPv4). Leave `pii_categories` empty to redact all of them.

**Redaction mode** (`pii_redaction_mode`):

| Mode                    | Behavior                                            |
| ----------------------- | --------------------------------------------------- |
| `placeholder` (default) | Replace the match with a tag, e.g. `[EMAIL]`        |
| `mask-all`              | Replace the whole match with `*` of the same length |
| `mask-middle`           | Keep the first and last character, mask the middle  |

<Note>
  Python's detection is regex-based, not the ML detector TypeScript uses — it covers a narrower, well-defined set and can occasionally false-positive (the `phone` pattern is intentionally loose). The `pii_preset` and `pii_confidence_threshold` fields exist for cross-SDK API compatibility but do not affect Python matching — use `pii_categories` and `pii_redaction_mode` to control it.
</Note>

## What is NOT redacted — your responsibility

* **Content typed into free-form fields is only caught by `redactKeys` if it sits under a matching key.** An email address embedded inside a user's chat message body is not redacted by key-based redaction — that's prose, not a keyed field. Enabling `redactPii` / `redact_pii` addresses this in both SDKs (TypeScript with `openredaction`, Python with regex — see above).
* **Data outside the SDK's capture path.** Redaction only applies to the fields the SDK captures and sends (messages, tool call arguments, system prompts, prompt/completion text). Anything you log, store, or transmit yourself outside of `zespan`'s wrappers is untouched by any of this.
* **Keys not in your configured list.** If a sensitive field's name isn't in `redactKeys`/`redact_keys` (and doesn't match a default), its value passes through unredacted. Review the fields your application actually sends to LLMs and make sure your key list covers them.
* **`storePrompts: false` disables capture, not redaction.** Setting `storePrompts` (`store_prompts`) to `false` stops `prompt_text`/`completion_text`/`system_prompt` from being captured at all — it isn't a redaction mechanism, and it doesn't affect `tool_call_args`, which is always redacted and always sent when present.

## Redaction and `storePrompts`

| Setting                                               | What is protected                                                                                                                                                                                                                                                     |
| ----------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `redactKeys` / `redact_keys`                          | Field names matched inside structured data captured from LLM calls — message objects, tool call arguments, and similar JSON-shaped fields — before the event is transmitted. Identical behavior in both SDKs.                                                         |
| `redactPii` / `redact_pii`                            | Free-form PII patterns (emails, phone numbers, and more) inside that same captured content — opt-in, off by default, local matching before transmission. TypeScript uses `openredaction` (ML-based); Python uses stdlib regex for a focused category set (see above). |
| `storePrompts: true` / `store_prompts=True` (default) | Prompt and completion text is captured, with `redactKeys` (and, in TypeScript, `redactPii` if enabled) applied first                                                                                                                                                  |
| `storePrompts: false` / `store_prompts=False`         | Prompt and completion text is never captured or sent                                                                                                                                                                                                                  |

<Tip>
  `tool_call_args` is redacted with `redactKeys`/`redact_keys` regardless of the `storePrompts` setting — it's a separate field from prompt/completion text.
</Tip>
