Fluent Commerce Logo
Docs

Fluent CLI & Fluent MCP - PII Scrubbing

Essential knowledge

Intended Audience:

Technical User

Author:

Marco Heuer

Changed on:

14 Oct 2026

Overview

How the Fluent CLI and the Fluent MCP server replace personal and sensitive values in live platform data with placeholder tokens such as `[REDACTED_EMAIL]` before the data is printed or handed to an AI assistant.

Key points

  • Automatic: scrubbing is always on. No flag turns it on or off.
  • Output only: nothing sent to the platform is modified.
  • Where it applies: `fluent gql query`, `fluent event list`, `fluent event send` responses, and the MCP `graphql_execute` tool — both table and `--json` output.
  • Diagnostics stay intact: entity IDs, refs, statuses, location refs, tracking numbers, country codes and business address fields come through unchanged.
  • Setting `lobValue` is exempt so manifests and other JSON settings can be read, edited and written back without saving redaction tokens.
  • Best-effort and rule-based: it matches known field names and a few value patterns. It does not guarantee that all PII is removed.

Overview

Some Fluent CLI commands, and the Fluent MCP server's GraphQL execute tool, return live platform data such as GraphQL query results and orchestration events. Before that data is printed or handed to an AI assistant, it goes through a PII scrubber that replaces personal and sensitive values with fixed placeholder tokens such as `[REDACTED_EMAIL]`.This matters most when an AI assistant drives the CLI or calls the MCP server. Claude Code, Codex, Gemini CLI and similar tools read command and tool output into their context, so scrubbing keeps customer names, contact details, addresses and credentials out of the AI session. Business identifiers stay intact, so you can still debug: entity IDs, refs, statuses, location refs, tracking numbers and country codes all come through unchanged.

Where Scrubbing Is Applied

Command / toolWhat is scrubbed
`fluent gql query`The whole GraphQL response, before it is shown as a table or as `--json`.
`fluent event list`Every event record returned by the Event API (`GET /api/v4.1/event`), including `context` and `attributes`.
`fluent event send`The Event API response (`POST /api/v4.1/event/async` or `/sync`). The request payload is sent as-is.
MCP tool `graphql_execute`The whole GraphQL response, including any `errors[]` returned with it, before it is returned to the AI assistant. If the request fails, the error payload (message and query echo) is also scrubbed.
The bundled `fluent-trace` skill depends on this behaviour. It tells the AI to read redaction tokens as expected values rather than errors, and not to try to recover the original data.

Not Scrubbed

These paths do not go through the scrubber at present:
PathNotes
Other MCP server toolsOnly `graphql_execute` is scrubbed. Other MCP tools (workflow, schema, health and vector tools) return their output unscrubbed.
Error messages from the Event APIWhen the API returns HTTP ≥ 400, the error message contains the raw response data without scrubbing.
Debug and error loggingWith debug logging on, GraphQL request payloads and unsuccessful responses are logged raw.
Unexpected event response shapes`fluent event list` logs the raw response at debug level when it can't find a records array.
Every other CLI commandWorkflow, setting, module and profile commands, and so on, print their output unscrubbed.
The MCP `graphql_execute` error path is scrubbed defensively. Today a failed request only returns the HTTP status line (for example `GraphQL HTTP 400: Bad Request`) and the first 200 characters of the query; the raw response body is never returned to the AI.

How It Works

After the CLI or MCP server receives a response from the platform, and before anything is displayed or returned to the AI assistant, it checks each text value in the response, including values in nested objects and lists. A value is replaced with a placeholder token in any of these cases:
  • Its field name identifies personal or sensitive data. Examples are `email`, `primaryPhone`, `firstName`, `postcode` and `password`. The value is redacted whatever it contains.
  • Its content is recognisably sensitive. Email addresses and `Bearer` tokens are redacted wherever they appear, even under a field name that isn't on the list. Emails embedded in a `message` string are replaced in place.
  • It is the `value` of a name/value attribute whose `name` identifies personal data, for example `{ "name": "email", "value": "..." }`.
All other fields are shown unchanged. The response keeps its original structure, so output stays readable and record counts, refs and statuses can still be used for troubleshooting. Each placeholder names the kind of data removed (for example `[REDACTED_NAME]`), so you can see what was there without seeing the value itself.

