Mask Card Numbers in Email Content Read by Agents
Masks payment-card-number (PAN) shapes in email content returned to agents by Gmail mailbox-read tools.
- Direction
- egress
- Rego package
gmail.egress.mask_pan- App
- gmail
- Bundles
- soc2pci-dssgdpr-ccpa
- Published
- Minimum gateway
- 1.0.0b24
- Schema version
- 1.0.0
- Checksum
sha256:879ee13a0594096f178508fd7569014f342d1e43e87ef96a86818554b85cbc7e
gmailmask-pan-egressegressemailcardholder-datadlpsoc2pci-dssgdpr-ccpa
What this policy does
Direction: egress (tool_post_invoke)
Default: allow (transform-only — never denies)
Package: gmail.egress.mask_pan
What it does
Masks payment-card-number (PAN) shapes in email content returned to agents
by Gmail mailbox-read tools. When an agent reads a message, thread, or
search result, any 13–19-digit card-number-shaped string in the response —
with or without space/dash separators — is replaced with [PAN REDACTED]
before the content reaches the agent.
The policy never blocks a call. It declares redact_patterns that the
gateway applies over the response text (input.payload.text), so mailbox
reads keep working and only the card numbers are masked.
Callers in a documented IdP group (placeholder: pci-full-pan) skip the
transform and see unmasked content. The exemption is fail-closed: a caller
with no claims, no groups claim, or a malformed groups claim is never
exempt.
Compliance alignment
- SOC 2 CC6.7 — supports the restriction on movement/removal of information: masking PANs before mailbox content reaches the agent keeps cardholder data from being relayed out of Gmail over the MCP path.
- SOC 2 C1.1 — supports identification and protection of confidential information: card numbers are treated as confidential and masked on the agent channel by default, with full-PAN visibility limited to a defined role.
- PCI DSS 3.4.1 — supports masking of PAN when displayed: the agent
channel shows
[PAN REDACTED]instead of full card numbers, with visibility of full PAN limited to a defined role (pci-full-pan). - PCI DSS 3.4.2 — supports preventing copy/relocation of PAN via remote access: an agent that never receives the full PAN cannot re-post it into tickets, chats, or files.
- PCI DSS 12.5.2 / 12.10.7 — supports PCI scope control: masking on the mailbox-read path is a backstop against cardholder data creeping into agent context from email, one of the classic PAN-where-not-expected channels.
- CCPA/CPRA §1798.150 — supports reducing exposure of nonredacted personal information: card numbers surfaced to agents from mailboxes are redacted by default.
- Also maps to ISO 27001 A.8.11 (data masking) on the MCP path, if you track that framework.
Tool name matching
The policy matches Gmail mailbox-content read tools case-insensitively by
suffix on input.resource.name, covering all three MCP-server vocabularies
in real use:
*get_thread— Google official / Claude Gmail connector; itsFULL_CONTENTformat returnsplaintextBodyfor every message in the thread, making this the main egress surface.*search_threads— Google official / Claude connector; results include ~200-char body snippets.*read_email,*search_emails— GongRzhe/Gmail-MCP-Server.*get_gmail_message_content,*get_gmail_thread_content,*get_gmail_messages_content_batch,*get_gmail_threads_content_batch,*search_gmail_messages— taylorwilsdon/google_workspace_mcp, including the batch variants that return dozens of full bodies per call.
The DTwo gateway prefixes tool names with the configured MCP server name
(e.g. gmail-mcp-get_thread), and that prefix is not standardized —
suffix matching keeps the policy portable. Verify the exact names your
gateway sends with the dump-input debug technique before relying on this
in production, and add suffixes if your Gmail MCP server uses different
read-tool names.
Patterns matched
Conservative PAN shapes only — each pattern is commented in the Rego:
- 16-digit PANs grouped 4-4-4-4 with space or dash separators (Visa/Mastercard/Discover print format).
- 15-digit American Express PANs grouped 4-6-5, constrained to the 34/37 IIN range.
- Unseparated 13–19-digit runs (the ISO/IEC 7812 PAN length range).
Response shape
Egress transforms are applied by the gateway over the serialized response
content blocks (input.payload.text). The patterns are byte-level and not
JSON-aware, so they mask PANs wherever they appear in the response —
plaintextBody fields, snippets, subjects — without the policy needing to
parse the tool-specific JSON shape.
Examples
Transformed (masked)
{
"input": {
"action": "tool_post_invoke",
"mode": "output",
"resource": { "name": "gmail-mcp-get_thread", "type": "tool" },
"payload": {
"name": "gmail-mcp-get_thread",
"text": ["{\"messages\":[{\"plaintextBody\":\"Card: 4111 1111 1111 1111, exp 12/27\"}]}"]
},
"subject": { "sub": "google-apps|casey@acme.com", "claims": { "groups": ["support"] } }
}
}
allow = true; transform emits the PAN patterns with replacement
[PAN REDACTED], so the agent sees Card: [PAN REDACTED], exp 12/27.
Allowed unmasked (exempt group)
{
"input": {
"action": "tool_post_invoke",
"mode": "output",
"resource": { "name": "gmail-mcp-get_thread", "type": "tool" },
"payload": {
"name": "gmail-mcp-get_thread",
"text": ["{\"messages\":[{\"plaintextBody\":\"Card: 4111 1111 1111 1111\"}]}"]
},
"subject": { "sub": "google-apps|pci-analyst@acme.com", "claims": { "groups": ["pci-full-pan"] } }
}
}
allow = true, no transform — the caller is in the pci-full-pan group.
Composition
This policy masks card numbers only. Useful companions:
- A companion Gmail PII redaction egress policy (PF-02
redact-pii-egress) for SSNs, national IDs, and credentials/secrets in mailbox content — one policy, one job; card data and broader PII are separate concerns with separate exemption groups. apps/gmail/cap-bulk-export(PF-08) for volume control on the batch read tools (get_gmail_messages_content_batch,get_gmail_threads_content_batch, searchmaxResults) — masking does not stop mass harvesting of masked content.apps/gmail/guard-external-sendandapps/gmail/guard-mailbox-persistencefor the outbound and persistence sides of the mailbox.
Known limitations
- Regex cannot Luhn-validate. Rego pattern matching is pure regex, so
matches are card-number shapes, not verified PANs, and fixed-string
replacement masks the entire match — BIN+last4 preservation (the usual
PCI display format) is not achievable here; treat
[PAN REDACTED]as the display format on the agent channel. - Byte-level over-matching. The patterns run over the serialized response bytes, so digit runs that merely look like PANs are masked too: 13-digit Unix millisecond timestamps, 13–19-digit order/tracking/account numbers, and digit-only 4-4-4-4 groups inside UUID-like identifiers. The grouped patterns require separators and the run pattern stops at 19 digits to limit this, but false positives are inherent — test against representative mailbox data.
- Obfuscated PANs are missed. Card numbers split across content
blocks, separated by characters other than space/dash (dots, unicode
spaces), spelled out, inside images, or base64-encoded in MIME parts do
not match. Runs of 20+ digits also do not match by design. Three regex
edges are worth calling out explicitly, because a red-team review found
them and they are inherent to the pattern set, not bugs:
- ASCII digits only. The patterns use
\d, which matches only[0-9]. Fullwidth (4111…) or Arabic-Indic (٤١١١…) digit variants are not masked. - Only two grouped shapes. Separated PANs are caught only in the
4-4-4-4 (16-digit) and 4-6-5 (Amex) print groupings. A PAN typed with
spaces in some other arrangement (e.g.
41111111 11111111, an 8+8 split, or4111 111111111111, a 4+12 split) matches neither the grouped patterns nor the unseparated-run pattern (the space breaks the run into sub-13-digit pieces). - Word-boundary anchoring. The unseparated-run pattern requires a
word boundary on both sides, so a digit run glued directly to letters
(
card4111111111111111x) is not masked. Real mailbox content almost always surrounds numbers with quotes/spaces/punctuation, so this is a contrived rather than common evasion — but it is a true residual.
- ASCII digits only. The patterns use
- Only mailbox-body read tools are matched — draft/label/filter reads
are not. The suffix allowlist deliberately covers message/thread/search
content tools only. The Google/Claude connector's
list_drafts, and the community servers' label/filter listing reads (list_email_labels,list_filters,get_filter,list_gmail_labels,list_gmail_filters), are not matched, so a card number sitting in a draft body (e.g. a human-composed draft the agent reads back vialist_drafts) is returned unmasked. This is a scoping choice, not a fix: draft/label/filter response shapes vary andlist_draftsmay return only metadata on some servers. If drafts routinely carry cardholder data in your environment, either addlist_drafts(and your server's draft-read suffix) tomailbox_read_suffixesafter verifying its response shape with the dump-input technique, or gate draft reads at ingress with a group-gated deny. - Attachment content is not maskable.
get_gmail_attachment_content(taylorwilsdon) anddownload_attachment(GongRzhe) return base64/binary data that byte-level regex cannot reliably mask — deliberately out of scope here. Gate those tools at ingress instead (e.g. a group-gated deny policy on attachment-content tools). - Egress masking only. The card number still exists in the mailbox and in Gmail's own UI; this policy controls what the agent sees on the MCP path.
- Group names are placeholders — replace
pci-full-panwith your IdP's group name at import time. The exemption readsinput.subject.claims.groups; confirm your IdP emits agroupsclaim (see the DTwo identity-claims documentation).
Compliance note. This policy supports alignment with the cited framework controls on the MCP path only. No policy or bundle makes an organization compliant with any framework; web-UI, native-API, and in-app access are outside the gateway's reach by design. Validate against your own compliance program before relying on it.
Policy source (Rego)
package gmail.egress.mask_pan
# Transform-only policy — never denies, only masks card-number shapes
# in mailbox content returned to the agent.
default allow := true
# Conservative PAN-shaped patterns. These are applied byte-level over the
# serialized response, so each is anchored with \b word boundaries to limit
# over-matching. Pure regex cannot Luhn-validate — matches are card-number
# shapes, not verified PANs (see Known limitations).
pan_patterns := [
# 16-digit PANs grouped 4-4-4-4 with space or dash separators
# (Visa / Mastercard / Discover print format, e.g. 4111 1111 1111 1111).
# Separators are required here; unseparated PANs are caught by the
# digit-run pattern below.
`\b\d{4}[ -]\d{4}[ -]\d{4}[ -]\d{4}\b`,
# 15-digit American Express PANs grouped 4-6-5 with space or dash
# separators, constrained to the 34/37 IIN range (e.g. 3782 822463 10005).
`\b3[47]\d{2}[ -]\d{6}[ -]\d{5}\b`,
# Unseparated 13-19 digit runs — the ISO/IEC 7812 PAN length range
# (Visa 13/16, Mastercard 16, Amex 15, Discover 16, JCB 16-19).
# Runs of 20+ digits never match: there is no word boundary inside a
# digit run, so this cannot partially mask a longer identifier.
`\b\d{13,19}\b`,
]
# Gmail mailbox-content read tools across the three MCP-server vocabularies
# in real use (Google official / Claude connector, GongRzhe, taylorwilsdon).
# The gateway prefixes tool names with the configured server name, so we
# match on the suffix to stay portable. Attachment-content tools are
# deliberately absent — base64/binary output is not regex-maskable; gate
# those at ingress instead.
mailbox_read_suffixes := [
# Google official / Claude connector — FULL_CONTENT returns plaintextBody
# for every message in the thread (the main egress surface).
"get_thread",
# Google official / Claude connector — results carry body snippets.
"search_threads",
# GongRzhe/Gmail-MCP-Server.
"read_email",
"search_emails",
# taylorwilsdon/google_workspace_mcp, incl. high-volume batch variants.
"get_gmail_message_content",
"get_gmail_messages_content_batch",
"get_gmail_thread_content",
"get_gmail_threads_content_batch",
"search_gmail_messages",
]
is_mailbox_read_tool if {
name := lower(input.resource.name)
some suffix in mailbox_read_suffixes
endswith(name, suffix)
}
# Callers in the placeholder full-PAN group see unmasked content.
# Fail-closed: missing subject, missing claims, missing groups, or a
# malformed groups claim all leave this rule undefined, so the transform
# applies. The is_array guard is load-bearing: without it a groups claim
# shaped as an object (e.g. {"role":"pci-full-pan"}) would iterate its
# *values* and match, granting the exemption to a caller who never held
# the group in an array. Requiring an array keeps every non-array shape
# (string, object, number, null) fail-closed.
# Replace "pci-full-pan" with your IdP's group name at import time.
caller_may_view_full_pan if {
claims := object.get(object.get(input, "subject", {}), "claims", {})
groups := object.get(claims, "groups", [])
is_array(groups)
some group in groups
group == "pci-full-pan"
}
# Mask PAN shapes in mailbox-read responses for non-exempt callers.
transform := {
"redact_patterns": pan_patterns,
"replacement": "[PAN REDACTED]",
} if {
input.mode == "output"
is_mailbox_read_tool
not caller_may_view_full_pan
} Canonical source: policy.md on GitHub · raw · raw on this site (.md)
Related policies
Airtable: Redact PII in Record Reads
Scans the responses of the Airtable record-read tools — the calls that return row fields values — and rewrites high-confidence PII shapes to a fixed…
Asana: Redact PII in Task & Comment Reads
On the Asana MCP read path, this transform scans the free-text business fields that ride back in task, comment/story, and status-update responses — notes,…
BigQuery: Redact PII in Query Results
Scans the content returned by BigQuery's result-returning tools and rewrites high-confidence PII shapes to fixed, non-recoverable redaction tokens before the…
Block Agent Email to External Recipients
Blocks agent-initiated Microsoft 365 email sends when any recipient address falls outside a corporate-domain allowlist.
Block BigQuery Exfiltration and Cross-Project Writes
Inspects the raw GoogleSQL string carried by BigQuery SQL tools and denies any statement that moves data out of the tenant's own project — even when the call…
bigqueryguard-warehouse-exportingresssqlexfiltrationsoc2pci-dssgdpr-ccpa
Block Bulk Export & External Staging (Snowflake)
Blocks Snowflake SQL-execution tool calls whose query text moves whole tables off the Snowflake perimeter — bulk export to cloud storage or a stage, and…
snowflakeguard-warehouse-sqlexportexfiltrationingresssoc2pci-dssgdpr-ccpa