--- name: HubSpot Block Deal Closure tags: - hubspot - deals - access-control - governance - ingress publishedAt: 2026-06-16 description: | # hubspot / block-deal-closure **Direction:** ingress (`tool_pre_invoke`) **Default:** deny on match, allow otherwise **Package:** `hubspot.ingress.no_close_deal` ## What it does Blocks HubSpot CRM-object calls that move a deal into a closed stage (`closedwon` or `closedlost`). Both create and update requests are inspected. Every other deal change — and every other HubSpot tool — passes through unchanged. ## Why ingress Closing a deal is a write with permanent side effects on the CRM (revenue reporting, workflow automation, downstream syncs). The violation is fully determined by the request payload, so denying at ingress prevents the stage change from ever reaching HubSpot. ## How it matches Two conditions must both hold for a call to be denied: - **Tool match.** The (lowercased) tool name ends with `-manage-crm-objects` — the HubSpot MCP tool that creates and updates CRM records. Suffix matching keeps the policy portable regardless of the MCP server name the gateway adds as a prefix. - **Closing a deal.** Within the call's `createRequest.objects` or `updateRequest.objects`, an object whose `objectType` is `deals` sets `properties.dealstage` to `closedwon` or `closedlost` (case-insensitive). ## Tool naming on the gateway DTwo prefixes tool names with the MCP server name configured on the gateway, so a HubSpot server registered as `hubspot` surfaces `hubspot-manage-crm-objects` while one registered as `hubspot-mcp` surfaces `hubspot-mcp-manage-crm-objects`. This policy matches on the **suffix** (`-manage-crm-objects`) so it stays portable across naming conventions. Confirm the exact tool name with the dump-input debug technique before deploying. ## Configuring closed stages The blocked stages live in `closed_stages` at the top of the Rego (`closedwon`, `closedlost`). If your pipeline uses custom closed-stage internal names, add them there. ## Examples ### Allowed (non-closing update) ```jsonc { "input": { "action": "tool_pre_invoke", "resource": { "name": "hubspot-manage-crm-objects", "type": "tool" }, "payload": { "name": "hubspot-manage-crm-objects", "args": { "updateRequest": { "objects": [ { "objectType": "deals", "id": "12345", "properties": { "dealstage": "qualifiedtobuy" } } ] } } } } } ``` `allow = true`, no reason. ### Denied (closing a deal) ```jsonc { "input": { "action": "tool_pre_invoke", "resource": { "name": "hubspot-manage-crm-objects", "type": "tool" }, "payload": { "name": "hubspot-manage-crm-objects", "args": { "updateRequest": { "objects": [ { "objectType": "deals", "id": "12345", "properties": { "dealstage": "closedwon" } } ] } } } } } ``` `allow = false`, `reason = "Closing deals is not allowed. Contact your InfoSec team to get access."`. ## Known limitations - **Suffix tool-name match.** The policy matches any tool ending in `-manage-crm-objects`. If a non-HubSpot MCP server happened to expose a tool with that same suffix, it would also be inspected — narrow the match if that is a concern in your environment. - **Custom stage names.** Only `closedwon` / `closedlost` are blocked by default; custom closed-stage internal names must be added to `closed_stages`. - **No identity-based exemptions.** All callers are treated the same. To allow a break-glass role to close deals, add an `allow if` branch gated on `input.subject.claims`. direction: ingress apps: - hubspot industries: [] bundles: - crm schemaVersion: 1.0.0 minimumGatewayVersion: 1.0.0b24 --- ```rego package hubspot.ingress.no_close_deal default allow := false closed_stages := {"closedwon", "closedlost"} allow if { not is_closing_deal } is_closing_deal if { endswith(lower(input.resource.name), "-manage-crm-objects") some obj in object.get(object.get(input.payload.args, "updateRequest", {}), "objects", []) lower(object.get(obj, "objectType", "")) == "deals" stage := lower(object.get(object.get(obj, "properties", {}), "dealstage", "")) closed_stages[stage] } is_closing_deal if { endswith(lower(input.resource.name), "-manage-crm-objects") some obj in object.get(object.get(input.payload.args, "createRequest", {}), "objects", []) lower(object.get(obj, "objectType", "")) == "deals" stage := lower(object.get(object.get(obj, "properties", {}), "dealstage", "")) closed_stages[stage] } reasons contains "Closing deals is not allowed. Contact your InfoSec team to get access." if { is_closing_deal } reason := joined if { count(reasons) > 0 reason_list := sort([r | some r in reasons]) joined := concat("; ", reason_list) } ```