Skip to content

Repository files navigation

slack — Slack CLI

A lightweight Slack CLI for quick workspace interaction from the terminal. Two implementations — TypeScript (bun-first, published to npm) and Rust (native binary, cargo install) — share one command surface and are verified byte-for-byte by tests/parity.sh.

Features

  • News — Activity feed showing recent mentions (to:me), grouped by day with human-readable timestamps
  • Messages — Browse recent messages across joined channels
  • Tail — Stream new messages from a channel in real time (like tail -f)
  • Search — Full-text search across the workspace
  • Send — Send messages to channels, DMs, or threads with a confirm-hash safety gate (prevents accidental sends); targeting a message permalink replies in that message's thread
  • Edit / Delete — Rewrite or remove a sent message, guarded by the same confirm-hash gate
  • React — Add or remove an emoji reaction — a lightweight ack that doesn't grow the thread
  • Dump — Bulk-export channel history as markdown

Output formatting

  • DM channels display as @DisplayName, public channels as #channel-name
  • Slack <@UID> mention tokens are resolved to display names
  • Slack <!date^...> markup is rendered as human-readable dates
  • Messages are grouped by day (Today / Yesterday / weekday)

Comparison with other Slack tools

slack-term focuses on everyday workspace interaction through shell commands: read, search, reply, and coordinate work from a terminal or an agent script. The alternatives below serve different workflows; links point to their upstream documentation.

Tool Main workflow How it compares with this project
slack-term (this project) Workspace messaging and automation TypeScript and Rust implementations; explicit user-token setup; message previews and confirm codes for send, edit, and delete; ask waits for answers and todo tracks tasks through reactions.
slkcli Slack commands for macOS users and agents Extracts credentials from the Slack desktop app for convenient onboarding. This project offers an explicit-token setup suitable for other platforms, at the cost of configuring an app and scopes.
Official Slack CLI Building, running, and deploying Slack apps Choose it for Slack app development. This project's commands focus on interacting with messages in an existing workspace.
Go slack-term Interactive terminal chat Provides a full-screen terminal client. This project uses individual shell commands, which fit scripts and quick queries. The two projects are unrelated despite sharing a name.
wee-slack Slack inside WeeChat Offers ongoing chat with threads, reactions, and synchronized read markers. Choose it if you already use WeeChat; this project runs as a standalone CLI.
slackcat Posting files and piped command output Focuses on sending stdin, files, and streaming logs to Slack. This project also covers reading, searching, and following conversations.
slackdump Archiving and exporting Slack data Offers dedicated archives, export formats, and a local viewer. This project's dump provides markdown history exports alongside everyday messaging commands.

Choose this project when you want scriptable conversations with a preview before sending. The confirmation step adds an extra invocation, and token setup requires the appropriate Slack scopes. Reactions are immediate and do not use the confirm gate. For a persistent chat UI or a dedicated archive, the specialized tools above may be a better fit.

Installation

TypeScript (npm, recommended)

One package, no native binaries, any platform with Node 18+.

npm install -g slack-term
# or: bun add -g slack-term  |  pnpm add -g slack-term

Note: Previously published as @snomiao/slack (now deprecated).

Rust (cargo)

cargo install --path rs

Both expose the same slack command.

Usage

# Activity feed (mentions directed to you)
slack news
slack news --limit 5

# Recent messages across joined channels
slack msgs

# Channel/DM history. Thread parents are marked `[+N replies]`, so a line with
# replies is distinguishable from one without — open it with `slack thread`.
# --json adds reply_count / reply_users_count / latest_reply.
slack read "#general"
slack read "#general" --unreplied   # only threads/posts whose last word isn't yours

# Search messages
slack search "deploy"
slack search "deploy" --count 50

# Send a message (two-step confirm — quote #channel)
slack send "#general" "Hello team"
# Prints who you're acting as (From: @handle (Uxxxx) — Workspace) + a destination
# preview + confirm code; rerun with --code=<code> to actually send. Every gated write
# (send/edit/delete/upload/schedule/channel create/drafts) shows that From: line and
# binds it into the code, so a profile switch mid-flow invalidates it instead of acting
# as the wrong account.
slack send "#general" "Hello team" --code=<code>

