Freeze Destructive Linear Operations
Denies destructive Linear tool calls — the delete , archive , and session-logout classes — unless the caller's IdP token carries the placeholder group…
- Direction
- ingress
- Rego package
linear.ingress.freeze_destructive_ops- App
- linear
- Bundle
- soc2
- Published
- Minimum gateway
- 1.0.0b24
- Schema version
- 1.0.0
- Checksum
sha256:b6dd4b06a24b10707fbcb23f8d5eac4ecb8774c5317b3327e1c32c86dd235317
linearfreeze-destructive-opsrecord-integrityingresssoc2
What this policy does
Direction: ingress (tool_pre_invoke)
Default: deny on match, allow otherwise
Package: linear.ingress.freeze_destructive_ops
What it does
Denies destructive Linear tool calls — the delete, archive, and session-logout classes — unless the caller's IdP token carries the placeholder group linear-admins. Every read tool and every non-destructive write passes through unchanged.
The destructive set is the community tacticlaunch/mcp-linear sidecar's destructive surface. Explicitly covered:
linear_deleteComment,linear_deleteInitiative— permanent deletes (Linear's delete is trash-then-permanent, distinct from recoverable archive).linear_archiveIssue,linear_archiveDocument,linear_archiveTeam— archives (archiving a team removes a whole team's workspace).linear_removeUserFromTeam— a membership-destruction verb that does not begin with delete/archive/logout, so it is matched explicitly.linear_logoutAllSessions,linear_logoutOtherSessions,linear_logoutSession— session revocation.logoutAllSessionslogs the user out of Linear everywhere — an account-level denial-of-service an agent should never be able to trigger.
Matching is by verb class, not by an enumerated list: any community tool whose verb segment is delete/archive/logout (anchored on the tacticlaunch linear_ tool prefix) is gated, plus removeUserFromTeam by name. So future community destructive tools (linear_deleteWebhook, linear_deleteCustomer, linear_archiveProject, linear_archiveMilestone, …) are frozen without a policy update.
The official Linear remote server (mcp.linear.app) exposes no delete/archive/logout verbs as of the last verified enumeration — its verbs are list_/get_/create_/update_. So on the official path this policy is a safe no-op that costs nothing; its job is to fence the community sidecar, where an agent acting on a hallucinated "cleanup" step or a prompt-injection payload could otherwise destroy records or lock a user out of Linear. The check runs at ingress, before the call reaches the MCP server, so a blocked delete/archive/logout never executes.
default allow := false here bites only on the matched destructive tools — the deny-by-default is neutralized for every other tool by an explicit pass-through allow branch (not is_destructive_tool). Reads (get_issue, linear_getIssues, linear_searchDocuments, …) and non-destructive writes (create_issue, linear_createIssue, linear_updateIssue, linear_createComment, …) are never touched by this policy.
Compliance alignment
- SOC 2 PI1.5 — supports integrity of stored records by removing the agent's unilateral ability to destroy them (comments, issues, initiatives, documents, and team workspaces cannot be permanently deleted or archived by a non-admin caller over the agent channel, whether by agent error or injected instruction); the admin-group exemption keeps the destructive verbs under least-privilege control (aligns with the RBAC posture of CC6.3). The session-logout freeze additionally supports availability of the record system by preventing an agent-triggered account lockout.
Tool name matching
The community tacticlaunch/mcp-linear server prefixes every tool with linear_ and uses camelCase verbs (linear_deleteComment, linear_archiveIssue, linear_logoutAllSessions). Behind a DTwo gateway the tool additionally receives the configured server-name prefix (e.g. linear-linear_deleteComment). The verb sits between the linear_ prefix and the object noun, so matching is done on the verb segment, case-insensitively, after normalizing underscores to hyphens:
linear-delete/linear-archive/linear-logout(substring match). These anchor on the tacticlaunch tool's ownlinear_prefix (normalized tolinear-), which the tool keeps regardless of the gateway server-name prefix. Solinear_deleteComment,linear-linear_deleteComment(gateway-prefixed), andfoo-linear_deleteComment(any server name) all match, and the match does not depend on knowing the gateway's server name.removeuserfromteam(substring match).linear_removeUserFromTeamis a destructive membership verb outside the three verb classes, so it is caught by an explicitcontains(same operator as the verb-class markers, so it is robust to a trailing-decorated name).
Underscores are normalized to hyphens before matching, so a snake_case-named destructive variant (e.g. a hypothetical linear_delete_comment) is gated too. The name is read from both the PARC field (input.resource.name) and the legacy alias (input.payload.name) via object.get chains, and the two are matched independently — as long as at least one of the two fields still carries the real tool name, a request that garbles or omits the other field cannot skip the match. Each field is coerced to a lowercased, whitespace-trimmed string (a number, null, array, or object resolves to the empty string), so a non-string value in one field can never suppress a genuine destructive verb in the other. trim_space strips leading/trailing whitespace (spaces, tabs, newlines) so padding the verb with a trailing space or newline (linear_deleteComment\n) does not evade the match. Note the corollary: if the only populated name field is a non-string (or both are), it coerces to the empty string and the call is treated as non-destructive and passes through — harmless because such a malformed PARC cannot route to a real destructive tool at the MCP server (see Known limitations).
The official server exposes no delete/archive/logout verbs and its tool set drifts (catalogs count 23 → 31), and the community set is ~150 tools. Verify the exact names your gateway sends with the dump-input debug technique before relying on this in production.
Argument shape
None. The decision uses only the tool name (input.resource.name, with the legacy input.payload.name as fallback) and the caller's identity (input.subject.claims.groups); arguments are not inspected. A delete/archive/logout call takes only a record or session identifier, so there is nothing in the arguments to distinguish a safe destructive call from a dangerous one — the whole verb class is frozen.
Group membership is read via an object.get(input.subject, "claims", {}) chain and fails closed: a missing subject, missing claims, missing groups, or a non-array groups value all mean "not admin", so the destructive call is denied.
Examples
Allowed — read tool, any caller
{
"input": {
"action": "tool_pre_invoke",
"resource": { "name": "linear-linear_getIssues", "type": "tool" },
"payload": { "name": "linear-linear_getIssues", "args": { "teamId": "T1" } }
}
}
allow = true, no reason.
Allowed — archiveIssue by an admin
{
"input": {
"action": "tool_pre_invoke",
"resource": { "name": "linear-linear_archiveIssue", "type": "tool" },
"subject": { "sub": "admin@example.com", "claims": { "groups": ["linear-admins"] } },
"payload": { "name": "linear-linear_archiveIssue", "args": { "id": "ISS-1" } }
}
}
allow = true.
Denied — logoutAllSessions by a non-admin
{
"input": {
"action": "tool_pre_invoke",
"resource": { "name": "linear-linear_logoutAllSessions", "type": "tool" },
"subject": { "sub": "user@example.com", "claims": { "groups": ["engineering"] } },
"payload": { "name": "linear-linear_logoutAllSessions", "args": {} }
}
}
allow = false, reason = "This destructive Linear operation is blocked (...)".
Composition
This policy is single-purpose: it freezes the delete/archive/logout classes plus removeUserFromTeam, and nothing else. Pair it with:
- a webhook lockdown (ingress deny of
*createWebhook/*deleteWebhook/*updateWebhook) — a standing webhook is out-of-band exfiltration and is intentionally out of scope here, - a membership + impersonation deny on
*addUserToTeam/*updateTeamMembership(grants) — this policy only freezes the removal verb, - a destructive-edit guard on
update_issue/linear_updateIssueif silent overwrite of a security ticket's description is in scope (see Known limitations), - an egress redaction / roadmap-egress gate on the read tools that leak initiatives, customer data, and audit events.
Known limitations
- Group names are placeholders — replace
linear-adminswith your IdP's group name at import time. The gate readsinput.subject.claims.groups; confirm your IdP actually emits agroupsclaim (Auth0 and most IdPs require explicit configuration) before relying on the admin exemption. With nogroupsclaim the policy still fails closed: destructive calls are denied for everyone. - Destructive edits are out of scope. This policy freezes delete/archive/logout verbs; it does not stop a destructive update — e.g.
update_issue/linear_updateIssueoverwriting a description, silently closing a security ticket, orlinear_updateIssueCustomFieldclearing a value. Linear deliberately flattens its API so an update can blank fields. Guard those with a separate write/field-scope policy. - Community-prefix assumption. Matching anchors on the tacticlaunch
linear_tool prefix and specifically on thelinear→verb boundary being an underscore (normalized tolinear-<verb>). Three ways a name could slip the delete/archive/logout markers, all requiring a differently-named upstream tool: (a) a community server drops thelinear_prefix entirely and the gateway server name does not containlinear(a baredelete_comment→lin-delete-comment, nolinear-delete); (b) a server glues the prefix without a separator (linearDeleteComment→lineardeletecomment, no hyphen); (c) a destructive verb outsidedelete/archive/logoutand not containingremoveUserFromTeam(e.g. apurge*ordestroy*verb). In every case the name the gateway routes on differs from a real tacticlaunch tool, so a reallinear_delete*/archive*/logout*call is still frozen — these are gaps against hypothetical differently-named servers, not evasions of the enumerated destructive set. The official server has no delete/archive/logout verbs, so this is a fence for community traffic; for a hard guarantee against unknown tools, compose the PF-28default-deny-unknown-toolsallowlist alongside this policy. - Verb-class matching is broad by design. It catches the whole community delete/archive/logout class (including tools not enumerated above, such as
linear_deleteWebhookorlinear_archiveMilestone). For a record-integrity freeze, over-matching is the safe direction. A read tool is not caught because the anchor islinear-delete/linear-archive/linear-logout: a read likelinear_getDocumentContentHistoryorlinear_getInitiativescontains no such segment. - Substring matching is exact apart from surrounding whitespace. Matching is on the lowercased,
trim_spaced, underscore-normalized name, so leading/trailing spaces, tabs, and newlines are handled, and a trailing-decorated verb (extra chars afterdeleteComment/removeUserFromTeam) is still caught because every marker usescontains. It does not normalize other characters: a name carrying an invisible non-whitespace code point inside the verb (e.g. U+200B zero-width space) or a Unicode homoglyph of the verb would not match and would pass through. Likewise, a call whose only populated name field is a non-string (array/object/number/null) coerces to the empty string and passes through as non-destructive. These are not real evasions on a correctly configured gateway — the gateway routes on the exact server-registered tool name, so a padded/homoglyph/malformed name does not resolve to the real destructive tool at the MCP server — but if you cannot rely on that invariant, compose the PF-28default-deny-unknown-toolsallowlist alongside this policy.
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.freeze_destructive_ops
# Deny-by-default: only the explicit allow rules below permit the request.
# Note: the pass-through allow branch (`not is_destructive_tool`) neutralizes
# this default for every non-destructive tool, so the default-deny bites only
# on the matched destructive verbs.
default allow := false
# Placeholder IdP group allowed to run destructive Linear operations.
# Replace "linear-admins" with your IdP's group name at import time.
admin_group := "linear-admins"
# --- Destructive verb matching ---
# The community tacticlaunch/mcp-linear server prefixes tools with `linear_`
# and uses camelCase verbs (linear_deleteComment, linear_archiveIssue,
# linear_logoutAllSessions). Behind a DTwo gateway the tool also gets the
# configured server-name prefix (e.g. linear-linear_deleteComment). The verb
# sits BETWEEN the `linear_` prefix and the object noun, so we match on the
# verb segment (not a clean suffix), anchored on the tool's own `linear_`
# prefix so the gateway server name is irrelevant. The official Linear server
# has no delete/archive/logout verbs, so this is a no-op there.
# Verify the exact names on your gateway with the dump-input debug technique.
# Verb-class markers (underscores normalized to hyphens before matching):
# any tool whose verb segment after the community `linear_` prefix is
# delete/archive/logout is destructive.
destructive_markers := [
"linear-delete",
"linear-archive",
"linear-logout",
]
# Explicit destructive verbs outside the three classes. removeUserFromTeam is
# membership-destruction that does not start with delete/archive/logout, so it
# is matched as a substring (contains, not endswith) — consistent with the
# verb-class markers above and, since over-match is the safe direction for a
# record-integrity freeze, it also catches any trailing-decorated variant.
destructive_substrings := ["removeuserfromteam"]
# Tool name is read via object.get chains from BOTH the PARC field
# (input.resource.name) and the legacy alias (input.payload.name), so a
# request that somehow omits the resource block still cannot skip matching
# (red-team hardening: missing resource must not fail open).
# name_of coerces to a lowercased, whitespace-trimmed, underscore-normalized
# string. A missing OR non-string value (number, null, array, object) resolves
# to "" rather than leaving the rule undefined — an undefined name would make
# the match checks undefined and skip matching entirely (fail-open).
# trim_space strips leading/trailing whitespace so a padded name like
# "linear_deleteComment\n" cannot slip past. Underscores are normalized to
# hyphens so the markers match camelCase (linear_deleteComment) and any
# snake_case variant (linear_delete_comment) alike.
name_of(key) := replace(trim_space(lower(v)), "_", "-") if {
v := object.get(object.get(input, key, {}), "name", "")
is_string(v)
}
name_of(key) := "" if {
v := object.get(object.get(input, key, {}), "name", "")
not is_string(v)
}
resource_name := name_of("resource")
payload_name := name_of("payload")
# A single name is destructive if it contains a verb-class marker OR contains
# an explicit destructive verb.
name_is_destructive(n) if {
some marker in destructive_markers
contains(n, marker)
}
name_is_destructive(n) if {
some sub in destructive_substrings
contains(n, sub)
}
# Both names are checked independently. Keeping separate branches means a
# malformed (non-string) value in one field cannot suppress a real destructive
# verb in the other.
is_destructive_tool if {
name_is_destructive(resource_name)
}
is_destructive_tool if {
name_is_destructive(payload_name)
}
# --- Admin gate ---
# Reads the groups claim through object.get chains so a missing subject,
# missing claims, missing groups, or non-array groups value fails closed:
# the caller is simply not an admin and the destructive call is denied.
# The is_array guard is load-bearing: without it, a groups claim that is an
# OBJECT whose values happen to include "linear-admins" (e.g. {"0":"linear-admins"})
# would satisfy `some group in groups` and fail OPEN. Requiring an array means
# any non-array groups shape (string, object, number) fails closed.
caller_is_admin if {
claims := object.get(input.subject, "claims", {})
groups := object.get(claims, "groups", [])
is_array(groups)
some group in groups
group == admin_group
}
# Allow any tool outside the destructive set.
allow if {
not is_destructive_tool
}
# Allow destructive tools only for members of the admin group.
allow if {
is_destructive_tool
caller_is_admin
}
reasons contains "This destructive Linear operation is blocked. Delete and archive tools permanently remove records (comments, issues, initiatives, documents, teams) with no agent-side undo, and the session-logout tools can lock a user out of Linear entirely, so a hallucinated cleanup step or a prompt-injection payload could destroy work or trigger an account denial-of-service. Route this request through a member of your Linear admin group (placeholder: linear-admins) instead. If you believe this block is a false positive, ask your InfoSec team to add you to that group." if {
is_destructive_tool
not caller_is_admin
}
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)
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