Slack: Deny Sending Direct Messages
Blocks Slack message-write calls whose destination resolves to a direct conversation — a 1:1 DM, a message posted to a user ID (which Slack auto-opens as a…
- Direction
- ingress
- Rego package
slack.ingress.deny_direct_messages- App
- slack
- Bundles
- slacksoc2
- Published
- Minimum gateway
- 1.0.0b24
- Schema version
- 1.0.0
- Checksum
sha256:3bfcfdee14c1d8409123990b850b96ce507d93079e026b7c7d179b161c353910
slackaccess-controlgovernanceingresssoc2iso27001-nistfinserv-comms
What this policy does
Direction: ingress (tool_pre_invoke)
Default: allow, with a targeted deny on direct-message writes
Package: slack.ingress.deny_direct_messages
What it does
Blocks Slack message-write calls whose destination resolves to a direct conversation — a 1:1 DM, a message posted to a user ID (which Slack auto-opens as a DM), or a multi-party/group DM. Posts to regular channels, and every non-write Slack tool, pass through untouched. A blocked call returns a clear denial reason instead of delivering the DM.
Compliance alignment
- SOC 2 CC6.7 — supports the restriction on transmission/movement of information by keeping agent output out of 1:1 and group-DM destinations on the MCP path.
- ISO 27001 A.5.14 / NIST 800-53 AC-4 — information transfer / flow enforcement: agent messages stay in channels where they are visible and reviewable.
- GDPR Art. 5(1)(f) / Art. 32 — supports security of processing: the agent cannot open unsupervised person-to-person disclosure paths for personal data it has read.
- FINRA 3110(b)(4) / SEC 17a-4(b)(4) — supports supervision and preservation of business communications (channel discipline): agent-generated messages cannot land in DM surfaces that evade supervisory review flows.
Why ingress
Sending a message is a write with a permanent side effect — once it reaches Slack the DM exists in the recipient's history. The destination is fully visible in the request, so denying at ingress prevents the message from ever being delivered.
How it matches
A call is denied only when all three conditions hold:
- Slack-server scoping. The first hyphen-separated segment of the tool name
starts with
slack, so the policy works regardless of how the Slack MCP server is named on a given gateway (slack-...,slack-prod-...,slack-mcp-...). - Write-tool detection. The (lowercased) tool name ends with one of the
known message-write suffixes — the post/send, update/edit, scheduled,
ephemeral, and me-message families, with
-,_, and concatenated naming variants covered. - DM-recipient detection. The destination is identified by Slack's
ID-prefix conventions, not by name. On the channel argument (
channel,channel_id,channelId) a value beginning withD(1:1 DM channel),U/W(user ID — Slack auto-opens a DM when posting to a user ID), orG(multi-party/group DM) is treated as a DM. A dedicated recipient argument (user,user_id,userId) is matched only against user IDs (U/W).
Channel names and other non-matching ID shapes are intentionally not treated
as DMs — the policy fails open on uncertainty rather than over-blocking. Confirm
the exact tool and argument names your gateway emits with the dump-input debug
technique, and extend write_tool_suffixes or the recipient lookups if your
Slack MCP server differs.
Examples
Denied (posting to a 1:1 DM channel)
{
"input": {
"action": "tool_pre_invoke",
"resource": { "name": "slack-mcp-slack-post-message", "type": "tool" },
"payload": {
"name": "slack-mcp-slack-post-message",
"args": { "channel": "D0123456789", "text": "hi" }
}
}
}
allow = false,
reason = "Sending direct messages via Slack is not permitted via this gateway. ...".
(A post addressed to a user ID such as "channel": "U0123456789" is denied the
same way, since Slack would open a DM.)
Allowed (posting to a regular channel)
{
"input": {
"action": "tool_pre_invoke",
"resource": { "name": "slack-mcp-slack-post-message", "type": "tool" },
"payload": {
"name": "slack-mcp-slack-post-message",
"args": { "channel": "C0123456789", "text": "hi team" }
}
}
}
allow = true, no reason.
Composition
Single-purpose and default allow := true, so it composes cleanly with other
Slack ingress policies (e.g. block-secrets,
deny-channel-creation) on the same
pipeline.
Known limitations
- ID-based detection. Recipients are identified by Slack ID prefixes. A tool that accepts a DM destination as a plain name or under an unrecognized argument will not be matched — add the argument to the recipient lookups.
- Suffix list, not an exhaustive catalog. Only the message-write shapes
listed in
write_tool_suffixesare matched. Add the suffix for any additional write tool your Slack MCP server exposes. - No identity-based exemptions. All callers are treated the same. To allow a
specific break-glass user, gate a separate
allow ifbranch oninput.subject.claims.
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 slack.ingress.deny_direct_messages
# Default-allow: only deny writes whose destination resolves to a direct
# conversation (1:1 DM, user-ID-as-channel, MPIM, or user/userId arg).
default allow := true
# -----------------------------------------------------------------------------
# Slack-server detection: any tool whose first hyphen-separated segment starts
# with "slack" (slack-, slack3-, slack-prod-, etc.).
# -----------------------------------------------------------------------------
is_slack_tool if {
name := lower(input.resource.name)
server_name := split(name, "-")[0]
startswith(server_name, "slack")
}
# -----------------------------------------------------------------------------
# Write-tool detection — covers common shapes for the message-posting family.
# -----------------------------------------------------------------------------
write_tool_suffixes := {
# postMessage family
"-postmessage",
"-post-message",
"-post_message",
"-sendmessage",
"-send-message",
"-send_message",
# update / edit
"-updatemessage",
"-update-message",
"-update_message",
# scheduled posts
"-schedulemessage",
"-schedule-message",
"-schedule_message",
# ephemeral posts
"-postephemeral",
"-post-ephemeral",
"-post_ephemeral",
"-postephemeralmessage",
"-post-ephemeral-message",
# me-style messages
"-memessage",
"-me-message",
"-me_message",
}
is_slack_write_tool if {
is_slack_tool
name := lower(input.resource.name)
some suffix in write_tool_suffixes
endswith(name, suffix)
}
# -----------------------------------------------------------------------------
# DM-recipient detection — match any destination value that resolves to a
# direct conversation in Slack's ID conventions:
# D... → 1:1 DM channel ID
# U.../W... → user ID (Slack's chat.postMessage auto-opens a DM and posts
# when the channel arg is a user ID)
# G... → multi-party DM / group DM (in workspaces where private channels
# moved to C, G is effectively MPIM-only)
# Channel arg names checked: channel, channel_id, channelId.
# Separately checks user/user_id/userId args for tools that take the recipient
# under a dedicated user field.
# Names (non-ID strings) and other ID shapes don't match — fail-open on
# uncertainty.
# -----------------------------------------------------------------------------
# DM via the channel arg (channel ID OR user ID auto-converted to DM).
is_dm_recipient if {
v := object.get(input.payload.args, "channel", "")
regex.match(`^[DUWG][A-Z0-9]{7,}$`, v)
}
is_dm_recipient if {
v := object.get(input.payload.args, "channel_id", "")
regex.match(`^[DUWG][A-Z0-9]{7,}$`, v)
}
is_dm_recipient if {
v := object.get(input.payload.args, "channelId", "")
regex.match(`^[DUWG][A-Z0-9]{7,}$`, v)
}
# DM via a dedicated user arg (only user IDs are valid here).
is_dm_recipient if {
v := object.get(input.payload.args, "user", "")
regex.match(`^[UW][A-Z0-9]{7,}$`, v)
}
is_dm_recipient if {
v := object.get(input.payload.args, "user_id", "")
regex.match(`^[UW][A-Z0-9]{7,}$`, v)
}
is_dm_recipient if {
v := object.get(input.payload.args, "userId", "")
regex.match(`^[UW][A-Z0-9]{7,}$`, v)
}
# -----------------------------------------------------------------------------
# Deny rule
# -----------------------------------------------------------------------------
allow := false if {
is_slack_write_tool
is_dm_recipient
}
reason := "Sending direct messages via Slack is not permitted via this gateway. Contact your InfoSec team if this needs to change." if not allow Canonical source: policy.md on GitHub · raw · raw on this site (.md)
Used in these guides
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