A comprehensive Rust CLI tool for Slack, designed for AI agents and automation.
- 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 apicalls 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
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-cliThe skill lives in skills/slack/ and covers all commands with usage examples.
curl -fsSL https://raw.githubusercontent.com/TeamCadenceAI/slack-cli/main/install.sh | shInstalls 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.
git clone https://github.com/TeamCadenceAI/slack-cli
cd slack-cli
cargo build --release
# binary at target/release/slackOr via cargo install:
cargo install --git https://github.com/TeamCadenceAI/slack-cli# 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# 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"# 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-helpOAuth 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:writeThe 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.
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 listRun 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.
# 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# 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" --resolveMember 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.
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.
slack channels unread
slack --plain channels unreadThis 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.
# 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"# 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.
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~ |
|
# Heading |
*Heading* |
bold line |
[text](url) |
<url|text> |
linked text |
 |
<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.
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.123456Identifiers 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.
# 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.
# 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.csvEmail 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.
# 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 2Uploads 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.
# 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# 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.
# List workspace custom emoji
slack emoji list
slack --plain emoji listemoji 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.
# 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" Bk123456789Listing 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.
# 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# 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 Rm123456789Escape 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 GETFlags
| 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
GETandPOSTare supported.GETsends parameters as URL query parameters;POSTsends them form-encoded (application/x-www-form-urlencoded), which is what the Slack Web API expects. - There is no raw JSON request body:
--inputloads a JSON parameter object which is then form-encoded like-f/-Ffields. 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 fullhttps://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_URLstill 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/limityourself and followresponse_metadata.next_cursor. - Not supported by
slack api: custom headers, raw-body file uploads, name→ID resolution, jq filtering, or Edge API endpoints. Useslack files uploadfor 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.
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 --plainslack api is JSON-only and rejects --plain.
--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| 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) |
| Prefix | Type | Use Case |
|---|---|---|
xoxp- |
User OAuth | Full user access |
xoxb- |
Bot OAuth | Bot access (no search) |
xoxc- |
Browser | Requires xoxd cookie |
| 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}# Build
cargo build
# Run tests
cargo test
# Run with verbose output
cargo run -- -v channels list
# Format code
cargo fmt
# Lint
cargo clippyQ: 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.
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_keyringIf 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
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
"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
For complete implementation details, see PLAN.md.
MIT License - see LICENSE for details.
See CHANGELOG.md for release history.