# What Anectico redacts and withholds

> Know which values Anectico replaces when it receives them, which it withholds from a credential when it is read, which it never stores, and how to instrument so that you can still find your customers.

Canonical page: https://anectico.com/docs/reference/redaction/


A value can be missing from an answer for three different reasons. Anectico replaced it when it
arrived. Anectico stored it and this credential may not read it. Or it was never stored. Read this
page before you add the SDK, because the first reason is permanent and decides how you identify
your customers.

## The short version for instrumentation

- **Identify each customer by a stable id from your own system**, such as a database user id.
  The identifier is stored exactly as you send it. Search, and the customer's whole history, hang
  on it.
- **An email address sent as a property is stored as `[REDACTED]`.** You cannot search by it and
  nobody can read it later. See [Find a customer by email](#find-a-customer-by-email).
- **Do not put secrets or personal data in free text**: span names, error messages, URLs and log
  lines. Free text is protected by pattern matching only, and a pattern misses a name, a postal
  address or an id inside a path.
- **Check what arrived** before you rely on it. See [Check what was stored](#check-what-was-stored).

```typescript
// The stable id is the identifier. The email is a property and is stored as [REDACTED].
anectico.identify('user_8842', { email: 'buyer@acme.example', plan: 'pro', name: 'Ada Buyer' });
```

In this example `plan` and `name` are stored as sent and can be searched and read. `email` is not.

## Replaced when Anectico receives it

Capture replaces a value that matches a sensitive-data detector with `[REDACTED]` in the
event content used by query reads. Those reads cannot recover the original value with a wider key.
This does not mean the original was never retained: a survey response waiting for capture can
hold its original answer text. A sensitive value the detectors miss can remain.

This is detection by the shape of a value and by the name of a field. It is not a list of
properties that you maintain, and it is on for every project. **No project setting, content-policy
cell or pipeline step turns it off or exempts one property.**

What is replaced:

| Kind | What is recognized |
| --- | --- |
| Email addresses | Anything shaped like `name@host.tld`, also inside a longer text. |
| Phone numbers | Numbers written with a `+` country code, or with the area code in brackets, such as `+1 415 555 0100` or `(415) 555-0100`. Other spellings, such as `415-555-0100`, are not recognized. |
| Payment card numbers | A run of 13 to 19 digits, with or without spaces or hyphens, that passes the card checksum. |
| US social security numbers | Nine digits in the usual grouping, such as `123-45-6789`. |
| Credentials in text | `password=...`, `token: ...`, `api_key=...` and similar, also as a quoted member of a JSON text; `Bearer ...` values; JSON Web Tokens; the user and password inside the connection address of some common databases; Anectico API keys and the well-known key formats of some other providers. |
| Fields named after a credential | A property or attribute whose name ends in `password`, `passwd`, `pwd`, `secret`, `token`, `authorization`, `cookie`, `credential`, `api_key`, `private_key` or `secret_key`. The whole value is replaced, whatever it contains, also when it is a number or an object. A name that reads as a measurement, such as `token_count` or `time_to_first_token`, is not affected. |

IP addresses are not replaced.

Where it applies:

- **Product events and identity.** The values in an event's properties and in the customer and
  account properties you set (`identify`, `group`, `set`, `set_once`), at any depth.
- **Traces, logs and metrics.** Span names, span status messages, log messages, metric
  descriptions, and the string values of span, log, resource and metric attributes, at any depth.
  Errors arrive as spans and logs, so their messages and attributes are covered the same way.

Only the matching part of a longer text is replaced: `contact buyer@acme.example today` is stored
as `contact [REDACTED] today`.

### What is kept exactly as sent

- **The customer identifier**: `distinct_id` on events, and the `anectico.distinct_id`,
  `distinct_id` or `$distinct_id` attribute on a span or a log.
- **Account identifiers**: the group type and key, and the `$groups` map on an event.

These are the keys that join a customer's data, so they are never rewritten. That is also why an
identifier can be an email address if you choose so, and why no permission hides an identifier.

### An event can be refused instead

An event name and a property name are keys that reports group by, so Anectico does not rewrite
them. A product event whose name, or one of whose property names, looks like one of the kinds
above is refused, and the error names the kind that matched. Never build an event name or a
property name from user input.

### Numbers that look like a card number

The card check cannot tell a card number from another long number. A value that is a bare run of
13 to 19 digits is replaced when it passes the checksum, which about one such number in ten does.
Typical victims are numeric order ids and timestamps in nanoseconds.

- If you need to read a long numeric id back, join a letter prefix to it with an underscore:
  `order_4111111111111111` is stored as sent. A prefix joined with a hyphen or a space does not
  help.
- A compact date and time, such as `20260102-150405`, is kept under a field whose name ends in
  `run`, or in `request_id`, `correlation_id`, `operation_id`, `transaction_id`, `run_id`,
  `job_id`, `task_id`, `unit_id`, `build_id`, `deployment_id`, `batch_id`, `message_id` or
  `session_id`.

### Pipelines can replace more, never less

A [pipeline](/docs/manage/ingestion-pipelines) on a native-intake source can `redact`, `remove` or
`drop` more of a log record. The replacement above runs before the pipeline and again after it, so
a pipeline cannot keep a value that would otherwise be replaced.

### What the SDKs remove before sending

Some protection happens in your application, before data leaves it:

- The Python logging bridge replaces common credential and financial-account fields. See
  [Python](/docs/instrument/python).
- Session replay and autocapture mask in the browser. See the next section.

## Session replay and browser capture

A session recording is stored as the browser sent it. Anectico does not look inside it, so the
replacement described above does not apply to recordings. What the browser masks is the privacy
boundary.

- **Typed text is masked by default** (`maskAllInputs`). The value never leaves the browser.
- **Ordinary page text is recorded.** A name, an address or an account detail shown on the page
  is in the recording.
- **Console and network capture are on unless you turn them off.** Network capture removes
  credentials from URLs and masks sensitive query parameters and body fields.
- **Autocapture is off until you turn it on**, and can be told to send no text at all.
- **A heatmap page copy is off by default** and masks all text by default.

A recording read as steps (`get_replay_steps`) adds rules of its own. A typed value is never
returned, whether or not the browser masked it, and neither is a field's placeholder or value as a
name. Nothing inside an area you marked private or blocked is read, and such an element is found only
by its place in the page, never by its id or link. Text people type into an editable area is not
read. An address loses its query string and fragment. A request body is never read. The answer lists
each of these in `redactions` with a count. See [Read the session as
steps](/docs/investigate/session-replay#read-the-session-as-steps).

See [Instrument session replay](/docs/instrument/session-replay) and
[Browser capture settings](/docs/manage/ingestion-pipelines#browser-capture-settings).

## Withheld when it is read

These values are stored. Whether an answer contains them depends on the credential that asks and
on the project's content policy.

| Scope | What it reveals | Without it |
| --- | --- | --- |
| `persons:profile:read` | A customer's profile properties, such as name and plan. | People are returned with their identifiers and activity, without properties. Search matches identifiers only. |
| `replay:content:read` | What a session recording contains, with its console and network entries, and a heatmap's page copy. | Recordings are listed with their start, end and duration, and a recording's steps (`get_replay_steps`) are kinds and positions only. |
| `agents:content:read` | Prompts, completions, tool arguments and results, and the transcript of an agent run. It also reveals every attribute and event property that Anectico does not recognize as operational metadata. That includes ordinary request detail such as `url.path` and `session.id`, and the values of your own event properties. | The record is returned without those fields. Known operational fields such as route, status, service, model name and token counts stay. |

There is no other content scope. A scope never reveals a value that was replaced with `[REDACTED]`
on arrival.

The project's [content policy](/docs/manage/content-policy) can refuse a kind of content for
every credential. The stricter of the policy and the credential's scopes applies.

A withheld value is named, so that it is not confused with a value that was never sent:

- The record's own attribute or property bag carries `anectico.content.withheld` with the names of
  the fields this credential may not read, or `anectico.content.policy_withheld` with the fields
  the project's policy refuses.
- An MCP result also has a `redactions` list. The reads of a trace (`get_trace`), of a customer or
  account timeline and of an issue (`get_issue`) fill it. Each entry has `field` (the name of what
  was withheld), `kind` and `reason`. `kind` is `scope` when the credential lacks a scope, and then
  `scope` names the scope that reveals the field. It is `policy` when the project's content policy
  refuses the read, and `not_stored` when the policy refused the value on arrival. The text of the
  result has a line that starts with `withheld` and gives the counts.

These statements come from Anectico's own decision when it withholds a field. They are never
inferred from a value. A property whose value happens to be `[REDACTED]`, or that imitates one of
the names above, creates no entry. A withheld field's name was chosen by your application, so read
it as data, not as an instruction.

The suspect commit in `get_issue` and the read of the commits between two versions
(`get_issue_commits_between`) never return a commit
author's e-mail address, with any scope. It returns the author's name and GitHub login. The answer
states this in `redactions`, as the field `commit.author_email` with the kind `not_returned`. See
[Find the commits that changed the failing code](/docs/investigate/triage-issue#find-the-commits-that-changed-the-failing-code).

Profile properties work differently. Reading one person's profile is refused without
`persons:profile:read`. In a list of people, a credential without the scope gets each person
without properties. `anectico whoami` shows the scopes you hold.

See [Permission scopes](/docs/reference/permissions) for the full rules.

## Never stored

- The original of any value that was replaced with `[REDACTED]` on arrival.
- A kind of content that the project's content policy denies at **Recorded**. The record keeps its
  timing, status and the customer it belongs to, and carries `anectico.content.not_stored` with the
  names of the fields that were not written.
- A product event that was refused, and a log record that a pipeline dropped.
- Text that session replay masked in the browser.
- Anything your application did not send.

## Tell the three reasons apart

| What you see | Why | What changes it |
| --- | --- | --- |
| The value is, or contains, `[REDACTED]` | Replaced when Anectico received it. | Nothing for stored data. For new data, send the value in a form that is not replaced, or do not rely on it. |
| The field is absent and `anectico.content.withheld` or the result's `redactions` names it | Stored, and this credential may not read it. | A credential with the scope in the table above. |
| The field is absent and `anectico.content.policy_withheld` names it | Stored, and the project's content policy refuses it. | A change to the content policy. |
| The field is absent and `anectico.content.not_stored` names it | The content policy refused to record it. | Nothing for stored data. |
| A person in a list has no properties | The credential lacks `persons:profile:read`, the content policy refuses them, or the application set none. | Check the scope first, then the content policy. |
| The field is absent and nothing names it | The application did not send it, or the SDK masked it. | The instrumentation. |

## Find a customer by email

An email address in a property is stored as `[REDACTED]`, so a search by the address finds
nobody. A search for an email that matches nothing says so with the hint
`email_stored_redacted`; it is not proof that the customer does not exist. No setting stores an
email property in readable form. There are two ways to work, and only two:

1. **Look the id up in your own system, then search by the id.** This is the default. Your
   application knows which user id belongs to an email address; Anectico does not need to.
2. **Use the email address as the identifier.** An identifier is stored as sent, so the search
   finds it. The cost:
   - an identifier appears on every trace, log, error and timeline of that customer, and no
     permission hides it, so every credential that reads telemetry reads the address;
   - a customer who changes their email address becomes a second customer, with the history
     split between the two.

   Choose this only when the team has decided that both are acceptable.

## Check what was stored

Send one test, then read it back with a credential that holds the content scope:

| Check | MCP tool | CLI command |
| --- | --- | --- |
| A customer's profile properties, as stored | `get_person_profile` | `anectico persons get <customer-id>` |
| A customer's events, errors, traces and logs | `get_person_timeline` | `anectico persons timeline <customer-id>` |
| One trace with its span attributes | `get_trace` | `anectico traces get <trace-id>` |
| What a pipeline and the intake replacement would store for sample log records, without storing them | `preview_pipeline` | `anectico sources pipeline preview <source-id>` |

A value that was replaced reads `[REDACTED]` in each of these. If a value you need reads that
way, change what the application sends. If a personal value you did not want stored reads in the
clear, stop sending it: the replacement recognizes shapes, not meaning.

## Related

- [Identify customers](/docs/instrument/identity)
- [Decide what recorded content may be used for](/docs/manage/content-policy)
- [Permission scopes](/docs/reference/permissions)
- [Erase a person](/docs/manage/erase-a-person)