# send/edit/ask refuse ambiguous bare URLs such as https://example.com/path/内容.
# Put the URL on its own line or use <https://example.com/path/> (or <url|label>).
# Intentional Unicode URLs should also be wrapped. --allow-url-adjacent warns only;
# the normal confirmation code is still required.

# Reply in a thread — #chan:<thread_ts>, or just paste a message permalink
slack send "#general:1700000000.000100" "Replying in thread"
slack send "https://acme.slack.com/archives/C0123456789/p1700000000000100" "Replying in thread"

# @handle tokens are auto-converted to real <@USERID> mentions (on by default; also on `edit`).
# Resolves via users.list, then the channel's members so Slack Connect guests work;
# any handle that can't be resolved is left as plain text. The confirm preview shows
# the converted message before sending. Use --no-mentions to keep @text literal.
slack send "#general" "thanks @t.yamada19850101 and @alice"
slack send "#general" "ping @ops on call" --no-mentions   # leave @ops as plain text

# Edit or delete a sent message (same confirm-code gate)
slack edit "<permalink>" "fixed wording"
slack delete "<permalink>"

# React instead of replying for a simple ack — keeps the thread tidy (no confirm gate)
# 👀 seen  ✅ done  ⏳ working on it — target is the #chan:<ts> / permalink form
slack react "#general:1700000000.000100" white_check_mark
slack react "<permalink>" eyes --remove   # take a reaction back

# Ask a question with its choices pre-seeded as 1️⃣..🔟 reactions — answering is
# one tap on an existing pill, no emoji picker. Same two-step confirm gate as send.
slack ask "@bob" "本番に出してよい?" "出してよい" "待って"
# --wait blocks until answered and prints ONLY the answer on stdout, so it composes:
ANS=$(slack ask "@bob" "本番に出してよい?" "出してよい" "待って" --code=<code> --wait)
# exit 0 = answered, 2 = timed out (--timeout, default 3600s), 3 = transport failure,
# 4 = a reply matched several choices, 5 = a free-text reply that picked NO choice —
# the reply text is on stdout, it is not a decision, and the question stays open.
#
# The question must say WHO may answer — only their reaction/reply is taken as the
# answer, so a bystander can't decide it for them. `ask` refuses to post otherwise.
slack ask "#eng" "@alice この PR 出してよい?" "出す" "待つ"     # only alice's answer counts
slack ask "#eng" "@here 誰か見れる?" "見る" "あとで"            # anyone in #eng; real broadcast
# (In a 1:1 DM the other party counts automatically — no tag needed.)
#
# With no choices the question asks for a free-text reply and the reply is the answer.
# In a DM a plain reply counts; in a channel only reactions and thread replies do.
# Once answered, the question is edited to "✅ …回答済み > <answer>" and the unpressed
# seeds are removed, leaving the chosen pill visible.
#
# WITHOUT --wait, stdout is the command that collects the answer later:
RESUME=$(slack ask "@bob" "本番に出してよい?" "出してよい" "待って" --code=<code>)
#   -> slack ask --waitFor='https://acme.slack.com/archives/C00000001/p1700000000000100'
# Run it whenever you like: it re-reads the question from Slack, so nothing is
# stored locally and any machine holding the link can collect. Same stdout/exit
# contract as --wait, plus --timeout 0 = check once (exit 2 while still open, 5 with
# the reply on stdout if somebody wrote free text instead of choosing; re-wait past
# it with --after=<reply ts>),
# which is what a periodic monitor should use instead of parking on --wait.
eval "$RESUME --timeout 0" && echo answered
# A pressed pill is invisible to `slack tail` (it only sees `type: "message"`
# events and drops `message_changed`), so this is the only way to hear about an
# answer to a question you did not block on.