Field-Name Rules

Field names are matched case-insensitively and exactly. A key must equal one of the names below; containing one is not enough.
PlaceholderField names
`[REDACTED_EMAIL]``email`, `emailAddress`, `email_address`, `e_mail`, `primaryEmail`, `deliveryEmail`
`[REDACTED_PHONE]``phone`, `primaryPhone`, `phoneNumber`, `phone_number`, `mobile`, `mobileNumber`, `mobile_number`, `telephone`, `tel`
`[REDACTED_PASSWORD]``password`, `passwd`, `pwd`, `secret`
`[REDACTED_USERNAME]``username`, `user_name`, `login`, `loginName`
`[REDACTED_ADDRESS]``address`, `streetAddress`, `street_address`, `addressLine1`, `addressLine2`, `address_line1`, `address_line2`, `street`, `suburb`, `postcode`, `postalCode`, `postal_code`, `zipcode`, `zip_code`, `zip`
`[REDACTED_NAME]``firstName`, `first_name`, `lastName`, `last_name`, `fullName`, `full_name`, `deliveryFirstName`, `deliveryLastName`, `deliveryContact`
`[REDACTED_AUTH]``authorization`, `x-api-key`, `x-token`, `x-auth-token`

Value-Pattern Rules

These apply to string values under any key, including keys that are not in the list above.
PlaceholderPatternMatchesDoes not match
`[REDACTED_EMAIL]``^[^\s@]+@[^\s@]+\.[^\s@]{2,}$``bob@corp.io`, `a+b@x.io``Contact bob@corp.io` (the whole value must be an email), `@nodomain`
`[REDACTED_AUTH]``^Bearer\s+\S+$` (case-insensitive)`Bearer eyJhbGci...`, `bearer abc``Basic dXNlcjpwYXNz`, a bare JWT with no `Bearer` prefix
Under a key named `message` (for example GraphQL `errors[].message`), email addresses inside the text are also replaced: `"No customer found for alice@example.com"` becomes `"No customer found for [REDACTED_EMAIL]"`.

Attribute Rule

Fluent entities carry attributes as `{ name, type, value }` objects, so the PII signal is in the attribute's `name`, not in a key. When an object has a string `name` that equals one of the field names above (case-insensitive) and a non-null `value`, the whole `value` is replaced with `[REDACTED]`, whatever its type.
InputOutput
`{ "name": "email", "type": "STRING", "value": "alice@example.com" }``{ "name": "email", "type": "STRING", "value": "[REDACTED]" }`
`{ "name": "address", "type": "JSON", "value": { "street": "1 Main St" } }``{ "name": "address", "type": "JSON", "value": "[REDACTED]" }`
`{ "name": "loyaltyTier", "type": "STRING", "value": "GOLD" }`unchanged

Preserved Fields

Fields not matched by any rule pass through. Diagnostic identifiers are deliberately kept. These, for example, are not scrubbed: `id`, `ref`, `customer.id`, `status`, `type`, `category`, `trackingNumber`, `carrierRef`, `fromLocationRef`, `country`, `state`, `name`.`name` is kept on purpose. Across the Fluent domain it usually holds an event, ruleset, setting or entity name rather than a person's name.Business address and contact fields are also kept on purpose, because rules match by field name only and cannot tell a customer address from a store or warehouse address. Keeping them preserves sourcing and location diagnosis: `city`, `street2`, `companyName`, `latitude`, `longitude`, `supportEmail`, `supportPhone`, `supportPhoneNumber`, `supportContactName`.

Exempt Fields

Some fields are passed through as a whole: the scrubber does not look inside them at all, so no rule above applies to their contents. Field names are matched case-insensitively.
FieldWhy
`lobValue`Holds the body of JSON Settings such as Mystique manifests (`fc.mystique.manifest.*`) and security headers (`fc.mystique.security.headers`). These are read, edited and written back with `updateSetting`. Scrubbing them would persist redaction tokens into the setting — for example, manifest templates like `"street": "{{orderById.fulfilmentChoice.deliveryAddress.street}}"` would be saved as `[REDACTED_ADDRESS]`.
The bundled skills that write data back (`fluent-graphql-query`, `fluent-mystique-manifest-load-and-save`, `fluent-mystique-security`) also tell the AI never to send a value containing `[REDACTED` in a mutation.

