Slack: Deny DM and Private-Conversation Reads and Search
Denies the agent read reach into Slack DMs and private conversations on the paths below — the workspace's highest concentration of PII/PHI (HR issues, health…
- Direction
- ingress
- Rego package
slack.ingress.guard_dm_privacy- App
- slack
- Bundles
- soc2gdpr-ccpa
- Published
- Minimum gateway
- 1.0.0b24
- Schema version
- 1.0.0
- Checksum
sha256:0c759aa4beabbeace568b66ab311bf1e1d5b305ceb521e898868e53035d25397
slackprivacydmaccess-controlingresssoc2gdpr-ccpa
What this policy does
Direction: ingress (tool_pre_invoke)
Default: deny on match, allow otherwise
Package: slack.ingress.guard_dm_privacy
What it does
Denies the agent read reach into Slack DMs and private conversations on the paths below — the workspace's highest concentration of PII/PHI (HR issues, health disclosures, credentials, M&A chatter). Three independent deny branches (see Known limitations for read surfaces outside these branches):
- Private-scope search tools — any tool whose (lowercased) name ends
with
_search_public_and_private. The official Slack MCP server splits private scope into this dedicated tool name, so the name alone is sufficient to detect the private reach. - DM-filtered message search — the korotovsky community server's
conversations_search_messageswhen itsfilter_in_im_or_mpimargument is set truthy (booleantrue, the number1, or the strings"true"/"1"/"yes"). - DM history reads — the history/read tool family
(
slack_read_channel,slack_read_thread,conversations_history,conversations_replies,slack_get_channel_history,slack_get_thread_replies) when thechannel_idargument starts withD(a 1:1 DM channel ID) or@(the korotovsky@username_dmalias).
Callers whose IdP groups claim contains slack-private-ok are exempt.
Missing identity fails closed: no subject, no claims, or no matching group
means no exemption.
Public-channel reads and search (slack_search_public,
slack_search_channels, history reads on C-prefixed channel IDs) pass
through untouched. This closes the read-side gap left by
deny-direct-messages, which only
blocks DM sends.
Compliance alignment
This policy fences the agent's read reach into DMs and private conversations — the workspace's highest concentration of personal and special-category data — behind an explicit, IdP-asserted group, supporting minimum-necessary and access-management controls on the MCP path:
- SOC 2 CC6.3 — supports role-based least privilege: DM and
private-conversation reads require the explicit
slack-private-okgroup, with a read-only-public default for everyone else; C1.1 — supports identifying and protecting confidential information held in private conversations. - HIPAA §164.502(b) / §164.514(d) — supports the minimum-necessary standard when DMs and private channels carry health-related disclosures; §164.308(a)(4) — supports information access management by restricting which conversations the agent may read.
- GDPR Art. 9 / Art. 5(1)(c) — supports limiting access to special-category data (health and HR disclosures common in DMs) and data minimisation on the agent channel; CPRA §1798.121 — supports the consumer's right to limit use of sensitive personal information by keeping private-conversation content out of agent context absent an explicit role.
Why ingress
The private reach is fully visible in the request (tool name, filter argument, channel ID), so the call can be stopped before any DM content ever leaves Slack. Egress redaction would already have pulled the private content into the gateway; ingress denial means it is never fetched.
Tool name matching
All matching is case-insensitive and by suffix, because the DTwo gateway
prefixes tool names with the configured MCP server name (e.g.
slack-mcp-slack_read_channel) and that prefix is not standardized:
*_search_public_and_private— official server private-scope search.*conversations_search_messages— korotovsky message search (denied only when the DM filter is set).- History suffixes:
*slack_read_channel,*slack_read_thread(official);*conversations_history,*conversations_replies(korotovsky);*slack_get_channel_history,*slack_get_thread_replies(archived reference server).
Verify the exact names your gateway sends with the dump-input debug
technique before relying on this in production, and extend
history_tool_suffixes if your Slack MCP server exposes additional
history readers.
Argument shape
- Branch 2 reads
input.payload.args.filter_in_im_or_mpim(korotovsky). - Branch 3 reads
input.payload.args.channel_id— the key used by all six history tools listed above. All argument access goes throughobject.get; a missing argument simply doesn't match (see Known limitations for the fail-open consequence). - The exemption reads
input.subject.claims.groupsviaobject.getchains, so missing claims deterministically deny.
Examples
Denied (official private-scope search)
{
"input": {
"action": "tool_pre_invoke",
"resource": { "name": "slack-mcp-slack_search_public_and_private", "type": "tool" },
"payload": {
"name": "slack-mcp-slack_search_public_and_private",
"args": { "query": "salary review" }
}
}
}
allow = false, reason = "Searching Slack DMs and private conversations is not permitted through this gateway. ...".
Denied (DM history read)
{
"input": {
"action": "tool_pre_invoke",
"resource": { "name": "slack-mcp-slack_read_channel", "type": "tool" },
"payload": {
"name": "slack-mcp-slack_read_channel",
"args": { "channel_id": "D0123456789", "limit": 50 }
}
}
}
allow = false, reason = "Reading Slack DM and private-conversation history is not permitted through this gateway. ...".
Allowed (public search; public-channel history)
{
"input": {
"action": "tool_pre_invoke",
"resource": { "name": "slack-mcp-slack_search_public", "type": "tool" },
"payload": {
"name": "slack-mcp-slack_search_public",
"args": { "query": "deploy schedule" }
}
}
}
allow = true, no reason. Same for slack_read_channel with
"channel_id": "C0123456789".
Allowed (exempt caller)
A caller whose input.subject.claims.groups contains slack-private-ok
may run any of the calls above.
Composition
Single-purpose; composes with the other Slack ingress policies:
deny-direct-messages— the write side of the same boundary (blocks DM sends; this policy blocks DM reads).deny-read-search-summarize-sensitive-channels— channel-ID-specific denies for named sensitive channels, including private channels this policy cannot identify by ID shape.block-secrets— outbound DLP on sends.
Known limitations
- Group-DM and private-channel history reads are not caught by branch 3's
ID-shape check. Branch 3 denies history reads only on
D(1:1 DM) and the korotovsky@username_dmalias, per this policy's spec. Group DMs / legacy private channels carry aGprefix, and Slack now assigns newly created private channels the sameCprefix as public channels — neither is distinguishable from a public read by ID shape here, so a direct history read on aG- orC-prefixed private conversation passes through. Use thedeny-read-search-summarize-sensitive-channelscompanion policy to pin specific private/group channel IDs. korotovsky also accepts a#channel-namestring alias aschannel_id; a private channel referenced by#nameis likewise not caught by theD/@shape check and falls under the same companion-policy pinning. Branches 1 and 2 still cover private channels and group DMs for search, because those surfaces declare their scope (dedicated tool name /filter_in_im_or_mpim). - Read surfaces beyond the six history tools are not covered. Branch 3
matches only the six enumerated history/thread readers on a DM-shaped
channel_id. Other read tools that can surface DM/private content are out of scope by design: the korotovskyconversations_unreads(unread messages across all conversations, DMs included under a browser-token deployment) andsaved_list(saved messages, which may include saved DM messages), and the officialslack_read_canvas(a canvas that may live in a private channel or DM). None of these takes a DM-shapedchannel_idthis policy can key on, so each passes through. If these surfaces are in scope for your deployment, add the tool to a companion deny policy or pair with a group-scoped egress redaction policy on their responses. - Unfiltered korotovsky search may still surface DM content. Branch 2
denies
conversations_search_messagesonly whenfilter_in_im_or_mpimis set. Under a browser-token deployment the community server inherits the human user's full visibility, so a search without the filter can still return DM/mpim matches server-side. This policy trusts the filter as the DM-scope signal (per the landscape research); if your deployment returns private matches on unfiltered search, pair this with an egress redaction policy on search responses. - Missing
channel_idfails open on branch 3. A history tool called with nochannel_id(or with the target under a different key) is not matched. The six covered tools all takechannel_idper the mid-2026 landscape research; re-verify if your server differs. - Official tool names are observed, not contractual. Slack publishes
exact names only at runtime (
tools/listis the source of truth); the names here are corroborated from mid-2026 research but may change. - ID matching is exact-case. Slack channel IDs are uppercase; a
lowercase
d…value is not a valid Slack ID and is not matched. - Group name is a placeholder — replace
slack-private-okwith your IdP's group name at import time. The exemption fails closed when the caller has nogroupsclaim.
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.guard_dm_privacy
# Deny-by-default: only the explicit allow rules below permit the request.
default allow := false
# Placeholder IdP group whose members may reach DMs and private conversations.
# Replace "slack-private-ok" with your IdP's group name at import time.
private_ok_group := "slack-private-ok"
# Tool arguments, safe against a missing payload/args.
args := object.get(object.get(input, "payload", {}), "args", {})
# -----------------------------------------------------------------------------
# Exemption — fails closed: no subject, no claims, or no groups → not exempt.
# -----------------------------------------------------------------------------
caller_exempt if {
claims := object.get(object.get(input, "subject", {}), "claims", {})
groups := object.get(claims, "groups", [])
some g in groups
g == private_ok_group
}
# -----------------------------------------------------------------------------
# Branch 1 — official Slack MCP server: the dedicated private-scope search
# tool. The server splits DM/private reach into its own tool name, so the
# name alone identifies the private scope.
# -----------------------------------------------------------------------------
private_scope_search if {
endswith(lower(input.resource.name), "_search_public_and_private")
}
# -----------------------------------------------------------------------------
# Branch 2 — korotovsky/slack-mcp-server: conversations_search_messages
# scopes the search into DMs/group DMs via the filter_in_im_or_mpim argument.
# -----------------------------------------------------------------------------
dm_filtered_search if {
endswith(lower(input.resource.name), "conversations_search_messages")
dm_filter_set
}
# Boolean form of the filter.
dm_filter_set if {
object.get(args, "filter_in_im_or_mpim", false) == true
}
# String forms of the filter ("true", "1", "yes") — defensive against clients
# that serialize booleans as strings.
dm_filter_set if {
v := object.get(args, "filter_in_im_or_mpim", "")
is_string(v)
lower(v) in {"true", "1", "yes"}
}
# Numeric form of the filter (1) — defensive against clients that serialize the
# flag as a JSON number rather than a boolean or string.
dm_filter_set if {
object.get(args, "filter_in_im_or_mpim", false) == 1
}
# -----------------------------------------------------------------------------
# Branch 3 — history/read tools targeting a direct conversation. Covers the
# official server, korotovsky, and the archived reference server. Matched by
# suffix because the gateway prefixes tool names with the MCP server name.
# -----------------------------------------------------------------------------
history_tool_suffixes := {
"slack_read_channel", # official
"slack_read_thread", # official
"conversations_history", # korotovsky
"conversations_replies", # korotovsky
"slack_get_channel_history", # archived reference server
"slack_get_thread_replies", # archived reference server
}
is_history_tool if {
name := lower(input.resource.name)
some suffix in history_tool_suffixes
endswith(name, suffix)
}
# D-prefixed value → 1:1 DM channel ID (Slack IDs are uppercase).
private_history_read if {
is_history_tool
startswith(object.get(args, "channel_id", ""), "D")
}
# @-prefixed value → korotovsky's @username_dm alias for a DM.
private_history_read if {
is_history_tool
startswith(object.get(args, "channel_id", ""), "@")
}
# -----------------------------------------------------------------------------
# Decision
# -----------------------------------------------------------------------------
denied if private_scope_search
denied if dm_filtered_search
denied if private_history_read
# Anything that doesn't reach into DMs/private conversations passes through.
allow if {
not denied
}
# Members of the exemption group may reach private conversations.
allow if {
caller_exempt
}
reasons contains "Searching Slack DMs and private conversations is not permitted through this gateway. Use the public-channel search tool instead, or ask your InfoSec team for the slack-private-ok group if your role requires private-scope access." if {
private_scope_search
not caller_exempt
}
reasons contains "Slack message search scoped to DMs and group DMs (filter_in_im_or_mpim) is not permitted through this gateway. Re-run the search without the DM filter, or ask your InfoSec team for the slack-private-ok group if your role requires it." if {
dm_filtered_search
not caller_exempt
}
reasons contains "Reading Slack DM and private-conversation history is not permitted through this gateway. Read public channels instead, or ask your InfoSec team for the slack-private-ok group if your role requires DM access. Contact your InfoSec team if this block is a false positive." if {
private_history_read
not caller_exempt
}
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)
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