# Who sent what: every send/ask/poll/edit is logged locally with its sender —
# pid, cwd, git branch, agent CLI and session id — and the same attribution rides
# along as invisible Slack message metadata (event_type `slack_term_sent`).
slack sent                                     # newest first, sender on its own line
slack sent "deploy" --since 2h --kind ask      # substring of the text
slack sent --session 4f1c --cwd ~/ws/app --json
# Log: ~/.config/slack-cli/sent.sqlite (SLACK_TERM_SENT_DB overrides).
# Opt out of both with SLACK_TERM_ATTRIBUTION=off. Agent CLIs other than Claude
# Code are found by process name; set SLACK_TERM_AGENT_SESSION / _CLI / _PID to
# name the session explicitly.

# Task tracking on top of reactions — :pushpin: marks a message as a task,
# a second reaction carries its progress (see "todo" below)
slack todo ls                                  # my open tasks
slack todo ls --state untriaged --in "#general"
slack todo set "#general:1700000000.000100" doing
slack todo flag "<permalink>" blocked
slack todo doctor --in "#general"              # find messages stuck in two states

# Bulk export channel history
slack dump --days 7 --filter eng

# Stream new messages in real time (Ctrl-C to stop)
slack tail "#general"
slack tail "#general" --since=10m   # backfill last 10 minutes first
slack tail "#general" --thread=<ts> # follow a single thread
slack tail "#general" --me          # only messages that mention you
slack tail "@bob" --exit-on-message --timeout 30m   # wait for a reply, then exit

todo — task tracking on reactions

Tasks are just messages. A marker reaction (📌 :pushpin:) says "this is a task"; a progress reaction says where it stands; reason flags stack on top:

axis reaction meaning
progress 1 ✅ white_check_mark done
progress 2 🚫 no_entry_sign dropped
progress 3 👀 eyes doing
progress 4 ⏳ hourglass_flowing_sand pending
progress 5 (marker only) undefined / untriaged
flag ❗ exclamation alert
flag ❓ question needs discussion
flag 💬 speech_balloon waiting — the other party in this conversation owes a reply
flag 🔒 lock blocked — stuck on something outside this conversation

The invariant is "at least one progress reaction, readers collapse by priority" — not "exactly one". A message carrying both ✅ and 👀 reads as done.

Whose ball is it? There are only three answers, and pending ⏳ already means mine. The other two are the flags:

  • 💬 waiting — I've done my part; someone in this conversation owes the next move (I asked a question, I'm waiting on a review, I sent it and need a yes/no).
  • 🔒 blocked — the hold-up is outside this conversation: another task, a third party, an external dependency, a deploy window.

They stack with each other and with ❗/❓; --state stuck matches a task with any of them.

# Two listings, split by whose reactions count — the distinction is the command,
# not a flag, so "everyone's list" and "my list" can't be confused at a glance.
slack todo ls                              # EVERYONE's tasks (has:)
slack mytodo ls                            # only tasks YOU reacted to (hasmy:)

slack todo ls --state untriaged            # marked, no progress reaction yet
slack todo ls --state doing --in "#eng"    # scoped to a channel
slack mytodo ls --state stuck              # my unfinished tasks carrying a reason flag
slack todo ls --state stuck --from "@alice" # …and it was alice who wrote it
slack todo ls --from me                    # tasks on my own messages (any reactor)
slack todo ls --mine                       # same as `slack mytodo ls`
slack todo set "#eng:1700000000.000100" doing
slack todo flag "<permalink>" waiting          # their ball now
slack todo flag "<permalink>" needs-discussion
slack todo flag "<permalink>" blocked --remove
slack todo doctor --in "#eng"              # report messages in two progress states
slack todo doctor --in "#eng" --fix        # keep the highest-priority one

