Skip to content

Repository files navigation

Slack CLI

A comprehensive Rust CLI tool for Slack, designed for AI agents and automation.

Features

  • Multiple authentication methods: OAuth, browser tokens (xoxc+xoxd), direct tokens (xoxp/xoxb)
  • Full workspace access: Manage channels, messages, DMs, users, user groups, files, pins, bookmarks, custom emoji, reactions, reminders, and status
  • Message and file workflows: Edit, delete, schedule, and search messages; upload and search files
  • Generic API escape hatch: slack api calls any Slack Web API method with your stored auth
  • Agent-first design: JSON output by default, optimized for AI consumption
  • Minimal footprint: No config files, tokens stored in system keyring
  • Fast and reliable: Built with Rust for performance and safety
  • Shell completions: Bash, Zsh, Fish, PowerShell

Agent Skill

If you're using an AI coding agent (Claude, Codex, Cursor, etc.), install the Slack skill so your agent knows how to use this CLI:

npx skills add TeamCadenceAI/slack-cli

The skill lives in skills/slack/ and covers all commands with usage examples.

Installation

Quick Install

curl -fsSL https://raw.githubusercontent.com/TeamCadenceAI/slack-cli/main/install.sh | sh

Installs to ~/.slack/bin/slack and symlinks into ~/.local/bin/slack. On macOS the quarantine attribute is removed automatically so Gatekeeper won't block the binary.

From Source

git clone https://github.com/TeamCadenceAI/slack-cli
cd slack-cli
cargo build --release
# binary at target/release/slack

Or via cargo install:

cargo install --git https://github.com/TeamCadenceAI/slack-cli

Shell Completions

# Bash
slack completions bash > ~/.local/share/bash-completion/completions/slack

# Zsh
slack completions zsh > ~/.zsh/completions/_slack

# Fish
slack completions fish > ~/.config/fish/completions/slack.fish

# PowerShell
slack completions powershell >> $PROFILE

Quick Start

# Authenticate with a token
slack auth add --token xoxp-your-token-here

# Or, optionally, authorize through your own Slack app after configuring its
# redirect URL as described under Authentication
export SLACK_CLIENT_ID="your-slack-app-client-id"
export SLACK_CLIENT_SECRET="your-slack-app-client-secret"
slack auth add

# Check auth status
slack auth status

# List channels
slack channels list

# Send a message
slack messages send "#general" "Hello from CLI!"

# Search messages
slack messages search "important updates"

Commands

Authentication (slack auth)

# Add a user or bot token
slack auth add --token xoxp-...
slack auth add --token xoxb-...

# Add browser token (xoxc + xoxd cookie)
slack auth add --xoxc xoxc-... --xoxd xoxd-...

# Import from a workspace you're already signed into locally (Slack desktop app
# or a browser) - no manual token copying. Give just the subdomain or full URL:
slack auth add onlinegeniuses
slack auth add onlinegeniuses.slack.com
slack auth add myteam --browser slack   # narrow the source app/browser

# Discover which workspaces are signed into local apps (reads local storage
# only - no Keychain access or network calls unless you pass --check)
slack auth discover
slack auth discover --browser slack
slack auth discover --check            # also validate each token is live

# List configured workspaces (add --check to verify each token via auth.test)
slack auth list
slack auth list --check

# Show current auth status
slack auth status

# Switch default workspace
slack auth switch T1234567890

# Remove a workspace
slack auth remove T1234567890

# Get help extracting browser tokens
slack auth browser-help

OAuth with a Slack app

OAuth is an optional alternative to direct tokens and local session import. In your Slack app's OAuth & Permissions settings, add this exact redirect URL:

http://localhost:8765/callback

Then export the Slack app credentials shown in Basic Information (these identify the app; they are not a Slack access token):

export SLACK_CLIENT_ID="your-slack-app-client-id"
export SLACK_CLIENT_SECRET="your-slack-app-client-secret"

Run OAuth without a positional workspace or --url:

slack auth add             # default OAuth route; opens a browser
slack auth add --oauth     # explicitly selects the same browser flow
slack auth add --manual    # prints the URL and asks for the full redirect URL

# Replace the defaults with an explicit comma-separated scope list
slack auth add --oauth --scopes channels:read,channels:history,users:read,search:read,chat:write

The browser flow listens on localhost:8765 for the callback. The manual flow uses the same configured callback URL but does not need to receive it: after Slack redirects (the page may fail to load), paste the full URL from the browser's address bar into the CLI. If either credential variable is missing, auth add returns a configuration error instead of starting OAuth.

The current CLI scope defaults are channels:read, channels:history, users:read, and search:read. --scopes replaces that list; scopes needed by other commands are not added automatically, so request the complete set your Slack app and intended commands require. Successful OAuth tokens use the same system keyring as other auth methods, or the existing file store when SLACK_TOKEN_STORE_PATH is set.

A positional workspace, --url, or --from-browser selects local token extraction instead of OAuth. Direct --token and --xoxc/--xoxd flows are unchanged.

Selecting a workspace

With multiple workspaces configured, target one per command with -w / --workspace (or the SLACK_WORKSPACE env var). The value is matched against each workspace's team ID or domain — team names are not matched (they're user-editable and too volatile to be a stable selector):

slack -w T04U8BDD0KC channels list        # by team ID
slack -w cadence-app channels list         # by domain (subdomain)
slack -w cadence-app.slack.com channels list   # full URL also accepted

export SLACK_WORKSPACE=cadence-app         # session default
slack channels list

Run slack auth list to see the selectable values. Its columns are team_id, domain, name, token_type, and a * default marker (JSON includes a team_domain field):

T04U8BDD0KC   cadence-app   Cadence   Browser   *

The stored default (set via slack auth switch <team_id|domain>) is used when no -w / SLACK_WORKSPACE is given.

Channels (slack channels or slack c)

# List channels (public and private by default)
slack channels list

# List with specific types
slack channels list --types public_channel,private_channel,mpim,im

# Sort by popularity (member count)
slack channels list --sort-popularity

# Exclude archived channels
slack channels list --exclude-archived

# Get channel info by ID or name
slack channels info C123456789
slack channels info #general

# List direct messages
slack channels dms

# Export channels to CSV
slack channels export --output channels.csv

Channel members

# Member IDs, preserving Slack's order
slack channels members "#general"

# Resolve IDs to usernames (display name, then ID, are fallbacks)
slack --plain channels members "#general" --resolve

Member listing requires access to the conversation and its applicable read scope. --resolve additionally requires users:read and loads the workspace user directory once. IDs Slack omits from that directory remain visible with the ID as their name.

Channel lifecycle

slack channels create project-room
slack channels create leadership --private
slack channels join "#project-room"
slack channels invite "#project-room" @alice U123456789
slack channels set-topic "#project-room" "Quarterly launch"
slack channels set-purpose "#project-room" "Coordinate the launch"
slack channels rename "#project-room" launch-room
slack channels leave "#launch-room"
slack channels archive "#launch-room"
slack channels unarchive "#launch-room"

Names and IDs are accepted for channel operands, including archived channel names. Invitees are resolved to user IDs and deduplicated before one invite; if any user cannot be resolved, nobody is invited. These operations require the applicable Slack channel-management, join, and invite scopes and, where Slack requires it, membership or administrator permission. Archiving is destructive to normal channel use until an authorized user unarchives it; the CLI does not ask for confirmation.

Pass an empty string to set-topic or set-purpose to clear it. JSON mutations return {"ok":true,"channel":...}; --plain prints the channel ID.

Unread overview

slack channels unread
slack --plain channels unread

This command uses the Web API only. It checks joined public/private channels, DMs, and group DMs and includes positive counts when conversations.info exposes unread_count_display or unread_count. Slack omits these fields for some workspaces and token types, so this is a capability-dependent overview, not a guaranteed complete unread view. JSON lists omitted-count conversation IDs in unavailable_channels; if no eligible conversation exposes any count, the command fails with unread_unavailable. Applicable conversation read scopes are required.

Messages (slack messages or slack m)

# List messages in a channel (default: last 50)
slack messages list "#general"
slack messages list C123456789 --limit 100

