Skip to content

Slack

Netclaw talks to Slack over Socket Mode — outbound WebSocket connections only. No public URLs, no ingress rules, no reverse proxies. You create a Slack app, give netclaw two tokens, and it shows up in your workspace as a bot.

  • Netclaw installed and initialized (netclaw init)
  • A Slack workspace where you can install apps (some orgs restrict this to workspace admins)

Connecting Slack is a two-part process: create a Slack app to get your tokens, then enter them in netclaw config → Channels. The steps below walk through both.

The fastest path: create from a manifest. Go to api.slack.com/apps and click Create New App.

Slack Your Apps page

Select From a manifest, pick your workspace, and paste this:

{
"display_information": {
"name": "Netclaw",
"description": "AI assistant powered by Netclaw",
"background_color": "#512BD4"
},
"features": {
"bot_user": {
"display_name": "Netclaw",
"always_online": true
}
},
"oauth_config": {
"scopes": {
"bot": [
"app_mentions:read",
"channels:history",
"channels:read",
"chat:write",
"chat:write.customize",
"files:read",
"files:write",
"groups:history",
"groups:read",
"im:history",
"im:read",
"im:write",
"mpim:history",
"mpim:read",
"users:read"
]
}
},
"settings": {
"event_subscriptions": {
"bot_events": [
"app_mention",
"message.channels",
"message.groups",
"message.im",
"message.mpim"
]
},
"interactivity": {
"is_enabled": true
},
"org_deploy_enabled": false,
"socket_mode_enabled": true,
"token_rotation_enabled": false
}
}

Change the name and display_name to whatever you want your bot to be called.

After creating the app, you need two tokens:

  1. App-Level Token — Settings > Basic Information > App-Level Tokens. Click Generate Token and Scopes, name it anything, and add the connections:write scope.

Generate an app-level token dialog

Click Generate and copy the xapp-... token.

  1. Bot Token — Go to Install App in the sidebar and click Install to {Your Workspace}.

Slack Install App page

After approving, copy the xoxb-... Bot User OAuth Token from the OAuth & Permissions page.

Then invite the bot to each channel where it should respond: /invite @YourBotName

ScopeWhy
app_mentions:readReceive @-mention events
channels:historyRead message history in public channels
channels:readResolve channel names to IDs, list public channels
chat:writePost messages and replies in threads, plus native loading status
chat:write.customizePost with custom display name/avatar
files:readDownload files shared in conversations
files:writeUpload files (agent output, attachments)
groups:historyRead message history in private channels
groups:readList private channels the bot is in
im:historyRead DM history for thread context
im:readList DM conversations
im:writeOpen DM conversations for proactive messaging
mpim:historyRead group DM history
mpim:readList group DM conversations
users:readLook up users by name or email for lookup_slack_user

Run netclaw config → Channels, enable Slack, and paste your tokens. Netclaw resolves channel names to IDs and saves everything — no JSON editing required.

The Channels area in netclaw config

The Channels area — enable an adapter and manage its allow-list; here Slack is connected with 2 channels and 1 user.

For scripted or headless installs, store tokens with netclaw secrets:

Terminal window
netclaw secrets set Slack.BotToken xoxb-your-bot-token
netclaw secrets set Slack.AppToken xapp-your-app-token

Then enable Slack and point it at a channel in ~/.netclaw/config/netclaw.json:

{
"Slack": {
"Enabled": true,
"DefaultChannelName": "general"
}
}

Environment variables work too:

Terminal window
export NETCLAW_Slack__BotToken="xoxb-..."
export NETCLAW_Slack__AppToken="xapp-..."

Enter channel names or IDs (comma-separated) in netclaw config → Channels. Netclaw resolves each entry against the Slack API to its canonical channel ID before saving. The stored AllowedChannelIds field holds IDs, not display names; display names are shown dynamically in the config UI.

Resolution rules:

  • Confirmed ID — bot finds the channel by ID: kept as-is.
  • Display name — resolves to its channel ID and saved as the ID.
  • ID-shaped but not enumerable — kept; a channel ID is the stable ACL key and is never dropped, even if the bot can’t list it right now.
  • Unresolvable display name — dropped and flagged with a Warning in the config UI. Re-open netclaw config → Channels after inviting the bot to that channel.

DefaultChannelName is Slack-only and is resolved to a channel ID live at daemon startup, so it is unaffected by the ACL matching rules above.

FieldTypeDefaultDescription
EnabledboolfalseTurn on Slack
SocketModebooltrueMust be true. Only Socket Mode is supported.
BotTokenstringBot User OAuth Token (xoxb-...). Store with netclaw secrets set.
AppTokenstringApp-Level Token (xapp-...). Required for Socket Mode. Store with netclaw secrets set.
DefaultChannelNamestringChannel name, resolved to an ID at startup
DefaultChannelIdstringChannel ID directly (use instead of name if you prefer)
MentionOnlybooltrueOnly respond when @-mentioned
AllowDirectMessagesboolfalseAccept DMs
MentionRequiredInDmboolfalseRequire @-mention even in DMs
AllowedChannelIdsstring[][]Channel allow-list. Empty + no default = all channels denied
AllowedUserIdsstring[][]User allow-list. Empty = everyone in allowed channels is accepted
ChannelAudiencesobject{}Per-channel audience overrides. Keys are channel IDs or "dm". Values: "personal", "team", "public".