Notes:

  • todo ls vs mytodo ls — todo ls matches anyone's reactions (has:), mytodo ls only your own (hasmy:). Everything else about them is identical. Both print a From: @handle (Uxxxx) — Workspace line first, because hasmy: resolves against whichever account the token belongs to — "my tasks" means nothing until you know who my is, and a stale profile otherwise lists someone else's list without saying so. (todo ls --mine is a one-off shortcut to the narrow view.)

  • Both are read-only and run exactly one search.messages call — priority is expressed as negated has:/hasmy: terms in the query, not as multiple searches (search.messages is Tier 2, ~20 req/min). Slack's search index lags a little behind reactions.add, so a just-set task may take a moment to appear.

  • todo set adds the new reaction before removing the old ones, and removes serially. The reverse order would leave a window with no progress reaction at all — a crash or a 429 there would drop the task out of every query with nothing left pointing at it. Worst case here is a message with two progress reactions, which reads correctly and is repairable with todo doctor --fix.

  • --from <@user|me> maps to search's from: modifier (a missing @ is supplied; me passes through as-is). Reactions carry no reference to whom a task waits on, but the sender is usually that person — so filtering by author answers most of the question for free. It composes with --in in the same single search.

  • Not implemented, deliberately: encoding the actual reference ("blocked on @alice" / "blocked on task X") as a machine-readable token in a thread reply. Measured: Slack's full-text index splits on punctuation, so a quoted search for "blocked-on" returns 0 hits, and "1.91" matches 1.91.0. A token like todo:v1 blocked-on=@alice is therefore unsearchable. Any future attempt needs a single alphanumeric marker word (e.g. tdblock) plus a bare @mention.

  • todo doctor pages history sequentially and scans at most --limit messages (default 1000); if it hits the limit it says so rather than silently stopping.

  • Emoji are configurable in ~/.config/slack-cli/todo.json (same directory as profiles.json); anything omitted falls back to the defaults above:

    { "marker": "round_pushpin", "progress": { "doing": "construction" } }
  • Channel-name→ID and user-ID→handle lookups are cached in ~/.config/slack-cli/cache.json for 1 hour, namespaced by workspace (team_id) since both are workspace-scoped; --no-cache bypasses it. Reaction state and search results are never cached — they are exactly the values that change, and Slack's search index already lags. A corrupt or unwritable cache is ignored and the command runs uncached.

tail — real-time message stream

slack tail polls a channel every 3 seconds (configurable via --interval) and prints new messages as they arrive, in the same [ts] @handle: text format as the read command.

slack tail "#general"               # follow new messages from now
slack tail "#general" --since=30m   # backfill 30 minutes, then stream
slack tail "#general" --thread=1700000000.000100   # one thread only
slack tail "#general" --me          # only messages mentioning you

For automation, --exit-on-message stops as soon as the first message from someone else arrives (your own posts are ignored), and --timeout <dur> (e.g. 30m, 2h) auto-stops after the deadline with exit code 0. Together they make a "wait for a reply, then act" primitive that won't hang:

slack tail "@alice" --exit-on-message --timeout 30m --interval 15000

Note: Cross-channel mention streaming (--me without a target) is not yet supported — a target channel is required.

Configuration

Requires a Slack user token (xoxp-...) with the following scopes:

  • search:read — for search and news
  • channels:history, groups:history, im:history, mpim:history — for message history
  • channels:read, groups:read, im:read, mpim:read — for channel listing
  • users:read — for resolving display names
  • chat:write — for sending messages
  • reactions:write — for react and for ask (seeding / clearing the choice pills)

ask needs no reactions:read: it reads the answers back from conversations.history, which returns each message's reactions (with the user list) under the *:history scopes above. reactions.get would need reactions:read, so it is deliberately not used.

Add each scope to the token type the CLI actually uses. The CLI defaults to the user token (xoxp-...), so reactions:write must be under User Token Scopes. Adding it only to Bot Token Scopes makes slack doctor (which checks the bot token) look green while slack react still fails with missing_scope — the bot scope only helps --as-bot/bot-token usage. After changing scopes, reinstall the app to the workspace for it to take effect.

Set the token via environment variable:

export SLACK_MCP_XOXP_TOKEN=xoxp-...

Or place it in ~/.config/slack-cli/.env or a local .env file.

See SKILL.md for a full token-acquisition walkthrough.

Ubuntu desktop session

Sign in to Slack in Chrome or Slack Desktop. --from-chrome reads a browser xoxc- token and cookie from the same Chrome profile; desktop import reads xoxc- tokens from native, Snap, or Flatpak data directories. Browser profiles contain sensitive cookies, so the CLI asks Read local browser profiles and Slack session cookies? [y/N] before scanning. Enter y to allow a scan; the default is no.

slack auth login --from-desktop                 # desktop token only
slack auth login --from-chrome                  # Chrome token + Chrome cookie
slack auth login --from-firefox                 # desktop token + Firefox cookie
slack auth login --from-all                     # desktop and browser sources
slack auth login --from-chrome --yes            # bypass the browser-read prompt
slack auth tokens                              # print active credentials in .env format
slack auth save --envfile=./.env.local          # export active token + cookie

--from-all attaches a cookie only when exactly one browser session is found. If several are found, the desktop token is saved without a cookie; use slack auth chrome -w <name> or slack auth firefox -w <name> to select the matching profile. These commands also ask before reading browser profiles, and accept --yes for scripts. Select the saved workspace with slack auth use -g <name>. auth tokens prints the active SLACK_TOKEN, optional SLACK_COOKIE, and optional SLACK_BOT_TOKEN to stdout; treat its output as secret. auth save requires a cookie, writes SLACK_TOKEN and SLACK_COOKIE, and makes the env file owner-readable only on Unix. --workspace <name> selects a specific profile.

Chrome supports Linux v10 cookies and v11 cookies when secret-tool can read the unlocked GNOME keyring. Other Linux keyring backends are not yet supported. Firefox discovery covers native, Snap, and Flatpak profiles. If browser session access is unavailable, use slack auth token to add a user token from a Slack app. Treat desktop tokens and browser cookies as credentials; keep profile files and local env files private.

Development

# TypeScript
bun install
bun run dev -- news --limit 3      # run straight from source
bun run typecheck
bun run build                      # produces dist/cli.js

# Rust
cargo run --manifest-path rs/Cargo.toml --release --bin slack -- news --limit 3

# Parity test (requires a token — compares Rust and TS stdout)
bun run test:parity

Dependencies

TypeScript impl — zero runtime deps; uses built-in fetch, node:crypto, node:util argument parsing.

Rust impl

  • clap — CLI argument parsing
  • reqwest — HTTP client for Slack Web API
  • tokio — async runtime
  • chrono — date/time formatting
  • ring — SHA-256 for confirm hashes

Release notes

v0.x — 2026-05-15: slack tail

New tail subcommand streams channel messages in real time using poll-based delivery (3-second interval). Supports --since=<duration> for backfill, --thread=<ts> to follow a single thread, and --me to filter for messages that mention you. Uses conversations.history?oldest=<ts> as a cursor so already-seen messages are never re-printed, even across reconnects.

Ecosystem

This CLI is the human-comms surface of the agent-yes fleet: agent-yes runs, watches and delegates to AI coding agents, and slack is how they read and answer the humans they work with. Neither depends on the other — they share a stance rather than a library: file-based state over daemons, one command surface across two runtimes, gates on the irreversible and warnings on the merely unwise, and no real operational data in a public repo.

A write-up of the design decisions behind the confirm gate lives on the agent-yes lab: A confirm code that goes stale.

Related / prior art

  • slkcli by @therohitdas — a macOS-only Node CLI that auto-extracts xoxc- session tokens from the Slack desktop app. Different tradeoffs (zero-config on macOS vs. our cross-platform explicit-token approach). See docs/comparison-slkcli.md for a full UX side-by-side.
  • docs/ecosystem.md — survey of other terminal Slack tools (official, slack-term, wee-slack, slackcat, slackdump, …).

License

MIT

Print the active token (TypeScript CLI)

slack auth token prints the resolved token followed by a newline, like gh auth token. Select a saved workspace with slack auth token --workspace acme. It uses the same environment/profile precedence as other commands and makes no API request. Diagnostics go to stderr so stdout can be used in scripts.

Use slack auth login for interactive setup. Existing imports with slack auth token --token <token> --name acme continue to work.

slack auth env prints the active workspace credentials as quoted dotenv assignments (SLACK_TOKEN, plus SLACK_COOKIE for desktop tokens when available). Use slack auth env --workspace acme to select a specific saved workspace. It follows the same credential precedence, makes no API request, and prints only assignments to stdout. Each export contains one workspace, avoiding duplicate keys.

About

Slack CLI for power users

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages