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.
72
90%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
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