Freeze Destructive Gmail Operations
Denies the irreversible destruction surface that community Gmail MCP servers expose — permanent email deletion, label deletion, and filter deletion — for…
- Direction
- ingress
- Rego package
gmail.ingress.freeze_destructive_ops- App
- gmail
- Bundle
- soc2
- Published
- Minimum gateway
- 1.0.0b24
- Schema version
- 1.0.0
- Checksum
sha256:4238b7fbabe7f23035708c0160fcd168734ee90ad4820f45372b9abb67ba801c
gmailfreeze-destructive-opsrecord-integrityingresssoc2
What this policy does
Direction: ingress (tool_pre_invoke)
Default: deny on match, allow otherwise
Package: gmail.ingress.freeze_destructive_ops
What it does
Denies the irreversible destruction surface that community Gmail MCP servers expose — permanent email deletion, label deletion, and filter deletion — for every caller. All other tool calls pass through unchanged.
The community servers are where the risk lives. GongRzhe/Gmail-MCP-Server ships delete_email and batch_delete_emails (both permanent — they bypass the trash entirely, and the batch variant destroys up to 50 messages per call), plus delete_label and delete_filter. taylorwilsdon/google_workspace_mcp folds destruction into manage_gmail_label and manage_gmail_filter behind an action argument — this policy denies those two tools only when action is delete, so their list/get/create/update actions pass through for other policies to govern.
There is deliberately no identity exemption: a mailbox record must survive agent error and prompt injection regardless of who is driving the agent. The deny reason steers the agent to the reversible alternatives — archive via label modification (modify_email / batch_modify_gmail_message_labels) or move the message to trash — and points legitimate deletion needs at InfoSec, outside the agent channel.
The official Google remote Gmail MCP server (the surface behind Anthropic's Claude Gmail connector) has no delete tools of any kind — no message, label, or filter deletion. On that surface this policy matches nothing and is inert by design; that is the expected state, not a coverage gap. Attach it anyway: it costs nothing there and becomes load-bearing the moment a tenant swaps in a community server, which is exactly the migration that silently adds permanent-delete primitives.
Compliance alignment
- SOC 2 PI1.5 — supports integrity of stored records by removing the agent's unilateral ability to destroy messages, labels, and filters.
- HIPAA §164.312(c) — supports the integrity standard (protection of ePHI in patient email from improper destruction); §164.530(c) — supports privacy safeguards over records held in mailboxes.
- GDPR Art. 5(1)(d) — supports accuracy by preventing mass-deletion/corruption of personal-data records through the agent channel (one
batch_delete_emailscall can permanently destroy 50 messages).
Tool name matching
The DTwo gateway prefixes tool names with the configured MCP server name (e.g. gmail-delete_email for a server named gmail), and that prefix is deployment-specific, so the policy matches case-insensitively by suffix:
*delete_email— GongRzhe single-message permanent delete*batch_delete_emails— GongRzhe batch permanent delete (up to 50 IDs per call)*delete_label— GongRzhe label deletion*delete_filter— GongRzhe filter deletion*manage_gmail_label— taylorwilsdon, denied only whenargs.action == "delete"*manage_gmail_filter— taylorwilsdon, denied only whenargs.action == "delete"
Verify the exact names your gateway sends with the dump-input debug technique before relying on this in production. If the community server you use exposes a different delete-tool name, add its suffix to the matching rules in policy.md.
Argument shape
Only the two manage_gmail_* tools have their arguments inspected, and every read goes through object.get:
args.actionis read asobject.get(object.get(input.payload, "args", {}), "action", ""), then trimmed and lowercased before comparison, soDelete/DELETEand whitespace-padded variants (delete,delete\n) cannot slip past a server that strips/normalizes the action before dispatch.- A
manage_gmail_*call with noargsobject or noactionkey is not treated as a delete and passes through — the server itself will reject the malformed call; this policy only freezes confirmed deletions. - A non-string
actionvalue (array, object, number) does not compare equal to"delete"and passes through; the upstream server's schema validation rejects such calls anyway.
The plain suffix-matched tools (delete_email, batch_delete_emails, delete_label, delete_filter) are denied on tool name alone — no argument can make a permanent delete safe.
Examples
Allowed — archiving via label modification (the recommended alternative)
{
"input": {
"action": "tool_pre_invoke",
"resource": { "name": "gmail-modify_email", "type": "tool" },
"payload": {
"name": "gmail-modify_email",
"args": { "messageId": "18f3a2", "removeLabelIds": ["INBOX"] }
}
}
}
allow = true, no reason.
Denied — batch permanent deletion
{
"input": {
"action": "tool_pre_invoke",
"resource": { "name": "gmail-batch_delete_emails", "type": "tool" },
"payload": {
"name": "gmail-batch_delete_emails",
"args": { "messageIds": ["18f3a2", "18f3a3"], "batchSize": 50 }
}
}
}
allow = false, reason = "Permanent email deletion is blocked (...)".
Allowed — manage_gmail_label with a non-delete action
{
"input": {
"action": "tool_pre_invoke",
"resource": { "name": "gmail-manage_gmail_label", "type": "tool" },
"payload": {
"name": "gmail-manage_gmail_label",
"args": { "action": "create", "name": "audit-hold" }
}
}
}
allow = true — create/update/list/get actions are left for other policies to govern.
Composition
This policy is single-purpose: it freezes confirmed deletion operations and nothing else. Pair it with:
- a filter-creation guard on
create_filter/create_filter_from_template/manage_gmail_filterwithcreate/updateactions — filter creation is the auto-forward persistence/exfiltration primitive, and it is intentionally out of scope here, - an external-send guard on
send_email/send_gmail_message— externally visible and unrecallable, a different risk family, - an egress redaction policy on the read surface (
get_thread/read_email/get_gmail_*_content*) for regulated data leaving the mailbox.
Known limitations
- Inert on the official Google/Claude connector surface — by design. The official remote Gmail MCP server exposes no delete tools, so this policy never fires there. That is the documented, expected state: the policy exists to hold the line when a community server (with its delete primitives) is introduced.
- No identity exemption — by design. There is no admin group that may delete through the agent channel; records must survive agent error and prompt injection for every caller. Legitimate deletions belong outside the agent channel (Gmail UI or Google Admin console) via your InfoSec team. If your organization truly requires an agent-channel break-glass, add a
groups-gatedallowbranch per the identity-placeholder conventions — but understand it reopens the injection surface this policy closes. - Trash is a delayed-deletion path, not permanence. Moving a message to trash (via label modification) starts Gmail's ~30-day auto-purge clock. The policy guarantees a recovery window, not indefinite retention — pair with Google Vault or retention holds for true immutability.
- Destruction by overwrite/renaming is not covered.
update_labelandmanage_gmail_labelwithaction: "update"can rename labels and disturb mailbox organization without deleting anything; that belongs to a label-governance companion policy, not this one. manage_gmail_*action vocabulary is source-verified as of July 2026. Theactionargument key and itsdeletevalue are verified from taylorwilsdon'sgmail_tools.py. If a future version adds another destructive action value (e.g.purge), it would pass this policy until the matcher is extended.- Malformed
manage_gmail_*calls fail open here. A call with a missing or non-stringactionpasses the policy and is left for the server's own schema validation to reject. This is intentional (object.getdefaults), so a benign call without anactionargument is never misclassified as a delete. - Action normalization covers case + surrounding whitespace only. The
actionvalue istrim_space-d and lowercased, which defeats casing (DELETE) and padding (delete,delete\n). It does not normalize Unicode homoglyphs, zero-width characters, or interior whitespace (e.g.de lete, or a fullwidthdelete). Such a value would pass this policy — but it would also fail the upstream server's own exact-string dispatch, so no deletion occurs. If a future server variant does fuzzy/normalized action matching, extendrequested_actionaccordingly. - Suffix matching assumes the three verified snake_case vocabularies. All destructive tool names in the landscape note (GongRzhe
delete_email/batch_delete_emails/delete_label/delete_filter, taylorwilsdonmanage_gmail_*) are snake_case, and theendswithsuffixes match them. A community server that renamed a hard-delete tool to camelCase (batchDeleteEmails) or another shape would not match — add its suffix per the Tool name matching section before relying on the policy against that server.
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.ingress.freeze_destructive_ops
# Deny-by-default: only the explicit allow rule below permits the request.
default allow := false
# --- Destructive tool matching ---
# Only community Gmail MCP servers expose deletion; the official Google/Claude
# connector has no delete tools at all, so this policy is inert there by
# design. The gateway prepends the configured MCP server name (e.g. `gmail-`),
# so match by suffix, case-insensitively. Verify the exact names your gateway
# sends with the dump-input debug technique.
# GongRzhe delete_email — permanent single-message deletion, bypasses trash.
is_message_hard_delete if {
endswith(lower(input.resource.name), "delete_email")
}
# GongRzhe batch_delete_emails — permanent deletion of up to 50 IDs per call.
is_message_hard_delete if {
endswith(lower(input.resource.name), "batch_delete_emails")
}
# GongRzhe delete_label — destroys the label across the whole mailbox.
is_label_delete if {
endswith(lower(input.resource.name), "delete_label")
}
# taylorwilsdon manage_gmail_label — destructive only when action == "delete";
# list/get/create/update actions pass through for other policies to govern.
is_label_delete if {
endswith(lower(input.resource.name), "manage_gmail_label")
requested_action == "delete"
}
# GongRzhe delete_filter — silently removes mail-routing rules.
is_filter_delete if {
endswith(lower(input.resource.name), "delete_filter")
}
# taylorwilsdon manage_gmail_filter — destructive only when action == "delete".
is_filter_delete if {
endswith(lower(input.resource.name), "manage_gmail_filter")
requested_action == "delete"
}
# Safe read of the manage_* `action` argument: a missing args object or a
# missing action key yields "" (never treated as a delete). The value is
# trimmed and lowercased before comparison, so `Delete`, `DELETE`, or a
# whitespace-padded ` delete `/`delete\n` cannot slip past a server that
# strips/normalizes the action before dispatch. A non-string action leaves
# requested_action undefined, which also means "not a delete" — the upstream
# server's schema validation rejects such calls.
requested_action := lower(trim_space(action_value)) if {
action_value := object.get(object.get(input.payload, "args", {}), "action", "")
is_string(action_value)
}
is_destructive_call if is_message_hard_delete
is_destructive_call if is_label_delete
is_destructive_call if is_filter_delete
# Allow every tool call that is not a confirmed deletion.
allow if {
not is_destructive_call
}
reasons contains "Permanent email deletion is blocked: delete_email and batch_delete_emails bypass the trash and cannot be undone, and records must survive agent error and prompt injection. Archive by modifying labels (modify_email / batch_modify_gmail_message_labels) or move the message to trash instead. Contact your InfoSec team if a record legitimately must be destroyed." if {
is_message_hard_delete
}
reasons contains "Deleting Gmail labels is blocked: removing a label silently strips it from every message that carries it and breaks retention and archiving workflows. Keep the label; create/update/list label actions remain available. Contact your InfoSec team if a label legitimately must be removed." if {
is_label_delete
}
reasons contains "Deleting Gmail filters is blocked: filters control mail routing and their removal is silent and hard to audit. Leave the filter in place; list/get/create/update filter actions remain available. Contact your InfoSec team if a filter legitimately must be removed." if {
is_filter_delete
}
reason := joined if {
count(reasons) > 0
reason_list := sort([r | some r in reasons])
joined := concat("; ", reason_list)
} 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