--- name: HubSpot Protect Lifecycle Stage tags: - hubspot - contacts - lifecycle - access-control - governance - ingress publishedAt: 2026-06-16 description: | # hubspot / protect-lifecycle-stage **Direction:** ingress (`tool_pre_invoke`) **Default:** deny on match, allow otherwise **Package:** `hubspot.ingress.protect_lifecycle_stage` ## What it does Blocks HubSpot CRM-object calls that set or change a contact's lifecycle stage. Any `hubspot-manage-crm-objects` call where a `contacts` object's `properties` include the `lifecyclestage` key — in either `createRequest` or `updateRequest` — is denied. Non-contact objects, contact edits that don't touch `lifecyclestage`, and all other tools pass through unchanged. ## Why ingress Lifecycle stage drives marketing automation, lead routing, and funnel reporting. The violation is fully determined by the request payload, so denying at ingress prevents the stage change from ever reaching HubSpot. ## How it matches All of the following must hold for a call to be denied: - **Tool match.** The (lowercased) tool name ends with `-manage-crm-objects` (suffix matching keeps the policy portable regardless of the MCP server name prefix the gateway adds). - **Contact object.** An object in `createRequest.objects` or `updateRequest.objects` has `objectType` `contacts` (case-insensitive). - **Lifecycle field present.** That object's `properties` include the `lifecyclestage` key (presence alone triggers the deny — the value is not inspected). ## 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. ## Examples ### Allowed (contact update with no lifecycle change) ```jsonc { "input": { "action": "tool_pre_invoke", "resource": { "name": "hubspot-manage-crm-objects", "type": "tool" }, "payload": { "name": "hubspot-manage-crm-objects", "args": { "updateRequest": { "objects": [ { "objectType": "contacts", "id": "12345", "properties": { "email": "lead@example.com" } } ] } } } } } ``` `allow = true`, no reason. ### Denied (lifecycle stage change) ```jsonc { "input": { "action": "tool_pre_invoke", "resource": { "name": "hubspot-manage-crm-objects", "type": "tool" }, "payload": { "name": "hubspot-manage-crm-objects", "args": { "updateRequest": { "objects": [ { "objectType": "contacts", "id": "12345", "properties": { "lifecyclestage": "customer" } } ] } } } } } ``` `allow = false`, `reason = "Changing the lifecycle stage of a contact is not permitted through this gateway. Contact your admin to update lifecycle stages."`. ## 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. - **Contacts only.** Only `contacts` objects are inspected; `lifecyclestage` on other object types is not blocked. Extend `is_lifecycle_change` if your environment uses lifecycle stage on companies or custom objects. - **No identity-based exemptions.** All callers are treated the same. To allow a break-glass role to change lifecycle stages, 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.protect_lifecycle_stage default allow := false allow if { not is_lifecycle_change } is_lifecycle_change 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", "")) == "contacts" "lifecyclestage" in object.keys(object.get(obj, "properties", {})) } is_lifecycle_change 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", "")) == "contacts" "lifecyclestage" in object.keys(object.get(obj, "properties", {})) } reason := "Changing the lifecycle stage of a contact is not permitted through this gateway. Contact your admin to update lifecycle stages." if not allow ```