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.
- 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
- 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)
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.
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-termNote: Previously published as
@snomiao/slack(now deprecated).
cargo install --path rsBoth expose the same slack command.
# 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 exitTasks 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 oneNotes:
-
todo lsvsmytodo ls—todo lsmatches anyone's reactions (has:),mytodo lsonly your own (hasmy:). Everything else about them is identical. Both print aFrom: @handle (Uxxxx) — Workspaceline first, becausehasmy: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 --mineis a one-off shortcut to the narrow view.) -
Both are read-only and run exactly one
search.messagescall — priority is expressed as negatedhas:/hasmy:terms in the query, not as multiple searches (search.messagesis Tier 2, ~20 req/min). Slack's search index lags a little behindreactions.add, so a just-set task may take a moment to appear. -
todo setadds 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 withtodo doctor --fix. -
--from <@user|me>maps to search'sfrom:modifier (a missing@is supplied;mepasses 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--inin 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"matches1.91.0. A token liketodo:v1 blocked-on=@aliceis therefore unsearchable. Any future attempt needs a single alphanumeric marker word (e.g.tdblock) plus a bare@mention. -
todo doctorpages history sequentially and scans at most--limitmessages (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 asprofiles.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.jsonfor 1 hour, namespaced by workspace (team_id) since both are workspace-scoped;--no-cachebypasses 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.
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 youFor 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 15000Note: Cross-channel mention streaming (--me without a target) is not yet
supported — a target channel is required.
Requires a Slack user token (xoxp-...) with the following scopes:
search:read— for search and newschannels:history,groups:history,im:history,mpim:history— for message historychannels:read,groups:read,im:read,mpim:read— for channel listingusers:read— for resolving display nameschat:write— for sending messagesreactions:write— forreactand forask(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-...), soreactions:writemust be under User Token Scopes. Adding it only to Bot Token Scopes makesslack doctor(which checks the bot token) look green whileslack reactstill fails withmissing_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.
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.
# 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:parityTypeScript 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
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.
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.
slkcliby @therohitdas — a macOS-only Node CLI that auto-extractsxoxc-session tokens from the Slack desktop app. Different tradeoffs (zero-config on macOS vs. our cross-platform explicit-token approach). Seedocs/comparison-slkcli.mdfor a full UX side-by-side.docs/ecosystem.md— survey of other terminal Slack tools (official,slack-term,wee-slack,slackcat,slackdump, …).
MIT
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.