HubSpot Cap Bulk Export
Clamps the page size of HubSpot bulk-read tool calls before they reach the HubSpot MCP server, so a single agent request to a covered bulk-read tool can…
- Direction
- ingress
- Rego package
hubspot.ingress.cap_bulk_export- App
- hubspot
- Bundles
- crmsoc2hipaapci-dssgdpr-ccpa
- Published
- Minimum gateway
- 1.0.0b24
- Schema version
- 1.0.0
- Checksum
sha256:dde664fd7b43b6fe4187d2b623b2b3ada8376c469f1e45455ca5a435f3325bc2
hubspotcap-bulk-exportpiidata-minimisationingresssoc2hipaapci-dssgdpr-ccpa
What this policy does
Direction: ingress (tool_pre_invoke)
Default: allow (transform-only — never denies)
Package: hubspot.ingress.cap_bulk_export
What it does
Clamps the page size of HubSpot bulk-read tool calls before they reach the HubSpot MCP server, so a single agent request to a covered bulk-read tool can never pull more than 50 CRM records. (Coverage is by tool-name suffix — see Tool name matching and Known limitations for what is and isn't clamped.) Contact, company, deal, and ticket records carry names, emails, phones, and addresses — search/list pagination is the mass-PII export channel in every HubSpot MCP implementation.
Two clamps are applied:
- Page-size clamp — on search/list tools, any numeric
limitoutside the range 1–50 is rewritten to 50: values above the cap, and non-positive values (limit: 0/ negatives, which some servers treat as "unbounded").limit: 50is also injected when the field is absent or non-numeric (the remote server otherwise defaults up to 200 records per page). Calls that already request between 1 and 50 pass through untouched. - Batch-read truncation — on ids-style batch reads (
get_crm_objectsaccepts up to 100 IDs;hubspot-batch-read-objectstakes aninputsarray), anyids/objectIds/inputsarray longer than 50 entries is truncated to its first 50.
The policy never denies, so read workflows keep functioning — just at bounded
page sizes. Every possibly-missing field is read with object.get, so
malformed or minimal calls pass through rather than erroring.
Compliance alignment
- SOC 2 CC6.7 — supports the restriction on transmission/movement/removal of information by bounding how much CRM data any single agent call can move out of HubSpot.
- HIPAA §164.502(b) / §164.514(d) — supports the minimum-necessary standard: agents retrieve pages sized to the task, not the maximum the API permits.
- PCI DSS 7.2.6 — supports restricting programmatic/agent access to repositories of stored cardholder data: bounding the record count a single query can retrieve keeps an over-broad agent read from mass-extracting card-adjacent CRM data.
- GDPR Art. 5(1)(c) — data minimisation on the agent channel: the query is minimised before it reaches HubSpot; Art. 5(1)(d) — smaller read/write surfaces reduce mass-corruption blast radius on downstream batch workflows.
- CCPA 11 CCR §7002 — supports proportionality: collection and use of personal information stays proportionate to the disclosed purpose rather than defaulting to bulk retrieval.
Why ingress
The over-broad request itself is the problem: once HubSpot has returned 200
records, an egress policy can only mask fields — the volume has already been
fetched, logged, and counted against rate limits. Rewriting limit at ingress
enforces minimisation before the query executes, which is the only place the
record count can be controlled.
Tool name matching
The DTwo gateway prefixes tool names with the configured MCP server name
(e.g. hubspot-mcp-hubspot-list-objects), so matching is by case-insensitive
suffix to stay portable across deployments. Covered names, per server family:
- Remote server / Claude connector (verified):
search_crm_objects,get_crm_objects @hubspot/mcp-serverlocal beta (verified):hubspot-search-objects,hubspot-list-objects,hubspot-batch-read-objects- shinzo-labs community server (verified names):
crm_list_objects,crm_search_objects,crm_search_contacts,crm_search_companies
Verify the exact names your gateway sends with the dump-input debug technique before relying on this in production, and extend the suffix lists if your server exposes additional list/search tools.
Argument shape
- Search/list tools take a top-level numeric
limit(verified forsearch_crm_objects/hubspot-search-objects, alongsideobjectType,query,filterGroups,properties,after). All other arguments are preserved unchanged by the rewrite. hubspot-batch-read-objectstakesobjectType+ aninputsarray (verified pattern for the local-beta batch tools).get_crm_objectsaccepts up to 100 IDs, but HubSpot does not fully publish the parameter name — the policy truncates both common shapes (idsandobjectIds) when present as arrays. Confirm the live shape fromtools/listbefore relying on this clamp.
Examples
Passed through unchanged
{
"input": {
"action": "tool_pre_invoke",
"resource": { "name": "hubspot-remote-search_crm_objects", "type": "tool" },
"payload": {
"name": "hubspot-remote-search_crm_objects",
"args": { "objectType": "contacts", "query": "smith", "limit": 25 }
}
}
}
allow = true, no transform — the requested page size is already within the cap.
Transformed
{
"input": {
"action": "tool_pre_invoke",
"resource": { "name": "hubspot-remote-search_crm_objects", "type": "tool" },
"payload": {
"name": "hubspot-remote-search_crm_objects",
"args": { "objectType": "contacts", "query": "smith", "limit": 200 }
}
}
}
allow = true, transform rewrites the args to
{ "objectType": "contacts", "query": "smith", "limit": 50 }. A call with no
limit at all gets limit: 50 injected the same way.
Composition
This policy bounds record volume; it does not mask record content. Pair it with:
apps/hubspot/redact-pii— egress masking of contact identifiers in whatever records are returned. Together they enforce minimisation on both the query and the response.apps/hubspot/read-only— if agents should not write to the CRM at all.
Known limitations
- Per-request caps do not stop patient pagination. An agent that walks the
aftercursor page by page can still enumerate the full dataset — it just takes 4× as many calls atlimit: 50. Detecting cursor-driven crawls requires cross-request state the policy engine does not have; use gateway audit logs / alerting to spot high-frequency paging. - shinzo per-engagement read tools are not clamped. The shinzo server
exposes
calls_/emails_/meetings_/notes_/tasks_list/search/batch_readtools — bulk-read channels for engagement bodies, which frequently carry customer PII/PHI verbatim — but their argument shapes (thelimitkey and the batch array key) are not verified, so this policy does not clamp them. An agent that callsemails_search,calls_list, etc. can still request large pages. Add their suffixes tolimit_tool_suffixes/batch_array_keysonce you have confirmed the live shape fromtools/list. - The baryhuang community server is not covered. Its bulk reads
(
hubspot_get_active_contacts,hubspot_get_active_companies,hubspot_search_data) use a distinct name family and are not clamped;hubspot_search_dataadditionally vectorises CRM data into a local store outside HubSpot. The 50-record bound holds for the remote and local-beta families plus the four shinzo CRM search/list names listed above — not for every conceivable HubSpot MCP server. Extend the suffix lists per server. get_crm_objectsID parameter name is partially verified. The 100-ID capacity is documented, the exact key is not; the policy coversidsandobjectIds. An implementation using a different key passes through unclamped until you add it.- Only the listed argument keys are clamped. A server that spells the page
size differently (
pageSize,maxResults,count) is not covered — extend the policy if yourtools/listshows other shapes. - No identity-based exemptions. All callers are clamped equally. If a
data-ops group legitimately needs full-page reads, add an
input.subject.claims-gated bypass as a separate rule.
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.cap_bulk_export
# Transform-only policy — never denies, only clamps bulk-read page sizes.
default allow := true
# Maximum records a single agent call may request.
max_records := 50
# --- Tool matching -----------------------------------------------------------
# The gateway prefixes tool names with the configured MCP server name, so we
# match case-insensitively by suffix to stay portable. Verify the exact names
# your gateway sends with the dump-input debug technique before production use.
# Search/list tools that take a numeric `limit` argument.
# Remote server family: search_crm_objects
# Local beta family: hubspot-search-objects, hubspot-list-objects
# shinzo family: crm_list_objects, crm_search_objects,
# crm_search_contacts, crm_search_companies
limit_tool_suffixes := [
"search_crm_objects",
"hubspot-search-objects",
"hubspot-list-objects",
"crm_list_objects",
"crm_search_objects",
"crm_search_contacts",
"crm_search_companies",
]
is_limit_tool if {
some suffix in limit_tool_suffixes
endswith(lower(input.resource.name), suffix)
}
# Ids-style batch reads: get_crm_objects (remote, <=100 IDs) and
# hubspot-batch-read-objects (local beta, `inputs` array).
is_batch_tool if {
endswith(lower(input.resource.name), "get_crm_objects")
}
is_batch_tool if {
endswith(lower(input.resource.name), "hubspot-batch-read-objects")
}
# --- Argument access (object.get everywhere — fields may be missing) ---------
args := object.get(input.payload, "args", {})
limit_value := object.get(args, "limit", null)
# Clamp when `limit` is absent (the remote server otherwise defaults up to
# 200 records per page).
needs_limit_clamp if {
limit_value == null
}
# Clamp when a numeric `limit` exceeds the cap.
needs_limit_clamp if {
is_number(limit_value)
limit_value > max_records
}
# Clamp when a numeric `limit` is below 1 (0 or negative). Some servers treat a
# non-positive `limit` as "unbounded" or silently fall back to their large
# default page, so `limit: 0` / `limit: -1` would otherwise be a fail-open
# bypass of the cap. Any numeric limit outside [1, max_records] is normalised.
needs_limit_clamp if {
is_number(limit_value)
limit_value < 1
}
# Clamp when `limit` is present but not a number (fail safe: replace an
# unparseable value with the cap rather than letting the server default win).
needs_limit_clamp if {
limit_value != null
not is_number(limit_value)
}
# --- Batch truncation --------------------------------------------------------
# `inputs` is the verified key for hubspot-batch-read-objects; `ids` and
# `objectIds` cover the common shapes for get_crm_objects (exact key not
# fully published by HubSpot — see Known limitations).
batch_array_keys := ["ids", "objectIds", "inputs"]
oversized_batch_keys contains key if {
some key in batch_array_keys
value := object.get(args, key, [])
is_array(value)
count(value) > max_records
}
truncated_batch_args := {key: truncated |
some key in oversized_batch_keys
truncated := array.slice(object.get(args, key, []), 0, max_records)
}
# --- Transforms ---------------------------------------------------------------
# Only one of these can fire per call: the limit-tool and batch-tool suffix
# sets are disjoint, so the complete `transform` rule never conflicts.
# Rewrite (or inject) `limit` on search/list tools.
transform := {"transformed_payload": object.union(args, {"limit": max_records})} if {
input.action == "tool_pre_invoke"
is_limit_tool
needs_limit_clamp
}
# Truncate oversized ID/inputs arrays on batch-read tools.
transform := {"transformed_payload": object.union(args, truncated_batch_args)} if {
input.action == "tool_pre_invoke"
is_batch_tool
count(oversized_batch_keys) > 0
} 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