---
title: Scrubbing
description: The default rules that clear secrets and personal data before an error is stored, and the policy an environment adds on top of them.
type: explanation
product: monitoring
summary: What is removed before an error is stored, and how an environment adds to it
updated: 2026-09-28
order: 2
---

Scrubbing is the line between a useful error report and a stored credential. A
Scrubbing Policy is what is removed from every captured error before it is
stored, and it runs on the way in rather than on the way out: the occurrence
that is written is the occurrence after the rules have been applied, so a
secret that a stack trace carried is not in the record you read back later. The
`stack` field is scrubbed with the rest of the occurrence.

## The default rules

The defaults always apply, to every occurrence, whatever the environment's
policy says:

- Fields named like a password, a secret, a token, a key, a cookie, a session,
  an authorization or a card are replaced with `[Filtered]`.
- Card numbers, bearer tokens, JWTs and Sylphx secret keys are replaced
  wherever they appear.
- Email addresses are replaced with `[email]` unless `keep_emails` is set.

The first rule reads a name, the second reads a value, and together they catch
the two ways a secret reaches a stack trace: carried in a field whose name says
what it is, or pasted into one whose name does not.

## An environment's policy adds to them

The environment's [Scrubbing Policy](/docs/api/scrubbing_policies) adds to the
defaults; it cannot remove them. There is no setting that turns the default
rules off, which is the reason a policy can be written by whoever owns the
application without anyone having to read it as a security review.

<PropertyTable
	properties={[
		{
			name: 'denied_fields',
			type: 'string[]',
			description: 'More field names to replace with [Filtered] wherever they appear in tags, extra data, request data, or breadcrumb data (case-insensitive, matched as a substring).',
		},
		{
			name: 'keep_emails',
			type: 'bool',
			description: 'Keep email addresses instead of replacing them with [email].',
		},
	]}
/>

`denied_fields` is matched case-insensitively and as a substring, so one name
covers every field that contains it — `national_insurance` covers
`national_insurance_number` and `customerNationalInsurance`. Add a name when
the defaults do not know what your domain calls the thing that must not be
stored.

`keep_emails` exists because an email address is often the identifier you need:
for an application whose users are told their address appears in error reports,
keeping them makes a group readable. It is the one default rule an environment
may switch off, and switching it off is a decision about your users rather than
about the platform.

## One policy per environment

A policy is a singleton, and its name says so:

<KeyValue
	items={[
		{ key: 'Name', value: 'orgs/{org}/projects/{project}/envs/{env}/scrubbing_policies/default', mono: true },
		{ key: 'Id', value: 'scp_<env>', mono: true },
	]}
/>

There is one per environment and no call that creates another. `meta.etag`
fences updates: send the etag you read and the write lands only if nothing has
changed since; a mismatch fails with `ETAG_MISMATCH` and writes nothing. Two
people editing the policy at once is a conflict rather than a silent overwrite.

## Read it

Reading the policy needs `observability:read`. It answers with the environment's
own fields — the extra names it filters and the email switch — and the metadata
every resource carries. The default rules are not listed beside them, because
they are not the environment's to state.

```bash
sylphx observability scrubbing-policies get orgs/acme/projects/shop/envs/production/scrubbing_policies/scrubbing-policy
```

```bash
curl "https://api.sylphx.com/v1/orgs/acme/projects/shop/envs/production/scrubbing_policies/scrubbing-policy" \
  -H "Authorization: Bearer $SYLPHX_API_KEY"
```

## Add to it

Writing the policy needs `observability:write`, and it takes an `update_mask`:
the fields you name are the fields written, and an unset mask writes every
populated field.

```bash
sylphx observability scrubbing-policies update orgs/acme/projects/shop/envs/production/scrubbing_policies/scrubbing-policy
```

```bash
curl -X PATCH "https://api.sylphx.com/v1/orgs/acme/projects/shop/envs/production/scrubbing_policies/scrubbing-policy" \
  -H "Authorization: Bearer $SYLPHX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"orgs/acme/projects/shop/envs/production/scrubbing_policies/scrubbing-policy"}'
```

```ts
const response = await sylphx.observability.scrubbingPolicies.update({ scrubbingPolicy: { name: 'orgs/acme/projects/shop/envs/production/scrubbing_policies/scrubbing-policy' } })
```

`--denied-fields` names one more field to filter and repeats for each;
`--keep-emails` flips the email rule. The
[scrubbing_policies commands](/docs/cli/scrubbing_policies) show both flags.

## Why the floor is the point

A policy that could remove a default rule would be a policy nobody could read
at a glance: every reader would have to check what the environment had turned
off before deciding whether a report was safe to keep. Making the policy
additive is what lets the two questions stay separate — the defaults are the
platform's answer to what must never be stored, and the environment's answer is
only what else it wants gone.

<RelatedDocs
	links={[
		{
			href: '/docs/monitoring/errors',
			label: 'Errors',
			description: 'Capture an occurrence, and what the stack is read for.',
		},
		{
			href: '/docs/monitoring/source-maps',
			label: 'Source maps',
			description: 'Upload the map that symbolicated the frames before grouping.',
		},
		{
			href: '/docs/platform/keys-and-scopes',
			label: 'Keys and scopes',
			description: 'What observability:read and observability:write grant.',
		},
	]}
/>
