HubSpot Freeze Destructive Ops
Blocks every archive/deletion-class HubSpot tool call, plus the consent-destroying contact unsubscribe, before it reaches the MCP server.
- Direction
- ingress
- Rego package
hubspot.ingress.freeze_destructive_ops- App
- hubspot
- Bundles
- crmsoc2
- Published
- Minimum gateway
- 1.0.0b24
- Schema version
- 1.0.0
- Checksum
sha256:61bf268f5c76551c996d77c84cc5ca8c398f671d279c1e9bd0e08d81e2885c56
hubspotfreeze-destructive-opsarchiveconsentingresssoc2
What this policy does
Direction: ingress (tool_pre_invoke)
Default: deny on match, allow otherwise
Package: hubspot.ingress.freeze_destructive_ops
What it does
Blocks every archive/deletion-class HubSpot tool call, plus the consent-destroying contact unsubscribe, before it reaches the MCP server. Two classes are denied:
- Deletion/archive-class tools — any tool whose name contains a
destructive verb token (
archive,delete,void, orpurge) delimited by_or-. On the@shinzolabs/hubspot-mcpcommunity server the verified destructive surface all usesarchive:crm_archive_object,crm_batch_archive_objects,calls_archive,emails_archive,meetings_archive,notes_archive,tasks_archive, their*_batch_archivevariants,crm_archive_association, andproducts_archive. Thedelete/void/purgeverbs are forward-guard coverage (family PF-06) for a server swap or an upstream release that names deletion differently — no currently verified HubSpot tool uses them. - Consent destruction —
communications_unsubscribe_contact, which irreversibly flips a contact's subscription/consent state.
There is no identity exemption: records and consent state survive agent error or prompt injection regardless of who the caller is. All other tools pass through unchanged.
Compliance alignment
- SOC 2 PI1.5 — supports the integrity of stored records: agent-initiated archival of CRM objects, engagements, associations, and products is denied on the MCP path, so records survive agent error or injection.
- HIPAA §164.312(c) — supports the integrity (anti-alteration/destruction) safeguard where CRM records reference patient contacts; §164.530(c) — supports privacy safeguards by keeping consent state under human control.
- GDPR Art. 5(1)(d) — supports accuracy by preventing agent mass-destruction of records and of subscription/consent state that is hard to reconstruct.
Why ingress and not egress
Archival and unsubscribe are writes with permanent side effects — once the call reaches HubSpot the record is gone from active use and the consent flag has flipped. Egress can only mask the response, not undo the action. Ingress denial is the only placement that actually prevents the destruction.
Tool name matching
The DTwo gateway prefixes tool names with the configured MCP server name
(e.g. hubspot-mcp-crm_archive_object), and that prefix is not
standardized, so the policy matches name shapes rather than exact strings:
- Destructive-verb token: regex
(?:^|[_-])(?:archive|delete|void|purge)(?:[_-]|$)on the lowercased tool name — a destructive verb bounded by_/-or the ends of the name. This catches suffix forms (calls_archive,tasks_batch_archive), mid-name forms (crm_archive_object,crm_batch_archive_objects,crm_archive_association), and kebab-case variants (products-archive), without matching unrelated words such asarchivedordeleted. HubSpot's verified destructive tools all usearchive; thedelete/void/purgealternatives fire only if a future or swapped server names deletion that way. - Unsubscribe suffix: the lowercased name ends with
unsubscribe_contactorunsubscribe-contact. The suffix is long enough thatcommunications_subscribe_contact(the legitimate opt-in tool) does not match.
Verify the exact names your gateway sends with the dump-input debug technique before relying on this in production.
Argument shape
None. The decision is made purely on the tool name — arguments are never
inspected, so the policy fails closed on the name alone even when args is
empty or missing.
Examples
Allowed
{
"input": {
"action": "tool_pre_invoke",
"resource": { "name": "hubspot-mcp-crm_list_objects", "type": "tool" },
"payload": {
"name": "hubspot-mcp-crm_list_objects",
"args": { "objectType": "contacts", "limit": 10 }
}
}
}
allow = true, no reason.
Denied
{
"input": {
"action": "tool_pre_invoke",
"resource": { "name": "hubspot-mcp-crm_archive_object", "type": "tool" },
"payload": {
"name": "hubspot-mcp-crm_archive_object",
"args": { "objectType": "contacts", "objectId": "12345" }
}
}
}
allow = false, reason = "Agent-initiated deletion or archival of HubSpot records is disabled on the MCP path. Delete or archive records in the HubSpot UI, where the action is audited and can be undone. Contact your admin if this blocks a legitimate workflow."
Composition
This policy is single-purpose — it freezes destruction. Useful companions from the same app directory:
hubspot/read-only— the stricter posture: allow only read tools.- A consent-protection policy gating
communications_update_subscription_status/communications_update_preferences, which can also change consent state but are not outright unsubscribes and are therefore out of scope here. hubspot/protect-associations,hubspot/protect-lifecycle-stage— guard other integrity-critical write paths.
Known limitations
- Community-server-specific surface. Only the
@shinzolabs/hubspot-mcpcommunity server exposes these destructive tools. HubSpot's official remote server (mcp.hubspot.com) and local beta (@hubspot/mcp-server) currently ship no delete/archive tools, so on those servers this policy never fires. Keep it attached anyway — it is a forward guard against a server swap or an upstream release that adds destructive tools. - Token match is deliberately broad. Any tool whose name contains a
delimited destructive-verb token (
archive/delete/void/purge) is blocked, including hypothetical future read-side tools (e.g. alist-archive-items). That over-match is the conservative direction for a destruction freeze; narrow the regex if it bites in your environment. - Server-prefix over-match (false positive). Because the gateway prefixes
tool names with the configured MCP server name and the match is not
anchored to the tool segment, a server literally named with a leading
destructive verb — e.g.
archive-mcpordelete-svc— makes the leadingarchive-/delete-segment match, so every benign read on that server (e.g.archive-mcp-crm_list_objects) is denied. Name the HubSpot server something neutral (hubspot,hubspot-mcp) to avoid this; it does not weaken the freeze, it only over-blocks. - Match relies on exact upstream tool names. Detection is on the tool
name only, so an obfuscated name — trailing whitespace/newline
(
...unsubscribe_contact\n), unicode homoglyphs, or split/renamed tokens — does not match and is allowed through. This is not an exploitable bypass: the gateway forwards the name verbatim and the upstream MCP server routes a call only when the name matches a registered tool exactly, so a mangled name cannot invoke the real destructive tool — it simply errors upstream. Confirm the exact names your gateway sends (dump-input) before relying on the suffix/token shapes. - Consent updates are not fully covered. shinzo's
communications_update_subscription_statusandcommunications_update_preferencescan effectively unsubscribe a contact by setting the status value; this policy blocks only the dedicated unsubscribe tool. Pair with a consent-protection policy (see Composition). - No identity-based exemptions — by design. There is no break-glass
group; humans perform archival and unsubscribe in the HubSpot UI, where
audit and undo exist. If you must exempt a group, add an
allow ifbranch gated oninput.subject.claims. - MCP path only. Deletion via the HubSpot web UI or native API is outside the gateway's reach.
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 hubspot.ingress.freeze_destructive_ops
# Deny-by-default: only the explicit allow rule below permits a request.
default allow := false
# Lowercased tool name, fetched with object.get so a missing resource/name
# yields "" (and therefore a clean non-match) instead of a silent rule failure.
tool_name := lower(object.get(object.get(input, "resource", {}), "name", ""))
# Deletion/archive-class tools: a destructive verb token (archive | delete |
# void | purge) bounded by `_`/`-` or the ends of the name. On shinzo every
# verified destructive tool uses "archive" — calls_archive / emails_archive /
# meetings_archive / notes_archive / tasks_archive, their *_batch_archive
# variants, crm_archive_object, crm_batch_archive_objects,
# crm_archive_association, products_archive, and kebab-case renderings. The
# other verbs (delete/void/purge) are forward-guard coverage per policy family
# PF-06 for a server swap or upstream release that names deletion differently;
# no currently verified HubSpot tool uses them, so they never fire today. The
# delimiter bounding avoids matching words like "archived" or "deleted".
is_delete_archive_tool if {
regex.match(`(?:^|[_-])(?:archive|delete|void|purge)(?:[_-]|$)`, tool_name)
}
# Consent destruction: shinzo's communications_unsubscribe_contact. Suffix
# match keeps the policy portable across gateway server-name prefixes; the
# suffix is long enough that communications_subscribe_contact never matches.
is_unsubscribe_tool if {
endswith(tool_name, "unsubscribe_contact")
}
# Kebab-case portability variant of the same operation.
is_unsubscribe_tool if {
endswith(tool_name, "unsubscribe-contact")
}
is_destructive_tool if is_delete_archive_tool
is_destructive_tool if is_unsubscribe_tool
# Allow everything that is not a destructive-class tool. No identity
# exemption: records and consent state survive agent error or prompt
# injection regardless of who the caller is.
allow if {
not is_destructive_tool
}
reasons contains "Agent-initiated deletion or archival of HubSpot records is disabled on the MCP path. Delete or archive records in the HubSpot UI, where the action is audited and can be undone. Contact your admin if this blocks a legitimate workflow." if {
is_delete_archive_tool
}
reasons contains "Agent-initiated unsubscribes are disabled on the MCP path because they irreversibly destroy a contact's consent state. Manage subscription preferences in the HubSpot UI, where the change is audited. Contact your admin if this blocks a legitimate workflow." if {
is_unsubscribe_tool
}
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