# List messages from last 7 days
slack messages list "#general" --limit 7d

# Send a message (text is Markdown by default and converted to Slack mrkdwn)
slack messages send "#general" "Hello, **world**! See [docs](https://example.com)"

# Send verbatim, no Markdown conversion / mrkdwn parsing
slack messages send "#general" "literal *text*" --format plain

# Reply to a thread
slack messages send "#general" "Reply text" --thread-ts 1234567890.123456

# Read message from stdin
echo "Message from pipe" | slack messages send "#general" --stdin

# View thread replies
slack messages thread "#general" 1234567890.123456

# Search messages
slack messages search "important updates"
slack messages search "from:@username budget"
slack messages search "in:#general project" --count 50

# Get a specific message
slack messages get "C123456789:1234567890.123456"

Bounded and resolved reading

# Exclusive UTC bounds: dates, RFC3339 instants, or Slack timestamps
slack messages list "#general" --since 2026-01-01 --until 2026-02-01

# Fetch complete bounded history; numeric --limit is ignored with --all
slack messages list "#general" --since 1735689600.000001 --all

# Resolve authors and <@user> mentions in any read command
slack messages list "#general" --resolve-users
slack messages thread "#general" 1234567890.123456 --resolve-users
slack messages search "incident" --sort score --sort-dir desc --resolve-users

--since and --until are exclusive UTC bounds. A duration --limit still contributes an oldest bound, and the later of it and --since is used. --all conflicts with --cursor, reads pages of 200 in Slack response order, and ignores a numeric --limit. User resolution loads the complete paginated workspace directory once per nonempty command. JSON keeps user and adds user_name; explicitly resolved --plain output uses the name in its second (author) column and rewrites known mentions while preserving unknown markup.

Message formatting

messages send treats input as standard Markdown by default (--format markdown) and converts it to Slack mrkdwn before sending, so agents and scripts can emit ordinary Markdown:

Markdown Sent as (mrkdwn) Renders as
**bold**, __bold__ *bold* bold
*italic*, _italic_ _italic_ italic
~~strike~~ ~strike~ strike
# Heading *Heading* bold line
[text](url) <url|text> linked text
![alt](url) <url|alt> link
- item / * item / + item • item bullet
1. item / 1) item 1. item numbered

