Skip to content

Slack

netclaw talks to Slack over Socket Mode, which means outbound WebSocket connections only. You need no public URL, no ingress rule and no reverse proxy. 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 organizations limit that to admins)

Connecting Slack has two parts. Create a Slack app to get the tokens, then enter them in netclaw config → Channels.

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

Slack Your Apps page

The page has a filter box for your existing apps and the Create New App button at the top right.

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

{
"display_information": {
"name": "Netclaw",
"description": "AI assistant powered by Netclaw",
"background_color": "#512BD4"
},
"features": {
"app_home": {
"home_tab_enabled": false,
"messages_tab_enabled": true,
"messages_tab_read_only_enabled": false
},
"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",
"users:read.email"
]
}
},
"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 name and display_name to what you want the bot called.

The manifest turns on the App Home Messages tab (messages_tab_enabled), which is what lets people DM the bot. If you create the app another way and skip it, Slack tells anyone who tries to DM the bot “Sending messages to this app has been turned off”. You can fix that under Features > App Home > Messages Tab.

After the app exists, you need two tokens.

  1. App-level token. Open Settings > Basic Information, scroll to App-Level Tokens, click Generate Token and Scopes, name it anything, and add the connections:write scope.

    Generate an app-level token dialog

    The dialog shows the connections:write scope that Socket Mode needs. Click Generate and copy the xapp-... token.

  2. Bot token. Open Install App in the sidebar and click Install to {Your Workspace}.

    Slack Install App page

    The install page is also where you reinstall after changing scopes. After you approve, copy the xoxb-... Bot User OAuth Token from OAuth & Permissions.

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

ScopeWhy
app_mentions:readReceive @-mention events
channels:historyRead history in public channels
channels:readResolve channel names to IDs, list public channels and read channel info
chat:writePost replies in threads, and set the native loading status
chat:write.customizePost under a custom name or icon. Netclaw doesn’t do this today.
files:readDownload files shared in conversations
files:writeUpload files such as agent output
groups:historyRead history in private channels
groups:readList private channels the bot is in
im:historyReceive DM message events and read DM thread replies for context
im:readRead DM details, such as the name of a DM listed in AllowedChannelIds
im:writeOpen DMs for proactive messages
mpim:historySlack requires it for the message.mpim event the manifest subscribes to, and for reading group DM thread replies
mpim:readRead group DM details, such as the name of a group DM listed in AllowedChannelIds
users:readLook up users by name for lookup_slack_user
users:read.emailLook up users by email. Slack returns the email field only with this scope.

If you installed from an older copy of this manifest, add users:read.email under OAuth & Permissions and reinstall the app, or email lookups won’t match.

Run netclaw config → Channels, enable Slack, and paste your tokens. netclaw resolves channel names to IDs and saves, so you don’t edit JSON.

The Channels area in netclaw config

The Channels area lists Slack, Discord and Mattermost with a checkbox each. Once an adapter is set up, its row also shows how many channels and users it allows. Nothing is set up in this capture.

When you save from this screen, netclaw writes the first channel as DefaultChannelId and removes DefaultChannelName. If you set DefaultChannelName by hand, the TUI will replace it.

For scripted or headless installs, store the 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, with list entries indexed from zero:

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

An edit to netclaw.json restarts the daemon by itself. A vault or environment change needs a restart you run:

Where it runsRestart
Nativenetclaw daemon stop, then netclaw daemon start
systemd user unitsystemctl --user restart netclaw
Dockerdocker restart <container>

Enter channel names or IDs, comma-separated, in netclaw config → Channels. netclaw resolves each against the Slack API and saves the canonical ID, never the display name.

  • Confirmed ID: kept as is.
  • Display name: resolved and saved as the ID.
  • ID-shaped but not listable: kept. A channel ID is the stable ACL key, so it’s never dropped, even if the bot can’t list the channel right now.
  • Unresolvable display name: dropped and flagged with a warning. Invite the bot, then reopen the screen.

DefaultChannelName is Slack-only and resolves to an ID at daemon startup.

FieldTypeDefaultDescription
EnabledboolfalseTurn on Slack
SocketModebooltrueMust stay true. Slack supports Socket Mode only.
BotTokenstring-Bot User OAuth Token (xoxb-...). Store with netclaw secrets set.
AppTokenstring-App-level token (xapp-...). Store with netclaw secrets set.
DefaultChannelNamestring-Channel name, resolved to an ID at startup
DefaultChannelIdstring-Channel ID, if you’d rather not use a name
MentionOnlybooltrueIn channels, answer only when @-mentioned
AllowDirectMessagesboolfalseAccept DMs
MentionRequiredInDmboolfalseRequire an @-mention even in DMs
MentionRequiredInThreadByChannelobject{}Channel ID to bool. true makes a thread reply in that channel need an @-mention. See Mention behavior.
AllowedChannelIdsstring[][]Channel allow-list. Empty with no default denies every channel message.
AllowedUserIdsstring[][]User allow-list. Empty accepts everyone in allowed channels.
ChannelAudiencesobject{}Per-channel audience overrides. Keys are channel IDs or "dm". Values are "personal", "team" or "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 answers only in channels that pass the allow-list. DefaultChannelName or DefaultChannelId allows one, AllowedChannelIds allows more, and if all of them are empty every channel message is denied. That’s the number one setup mistake.

{
"Slack": {
"DefaultChannelName": "general",
"AllowedChannelIds": ["C0123456789", "C9876543210"]
}
}

To find a channel ID, right-click the channel name, choose View channel details, and scroll to the bottom. Slack’s help article has screenshots.

AllowedUserIds restricts who gets answers. Empty means everyone in an allowed channel. Non-empty means only the listed users, and everyone else is dropped silently. To copy a member ID, open the profile, then the three-dot menu, then Copy member ID (Slack’s help article).

Listed users are TrustedInternal in the security model, and everyone else is UntrustedExternal.

DMs are off by default. Turn them on with "AllowDirectMessages": true. Users then write without a mention, because MentionRequiredInDm defaults to false.

Audience resolves per message: ChannelAudiences[channelId] first, then ChannelAudiences["dm"], then a fallback. The fallback is Team for a channel listed in AllowedChannelIds or a user listed in AllowedUserIds, and Public for everything else. A DM accepted only because AllowedUserIds is empty is Public. So is a channel allowed only through DefaultChannelId or DefaultChannelName, which includes the DefaultChannelName example above, unless you also list it in AllowedChannelIds or set ChannelAudiences.

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

A channel-ID entry beats "dm", and an unrecognized value denies the message instead of falling back. Security model maps audiences to tools.

Each Slack thread is its own session, and the bot always replies in-thread. An idle thread is freed from memory when its session goes idle, after Session.IdleTimeout (one hour by default), and a later reply wakes it again. After a daemon restart, thread history is backfilled so conversations continue.

MentionOnly: true makes the bot ignore channel messages that don’t @-mention it. netclaw strips the mention before the text reaches the model. These rules sit on top:

  • Thread replies: once a thread has an active session, replies in it don’t need a mention, unless MentionRequiredInThreadByChannel is true for that channel.
  • File shares: a message with an attachment follows the same mention rules as any other message. An un-mentioned upload in a channel is ignored.

MentionRequiredInThreadByChannel maps channel IDs to true or false:

{
"Slack": {
"MentionRequiredInThreadByChannel": {
"C0123456789": true
}
}
}

With the rule on, an un-mentioned thread reply is ignored (ThreadMentionRequired in the log). The next message that mentions the bot makes netclaw re-read everything posted since its last reply, so it answers with full context. A channel with no entry behaves as false. The rule doesn’t apply to DMs.

In netclaw config → Channels, Space toggles Require @mention on the selected channel. A channel you add there starts with the rule on if its audience is team or public, and off if it’s personal. Only true entries are saved.

Netclaw sends each reply as a single Slack Markdown block, and Slack renders it, tables included. Replies over 12,000 characters go out as plain text instead.

While netclaw works on a reply, Slack shows the bot’s native thread status as <Bot Name> is thinking.... That uses assistant.threads.setStatus with the existing chat:write scope, so the manifest doesn’t need assistant:write.

When a tool call needs approval, netclaw posts a prompt in the thread with one button per choice. For a prompt from the main agent you can also reply with the letter next to a choice. A prompt from a sub-agent has buttons only, and a typed letter doesn’t answer it.

Tool approval prompt in a Mattermost thread: shell_execute, a push to origin main, and six lettered choices from A (Once) to F (Deny)

The picture shows the same prompt in Mattermost’s text-reply form. Slack renders the same six choices as buttons, with the lettered list as a fallback line underneath. A sub-agent prompt has no lettered line.

The choices are Once, This chat, Always here, Always anywhere, Deny, and This repository when it applies. A command netclaw can’t parse cleanly gets only Once and Deny. How tool approvals work explains what each one grants. Only the user whose request triggered the prompt can answer it. System-initiated calls (VerifiedAutomation) can be answered by anyone in the thread. A prompt that nothing can answer any more is redrawn as a single line with the reason and no buttons, such as This approval request was withdrawn because the task that asked for it ended. Nothing ran.

The model can start conversations with two tools:

ToolWhat it does
send_channel_messagePosts to a Slack channel or DM. Pass channel_key="slack" and a destination (kind, id) from lookup_slack_user or lookup_channel_destination. Respects ACL.
lookup_slack_userSearches users by name, display name or email, up to 10 matches, filtered to AllowedUserIds when set, cached for five minutes. Email matching needs users:read.email.

The bot drops empty messages, hidden messages, its own and other bots’ messages, subtypes it doesn’t handle (edits, joins), DMs when AllowDirectMessages is off, and un-mentioned channel messages when MentionOnly is on and there’s no active thread.

A supervisor checks the Socket Mode connection every 5 seconds and reconnects with exponential backoff up to 5 minutes. You don’t restart the daemon for a dropped connection. A bad token or SocketMode: false is a fatal connect error: Slack stays offline until you fix it and restart.

Terminal window
netclaw status

Look for slack: healthy (enabled) under connectors. Trimmed output from a daemon that has been on Slack for a few minutes:

daemon: PID 25721, uptime 11m 33s, endpoint http://127.0.0.1:5199
...
slack counters: recv=27 routed=27 dropped=26 replied=9 rejected=0 reply_failed=0
...
connectors:
- discord: disabled (disabled)
Discord connector is disabled in configuration.
...
- slack: healthy (enabled)

recv goes up as Slack delivers events, so a number above zero means the bot is hearing the workspace. dropped counts events the bot ignores on purpose, such as a message the ACL denies or a thread reply it isn’t part of. In 0.28.2 it also counts the bot’s own replies coming back from Slack, so expect it to climb with every answer. rejected and reply_failed count replies that Slack refused or that failed to post, and they should stay at 0. If Slack isn’t healthy, run netclaw doctor, which checks the bot token and the ACL config.

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

Channel troubleshooting is organized by symptom. The usual ones:

SymptomLikely cause
Connected but silentAllowedChannelIds is empty and no default channel is set.
Works in some channels, not othersThe channel isn’t in AllowedChannelIds, or the bot wasn’t invited.
Stopped answering in a threadMentionRequiredInThreadByChannel is on for the channel. Mention the bot.
Doesn’t match users by emailThe app lacks users:read.email.
Sending messages to this app has been turned offThe Messages tab is off.
Socket Mode won’t connectThe xapp-... token is wrong or revoked.