Tokens live in ~/.netclaw/config/secrets.json (encrypted at rest).

Slack ACL is default-deny. Three settings decide who talks to the bot and where.

The bot only responds in channels that pass the allow-list:

  • DefaultChannelName or DefaultChannelId allows one channel
  • AllowedChannelIds allows multiple
  • If all three are empty, every channel message is denied. This is the number one setup mistake.
{
"Slack": {
"DefaultChannelName": "openclaw",
"AllowedChannelIds": ["C0123456789", "C9876543210"]
}
}

Finding channel IDs: right-click a channel name in Slack, “View channel details,” scroll to the bottom. Slack’s help article has screenshots.

AllowedUserIds restricts who gets responses:

  • Empty (default) — everyone in allowed channels is accepted
  • Non-empty — only listed user IDs get responses, everyone else is silently dropped

Finding user IDs: click a user’s profile in Slack, open the three-dot menu, “Copy member ID.” Slack’s help article has screenshots.

Users in AllowedUserIds are treated as TrustedInternal by the security model. Everyone else is UntrustedExternal.

DMs are off by default:

{
"Slack": {
"AllowDirectMessages": true
}
}

With DMs on, users can just type normally — MentionRequiredInDm defaults to false, so no @-mention needed.

Audience is resolved per-message: Team for allow-listed channels and allow-listed users; Public for everything else — including unvetted DMs (DMs accepted only because AllowedUserIds is empty). To treat all DMs as Team, set an explicit ChannelAudiences "dm" override. Override with ChannelAudiences:

{
"Slack": {
"ChannelAudiences": {
"C0123456789": "team",
"dm": "personal"
}
}
}

The "dm" key is reserved — it matches every direct message rather than a channel ID. A channel-ID entry takes precedence over it. An unrecognized audience value is rejected outright: the message is denied rather than falling back to a default.

Security Model has the full breakdown on how audiences map to tools and permissions.

Each Slack thread is its own isolated session, and the bot always replies in-thread. Idle threads are freed from memory after 1 hour; the conversation context clears after 2 hours of inactivity.

On daemon restart, thread history is backfilled so in-progress conversations pick up where they left off.

MentionOnly: true (the default) means the bot ignores messages that don’t @-mention it. Two exceptions:

  • Thread replies — if a thread already has an active session, the bot responds to everything in that thread without needing a mention
  • File shares — attached files bypass the mention check entirely, with or without an active thread

Netclaw strips the @-mention before passing text to the LLM.

Netclaw converts LLM markdown to Slack Block Kit: headers, code blocks, blockquotes, lists, bold, italic, strikethrough, inline code, links. Everything renders natively in Slack.

While Netclaw is working on a reply, Slack shows the bot’s native thread status as <Bot Name> is thinking.... This uses Slack’s assistant.threads.setStatus API with the existing chat:write scope, so the manifest above does not need assistant:write or the Slack assistant split view.

When a tool call needs approval, netclaw posts a Block Kit prompt right in the thread:

Tool approval prompt in a Slack thread, with approve and deny buttons

Shows the tool name, the exact command, and the approval buttons. Only the user who triggered the request can approve. System-initiated tool calls (VerifiedAutomation) can be approved by anyone in the thread.

The full prompt offers five choices: Once, This chat, Always here, Always anywhere, and Deny — “Always anywhere” is the broadest grant, so reach for it sparingly. netclaw shows fewer when some don’t apply — a command it can’t cleanly parse (shell control flow, or unbalanced quotes) drops to just Once and Deny. You can reply with the letter shown next to each option instead of clicking.

The LLM can initiate conversations through two built-in tools:

ToolWhat it does
send_channel_messagePosts a message to a Slack channel or DM using a resolved destination. Pass channel_key="slack" and a destination object (with kind, id) from lookup_slack_user or lookup_channel_destination. Respects ACL.
lookup_slack_userSearches users by name, display name, or email. Returns up to 10 matches. Filtered to AllowedUserIds if set. Cached 5 minutes.

The bot drops: empty messages (no text, no files), hidden messages, other bots’ messages, its own messages, messages with unsupported subtypes (edits, joins, bot subtype messages), DMs when AllowDirectMessages is off, and un-mentioned channel messages when MentionOnly is on and there’s no active thread.

Restart the daemon and check status:

Terminal window
netclaw daemon stop && netclaw daemon start
netclaw status

Slack should show connected. If it doesn’t, run netclaw doctor — it checks token validity and ACL config.

Then @-mention the bot in an allowed channel. If it responds, you’re set.

Common problems and fixes are in Channel Troubleshooting. The hits:

  • Connected but silentAllowedChannelIds is empty and no default channel is set, so all traffic gets denied
  • Works in some channels, not others — channel missing from AllowedChannelIds, or the bot hasn’t been invited
  • Socket Mode keeps disconnecting — the xapp-... App-Level Token may have expired or been revoked