Inline code `…`, fenced code blocks ```, and existing mrkdwn spans (<@U…> mentions, <url|text> links) are passed through untouched. Use --format plain to send text verbatim with mrkdwn parsing disabled.

Immediate send JSON adds a top-level "permalink", and messages get fills the message object's existing "permalink" field, for example:

{"ok":true,"channel":"C123456789","ts":"1234567890.123456","message":{"ts":"1234567890.123456"},"permalink":"https://workspace.slack.com/archives/C123456789/p1234567890123456"}
{"ts":"1234567890.123456","text":"hello","permalink":"https://workspace.slack.com/archives/C123456789/p1234567890123456"}

Permalink lookup is best-effort for these two commands: a successful send/read remains successful with permalink: null and a warning on stderr if enrichment fails. Their existing --plain output is unchanged and makes no permalink request.

Message operations

slack messages edit "#general:1234567890.123456" "Updated **text**"
slack messages edit "https://workspace.slack.com/archives/C123456789/p1234567890123456" "literal *text*" --format plain
slack messages delete "#general:1234567890.123456"
slack messages permalink "#general:1234567890.123456"
slack messages mark "#general" 1234567890.123456

Identifiers are parsed locally as channel:timestamp or Slack permalinks; permalink URLs are never fetched. Explicit messages permalink errors are strictly propagated. Mutation JSON reports ok, channel, and message IDs; --plain prints only the timestamp (or URL for permalink). Delete does not prompt for confirmation, and Slack remains authoritative for ownership and permissions.

Advanced and scheduled sending

# Broadcast a thread reply (requires --thread-ts)
slack messages send "#general" "Visible reply" --thread-ts 1234567890.123456 --broadcast

# Block Kit from a file, with optional fallback text
slack messages send "#general" "Fallback text" --blocks blocks.json
cat blocks.json | slack messages send "#general" --blocks -

# Natural times use the machine's local timezone
slack messages send "#general" "Daily summary" --schedule "tomorrow at 9am"
slack messages scheduled list
slack messages scheduled delete "#general" Q123456789

--blocks must contain a nonempty JSON array; block strings are not Markdown converted. Block-only sends are allowed. --blocks - owns stdin and conflicts with --stdin. Scheduling accepts natural expressions, Unix timestamps, RFC3339, and existing reminder date forms, must be in the future and no more than 120 days away, and supports threads, formats, and blocks. --schedule conflicts with --mark-read and --broadcast. Scheduled sends do not post, mark read, or fetch a permalink; JSON reports permalink: null, while plain send output remains one scheduled message ID. Scheduled list plain output is id<TAB>channel_id<TAB>post_at<TAB>text.

Users (slack users or slack u)

# List all users
slack users list

# List only active users
slack users list --active-only

# Get current user info
slack users me

# Get user info by ID, name, or email
slack users info U123456789
slack users info @username
slack users info alice@example.com

# Send a direct message (opens or reuses the IM, then sends normally)
slack messages send @username "Hello directly"
slack messages send U123456789 "Hello by user ID"

# List user groups and group members
slack users groups list
slack users groups members @engineering
slack users groups members S123456789 --resolve

# Export users to CSV
slack users export --output users.csv

Email lookup requires users:read.email, and user-group commands require usergroups:read. Direct-message opening uses conversations.open and normally requires im:write (or the applicable conversation-write scope for the Slack token type). Slack API missing-scope errors are returned unchanged.

Files (slack files or slack f)

# List recent files
slack files list

# List files in a channel
slack files list --channel "#general"

# Filter and paginate files
slack files list --user U123456789 --limit 50 --cursor NEXT_CURSOR

# Get file info
slack files info F123456789

# Download a file (optionally emit base64)
slack files get F123456789 --output ./downloads/report.pdf
slack files get F123456789 --base64

# Upload a file (filename defaults to the path basename)
slack files upload ./report.pdf --channel "#general" --title "Quarterly report"
slack files upload ./data.bin --filename archive.bin

# Share an upload in a thread with a comment
slack files upload ./notes.txt --channel C123456789 \
  --comment "Meeting notes" --thread-ts 1234567890.123456

# Search files (user or browser token only)
slack files search "quarterly report"
slack files search "from:alice has:pdf" --count 50 --page 2

Uploads use Slack's external-upload flow and require the files:write scope. --filename must be supplied when the path has no UTF-8 basename; otherwise its standalone value overrides the basename. --comment and --thread-ts require --channel. File search requires search:read and a user OAuth or stored browser token; Slack does not support file search with bot tokens.

Reactions (slack reactions or slack r)

# Add a reaction
slack reactions add C123456789 1234567890.123456 thumbsup

# Remove a reaction
slack reactions remove C123456789 1234567890.123456 thumbsup

# List reactions on a message
slack reactions list C123456789 1234567890.123456

Pins (slack pins)

# Pin or unpin a message
slack pins add "#general" 1234567890.123456
slack pins remove C123456789 1234567890.123456

# List all pinned items in a channel
slack pins list "#general"

Adding and removing pins requires pins:write; listing requires pins:read. Pin list JSON preserves message, file, and other item records returned by Slack.

Emoji (slack emoji)

# List workspace custom emoji
slack emoji list
slack --plain emoji list

emoji list requires emoji:read and returns custom workspace emoji only, including unchanged image URLs and alias:<name> values. It does not include Slack's built-in Unicode emoji.

Bookmarks (slack bookmarks)

# List channel bookmarks
slack bookmarks list "#general"

# Add a link bookmark, optionally with an emoji
slack bookmarks add "#general" "Team docs" https://example.com/docs
slack bookmarks add C123456789 "Runbook" https://example.com/runbook --emoji :books:

# Remove a bookmark
slack bookmarks remove "#general" Bk123456789

Listing requires bookmarks:read; adding and removing require bookmarks:write. JSON output retains optional bookmark metadata. With --plain, lists use id<TAB>title<TAB>link<TAB>emoji, while mutations print the bookmark ID.

Status (slack status or slack s)

# Get current status
slack status get

# Set status with emoji and text
slack status set "Taking a break" --emoji coffee

# Set status with expiration
slack status set "In a meeting" --emoji meeting --expires 1h
slack status set "Out of office" --emoji calendar --expires today
slack status set "On vacation" --emoji palm_tree --expires tomorrow

# Clear status
slack status clear

# Set presence
slack status presence away
slack status presence auto

Reminders (slack reminders)

# List reminders
slack reminders list

# Create a reminder
slack reminders add "Review PRs" --when "in 2 hours"
slack reminders add "Team meeting" --when "tomorrow at 10am"

# Complete a reminder
slack reminders complete Rm123456789

# Delete a reminder
slack reminders delete Rm123456789

Generic API (slack api)

Escape hatch for any Slack Web API method that doesn't have a dedicated command. Reuses your stored credentials (including browser xoxc token + xoxd cookie), the -w/--workspace selector, and --token/SLACK_TOKEN overrides — your token's scopes still apply, so a method can fail with missing_scope just as it would with curl.

# Call a method (POST is the default HTTP method)
slack api conversations.create -f name=my-new-channel

# GET with query parameters
slack api conversations.info -X GET -f channel=C123456789

# Typed fields: -F parses JSON booleans, numbers, arrays and objects
slack api conversations.list -X GET -F limit=200 -F exclude_archived=true
slack api chat.postMessage -f channel=C123 -f text=hi -F unfurl_links=false

# Load the parameter object from a JSON file or stdin
slack api chat.postMessage --input params.json
echo '{"channel":"C123","text":"hi"}' | slack api chat.postMessage --input -

# Full Slack URLs are accepted and normalized to the method name
slack api https://slack.com/api/team.info -X GET

Flags

Flag Description
-X, --method <GET|POST> HTTP method. Defaults to POST (unlike gh api, which defaults to GET).
-f, --raw-field key=value Add a parameter as a plain string. Repeatable.
-F, --field key=value Add a typed parameter: true/false, numbers, and JSON arrays/objects are parsed as JSON; anything else is a string. -F key=null is rejected — omit the field instead. Repeatable.
--input FILE Read the parameter object from a JSON file, or from stdin with --input -. Cannot be combined with -f/-F. Fields whose value is null are omitted, matching the CLI's form encoder.

Duplicate parameter names (across -f/-F) are rejected.

Request conventions

  • Only GET and POST are supported. GET sends parameters as URL query parameters; POST sends them form-encoded (application/x-www-form-urlencoded), which is what the Slack Web API expects.
  • There is no raw JSON request body: --input loads a JSON parameter object which is then form-encoded like -f/-F fields. Nested arrays and objects (e.g. blocks, attachments) are serialized as JSON strings, per Slack convention.
  • Endpoints are Web API method names (conversations.info) or full https://slack.com/api/<method> URLs, which are normalized to the method name. URLs with other hosts, embedded credentials, query strings, fragments, or extra path segments are rejected. (SLACK_API_BASE_URL still overrides the base URL for testing/mocks.)
  • HTTP redirects are never followed, so your token and cookies can't leak to another host.
  • No automatic pagination — pass cursor/limit yourself and follow response_metadata.next_cursor.
  • Not supported by slack api: custom headers, raw-body file uploads, name→ID resolution, jq filtering, or Edge API endpoints. Use slack files upload for Slack's supported external-upload flow.

Output is the full JSON response from Slack on success. --plain is not supported for slack api. Slack ok: false responses, rate limits, and network failures exit with status 1 and structured JSON error codes; invalid arguments exit with status 2. Rate limits are retried at most five times; a server-requested delay over 60 seconds is returned immediately as a rate-limit error rather than blocking or retrying too early.

Output Modes

By default, output is JSON (optimized for AI agents). Use --plain for human-readable TSV output:

# JSON output (default)
slack channels list

# Plain text output (TSV format)
slack --plain channels list
slack channels list --plain

slack api is JSON-only and rejects --plain.

Global Options

--plain            # Plain TSV output instead of JSON
-w, --workspace    # Select workspace by team ID (T…) or domain (myteam / myteam.slack.com)
--token            # Override token (skip keyring)
-v, --verbose      # Enable verbose logging to stderr
--help             # Show help
--version          # Show version

Environment Variables

Variable Description
SLACK_TOKEN Default token (overrides keyring)
SLACK_WORKSPACE Default workspace (team ID or domain, same as -w)
SLACK_TOKEN_STORE_PATH Use file-based storage instead of keyring
SLACK_API_BASE_URL Override API base URL (for testing)

Token Types

Prefix Type Use Case
xoxp- User OAuth Full user access
xoxb- Bot OAuth Bot access (no search)
xoxc- Browser Requires xoxd cookie

Exit Codes

Code Meaning
0 Success
1 Any runtime failure: authentication required, API error (ok: false), rate limited, network error, not found, etc.
2 Usage error: invalid arguments or flags (including errors reported by the argument parser)

The exit code only distinguishes usage errors from runtime failures. To find out what failed, read the JSON error object on stdout — its code field is stable (auth_required, api_error, rate_limited, network_error, channel_not_found, user_not_found, search_not_available, usage_error, …) and detail carries the Slack error string when there is one:

{"error": true, "code": "auth_required", "message": "Authentication required. Run: slack auth add", "detail": null}

Development

# Build
cargo build

# Run tests
cargo test

# Run with verbose output
cargo run -- -v channels list

# Format code
cargo fmt

# Lint
cargo clippy

FAQ

Q: Why do I get "auth_required" errors?

A: You need to authenticate first with slack auth add --token xoxp-.... Make sure your token is valid and has the required scopes.

Q: How do I use browser tokens?

A: Run slack auth browser-help for detailed instructions on extracting xoxc and xoxd tokens from your browser.

Q: Can I use this with a bot token?

A: Yes, but bot tokens (xoxb-*) have limited access. Notably, search is not available with bot tokens.

Q: How do I switch between multiple workspaces?

A: Use slack auth switch <workspace> to set the default workspace. You can also use --workspace or -w flag to specify a workspace for a single command.

Q: Where are my tokens stored?

A: Tokens are stored in your system keyring (macOS Keychain, Windows Credential Manager, or Linux Secret Service). Set SLACK_TOKEN_STORE_PATH to use file-based storage instead (useful for testing).

Q: Authentication isn't persisting - what should I do?

A: See the Troubleshooting section below for keyring debugging steps and file-based fallback options.

Q: How do I get JSON output for scripting?

A: JSON is the default output format. Use --plain if you want human-readable TSV output.

Troubleshooting

Authentication Not Persisting

If slack auth add succeeds but slack auth list shows no workspaces or you get "auth_required" errors:

1. Test keyring access:

# Run the keyring test utility
cargo run --bin test_keyring

If all tests pass but auth still fails, enable verbose logging to see more details:

slack -v auth add --token xoxp-...

2. Use file-based storage as fallback:

If keyring access is problematic (CI environments, sandboxed apps, etc.), use file-based storage:

export SLACK_TOKEN_STORE_PATH=~/.slack-tokens.json
slack auth add --token xoxp-...

3. Platform-specific checks:

macOS:

  • Open Keychain Access and search for "slack-cli"
  • Verify the app has permission to access the keychain
  • Run: security find-generic-password -s slack-cli

Linux:

  • Ensure a Secret Service daemon is running (gnome-keyring, KWallet)
  • Check if you're in a headless/SSH environment (may need D-Bus session)

Windows:

  • Check Credential Manager for "slack-cli" entries

Rate Limiting

If you receive rate limit errors, the CLI will automatically retry with backoff. For bulk operations, consider:

  • Adding delays between requests
  • Using search instead of listing all messages
  • Batching operations appropriately

Token Errors

"invalid_auth" error:

  • Verify your token is valid and not expired
  • Check that the token has the required scopes
  • Try re-authenticating with slack auth add

"missing_scope" error:

  • Your token doesn't have permission for this operation
  • Use a token with more scopes, or try a different token type

Full Specification

For complete implementation details, see PLAN.md.

License

MIT License - see LICENSE for details.

Changelog

See CHANGELOG.md for release history.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages