Control Slack via CDP or headless API tokens. Navigate channels, read/send messages, search conversations, check unreads, and manage status. Two modes: CDP (Slack desktop with --remote-debugging-port) for full UI control, or headless (xoxp/xoxb token) for data operations without Slack running. Triggers on: slack, read slack, search slack, slack unreads, send slack message, slack status, navigate slack, check slack, slack messages, go to channel, slack DM.
97
97%
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Critical
Do not install without reviewing
Control Slack via two modes:
SLACK_USER_TOKEN or SLACK_BOT_TOKEN env var). No Slack
desktop needed. Covers data operations: read, send, search, status.Zero dependencies beyond Node 22. CDP mode also uses cdp-connect.
Slack must be running with remote debugging enabled:
/Applications/Slack.app/Contents/MacOS/Slack --remote-debugging-port=9222If connect fails, tell the user to quit Slack and relaunch with
that flag. Their session persists — they stay logged in.
slack-cdp.js — main script:
if [[ -n "${CLAUDE_SKILL_DIR:-}" ]]; then
SLACK_JS="${CLAUDE_SKILL_DIR}/scripts/slack-cdp.js"
else
SLACK_JS="$(command -v slack-cdp.js 2>/dev/null || \
find ~/.claude -path "*/slack-cdp/scripts/slack-cdp.js" -type f 2>/dev/null | head -1)"
fi
if [[ -z "$SLACK_JS" || ! -f "$SLACK_JS" ]]; then
echo "Error: slack-cdp.js not found." >&2
exit 1
ficdp.js — for UI operations (click, screenshot, ax-tree):
CDP_JS="$(command -v cdp.js 2>/dev/null || echo "$HOME/.local/bin/cdp.js")"node "$SLACK_JS" connect # Verify CDP + auth
node "$SLACK_JS" where # Current view info
node "$SLACK_JS" navigate <query> # Cmd+K quick switcher
node "$SLACK_JS" read [--channel ID] [--limit N] # Read messages
node "$SLACK_JS" send <channel|current> <message> # Send message
node "$SLACK_JS" search <query> [--limit N] # Full-text search
node "$SLACK_JS" whoami # User and team info
node "$SLACK_JS" unread # Unread channels
node "$SLACK_JS" status [":emoji: text"] # Get or set statusAll commands default to port 9222. Override with --port N.
When the user asks about Slack, pick the right tool:
Data operations (read, search, send, status, unread, whoami) → Use the API subcommands. Fast, reliable, structured output.
Navigation (go to a channel, switch DM, open an app)
→ Use navigate <query>. Works from any view.
Verify current state (what channel am I in?)
→ Use where. Reads title, channel ID, active tab from DOM.
Click a UI element (tab, button, sidebar item)
→ Delegate to cdp.js click '<selector>' with data-qa selectors.
Visual debugging (something looks wrong, need to see the screen)
→ Last resort: cdp.js screenshot /tmp/slack.png
Never screenshot to verify navigation. Use where instead.
Common multi-step operations:
Go to a channel and read messages:
node "$SLACK_JS" navigate "engineering"
node "$SLACK_JS" read --limit 20Search and navigate to result:
node "$SLACK_JS" search "deployment issue" --limit 5
# Extract channel from results, then:
node "$SLACK_JS" navigate "channel-name"Summarize unreads:
node "$SLACK_JS" unread
# For each unread channel:
node "$SLACK_JS" read --channel <ID> --limit 10After navigate, always verify with where:
node "$SLACK_JS" navigate "Andrei Tuicu"
node "$SLACK_JS" where
# Confirm title matches expected destinationFor tab switching or button clicks not covered by subcommands,
use cdp.js with Slack's data-qa selectors:
node "$CDP_JS" click '[data-qa="messages"][role="tab"]' --port 9222
node "$CDP_JS" click '[data-qa="message_input"]' --port 9222Prefer data-qa attributes (Slack's own QA hooks) over CSS classes.
If a Slack OAuth token is available via environment variable, data operations can run without Slack desktop or CDP. Two token types:
| Token | Env var | Acts as | Best for |
|---|---|---|---|
xoxp- (user) | SLACK_USER_TOKEN | The user | Full access: read, send, search, status |
xoxb- (bot) | SLACK_BOT_TOKEN | The bot | Channels the bot is in; sends as @bot |
Prefer the user token — it covers every data command. The bot token can only read channels it's been invited to and sends as the bot identity.
# Auth test
curl -s -X POST https://slack.com/api/auth.test \
-H "Authorization: Bearer $SLACK_USER_TOKEN"
# Read channel
curl -s -X POST https://slack.com/api/conversations.history \
-H "Authorization: Bearer $SLACK_USER_TOKEN" \
-d "channel=CHANNEL_ID&limit=10"
# Search (user token only)
curl -s -X POST https://slack.com/api/search.messages \
-H "Authorization: Bearer $SLACK_USER_TOKEN" \
-d "query=your+search&count=5"
# Send message
curl -s -X POST https://slack.com/api/chat.postMessage \
-H "Authorization: Bearer $SLACK_USER_TOKEN" \
-d "channel=CHANNEL_ID&text=Hello"Use headless (token) when:
Use CDP when:
User token (xoxp-) — requires a Slack app with User Token Scopes:
channels:history, channels:read, chat:write, search:read,
users.profile:read, users.profile:writehttps://localhost as a Redirect URLhttps://slack.com/oauth/v2/authorize?client_id=CLIENT_ID&user_scope=channels:history,channels:read,chat:write,search:read,users.profile:read,users.profile:write&redirect_uri=https://localhostcode parameter from the redirect URLcurl -s -X POST https://slack.com/api/oauth.v2.access \
-d "client_id=CLIENT_ID&client_secret=CLIENT_SECRET&code=CODE&redirect_uri=https://localhost"authed_user.access_token field is your xoxp- tokenBot token (xoxb-) — generated automatically when the app is
installed to a workspace. Found on the OAuth & Permissions page.
| Error | Fix |
|---|---|
Cannot connect to CDP on port 9222 | Restart Slack with --remote-debugging-port=9222 |
No Slack page target found | Slack is running but not fully loaded — wait and retry |
enterprise_is_restricted | API method blocked on Enterprise Grid — use search or DOM fallback |
Auth failed | Slack session expired — re-login in Slack, then retry |
navigate uses Cmd+K — works from any view, no need to check starting statesend current <msg> sends to whatever channel is currently opensearch returns results across all channels — good for finding channel IDsstatus with no args reads current status; with args sets itread without --channel reads from the currently visible channel