DTwo Policy Store

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
Bundle
slack
Published
Minimum gateway
1.0.0b24
Schema version
1.0.0
Checksum
sha256:3bfcfdee14c1d8409123990b850b96ce507d93079e026b7c7d179b161c353910

slackaccess-controlgovernanceingress

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.

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 with D (1:1 DM channel), U/W (user ID — Slack auto-opens a DM when posting to a user ID), or G (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_suffixes are 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 if branch on input.subject.claims.

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