DTwo Policy Store

HubSpot Protect Lifecycle Stage

Blocks HubSpot CRM-object calls that set or change a contact's lifecycle stage.

Direction
ingress
Rego package
hubspot.ingress.protect_lifecycle_stage
App
hubspot
Bundle
crm
Published
Minimum gateway
1.0.0b24
Schema version
1.0.0
Checksum
sha256:16f9e6e25e396ba938081c793f12d3bfbc0eb030440fa04e871885e6fcc3697f

hubspotcontactslifecycleaccess-controlgovernanceingress

What this policy does

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)

{
  "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)

{
  "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.

Policy source (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

Canonical source: policy.md on GitHub · raw · raw on this site (.md)

Used in these guides