Example

Raw GraphQL Response

1{
2  "orders": {
3    "edges": [
4      {
5        "node": {
6          "id": "1234",
7          "ref": "HD-001",
8          "status": "BOOKED",
9          "customer": {
10            "id": "42",
11            "ref": "CUST-001",
12            "firstName": "Alice",
13            "lastName": "Wong",
14            "email": "alice@example.com",
15            "primaryPhone": "0412345678"
16          },
17          "fulfilmentChoice": {
18            "deliveryAddress": {
19              "street": "1 Main St",
20              "city": "Sydney",
21              "postcode": "2000",
22              "country": "AU"
23            }
24          }
25        }
26      }
27    ]
28  }
29}

Scrubbed Output

1{
2  "orders": {
3    "edges": [
4      {
5        "node": {
6          "id": "1234",
7          "ref": "HD-001",
8          "status": "BOOKED",
9          "customer": {
10            "id": "42",
11            "ref": "CUST-001",
12            "firstName": "[REDACTED_NAME]",
13            "lastName": "[REDACTED_NAME]",
14            "email": "[REDACTED_EMAIL]",
15            "primaryPhone": "[REDACTED_PHONE]"
16          },
17          "fulfilmentChoice": {
18            "deliveryAddress": {
19              "street": "[REDACTED_ADDRESS]",
20              "city": "Sydney",
21              "postcode": "[REDACTED_ADDRESS]",
22              "country": "AU"
23            }
24          }
25        }
26      }
27    ]
28  }
29}

Redaction Tokens Reference

When output contains one of these tokens, the original value was present and has been removed. Read them as expected values, not as data errors.
TokenMeaning
`[REDACTED_EMAIL]`Email field, a value shaped like an email address, or an email inside a `message` string
`[REDACTED_PHONE]`Phone, mobile or telephone field
`[REDACTED_ADDRESS]`Address, street, suburb, postcode or zip field
`[REDACTED_PASSWORD]`Password or secret field
`[REDACTED_USERNAME]`Username or login field
`[REDACTED_NAME]`First, last, full or delivery contact name field
`[REDACTED_AUTH]`Auth header field, or a `Bearer` token value
`[REDACTED]`Value of a name/value attribute whose name is a PII field

Limitations

The scrubber is rule-based. It lowers the chance of exposing PII but does not guarantee that all PII is removed. Known gaps:
GapExample that is not redacted
Exact field-name match only: prefixed or compound names are missed`customerEmail`, `billingAddress` (string), `contactName`
Business address fields are kept, so customer values in them pass throughCustomer `city`, `street2`, `companyName`, `latitude`, `longitude`
Field-name rules apply to every entity, so business values in matching fields are redacted tooStore and warehouse `street` and `postcode`
Location fields outside the list`state`, `region`, `country`
Only string values are checked`"phone": 61412345678`, `"postcode": 2000` (numbers)
Strings directly inside arrays have no key, so only nested objects are checked`"emails": ["alice@example.com"]`
Attribute names must match a field name exactly`{ "name": "customerPhone", "value": "0412345678" }`
Embedded matching only covers emails in `message` stringsFree text such as `"note": "Call Alice on 0412 345 678"` or `"note": "alice@example.com, VIP"`
Common credential key names outside the list`token`, `accessToken`, `access_token`, `apiKey`, `client_secret`, `refreshToken`
Unprefixed tokensA raw JWT without the `Bearer` prefix
Object-valued `address` is not redacted as a whole`"address": { ... }` is recursed into, and only matching child keys are redacted
Nesting deeper than 32 levels is returned as-isValues below the 32nd nested object or array
Setting `lobValue` is exempt, so anything stored inside it is shownCredentials or contact details in JSON settings, e.g. an SMS service `username` or `fromEmail`
The attribute rule applies to any object with `name` and `value`A Setting literally named `email` or `phone` has its short `value` redacted
For workloads with strict data-handling requirements, also limit what the AI can see at the source. For example, select only the fields you need in GraphQL queries, and use non-production data where possible.