Slack in your terminal, as yourself: a full TUI plus scriptable commands.
One Rust binary called slack, ~10 ms startup.
Unofficial. It logs in with your browser session, not a Slack app, so it needs no admin approval. Check that your workspace rules allow this before you use it.
macOS only for now (keychain, open, pbcopy).
Vibe Coded for personal usage but use at your own risks.
cargo install --git https://github.com/vmeyet/slack-tuiThat is all: the slack command is now on your path.
| You need | Get it |
|---|---|
| macOS | |
Rust 1.90+ (cargo) |
curl -sSf https://sh.rustup.rs | sh |
| Brave, Chrome, Chromium or Edge | Only used once, to log in |
Optional shell completions: slack completions zsh > ~/.zfunc/_slack (also bash, fish).
Update with slack update (a no-op when you already run the latest commit, -f to rebuild anyway); remove with cargo uninstall slack.
slack --version prints the version and the commit it was built from.
slack login acme # opens acme.slack.com in your browser; log in there, done
slack whoami
slack logout # forgets the keychain entry, config and cache| Option | Use |
|---|---|
--browser chrome |
Pick the browser: brave, chrome, chromium, edge, or a path. |
--profile <dir> |
Reuse a browser profile that is already logged in. |
--cookie - |
Skip the browser and paste a d cookie on stdin. |
slack # or slack tuiThree panes: channels, messages, thread.
Messages, edits, deletions and reactions arrive live; other channels light up with ●.
Whoever is typing in the open conversation shows under the last message.
| Key | Action |
|---|---|
j k / arrows |
Move |
enter / l |
Open the channel or the thread |
h / esc |
Go back |
r / t |
Reply / reply in thread |
e |
Rewrite your own message |
ctrl-e |
Write the message in $EDITOR ($VISUAL first, then vi); save to send, quit empty to cancel |
+ |
React: 1–8 from the strip (on the message, then your most used), / to search by name; pick one of yours again to take it off |
o / u |
Open the message in Slack / open its first link |
y |
Copy the permalink |
/ |
Filter channels |
s |
Search |
ctrl-k |
Fuzzy jump to a channel, a person or a followed thread; start with > to search Slack |
i |
Inbox |
f |
Firehose: every conversation as one live wall; + reacts to the selected line |
p |
Promises: follow-ups you said you would do and have not closed (needs triage) |
z |
Reading mode: one centered column, nothing else |
: |
Command line |
? |
Every key |
⌘k also works on terminals that forward it (Ghostty, Kitty, WezTerm, iTerm2 with the kitty keyboard protocol).
Command line.
:join #ops, :leave, :go @bob, :msg @bob on my way, :compose, :react rocket, :search deploy failed, :export md, :read, :snooze 1h, :set theme=nord, :help, :quit.
Tab completes verbs, channels, people and emoji; ↑ recalls history.
Inbox.
Unread DMs, mentions and thread replies in one list.
→ marks read, ← snoozes (1h, 3h, tomorrow, monday), r replies in place, enter opens the conversation, a clears everything.
Triage (opt-in).
With [typesafe] enabled = true, TypeSafe's Jev model ranks the inbox (↩ needs reply first, then ‼ urgent and ! soon) and tags each firehose line as incident, question-for-me, fyi or noise.
The firehose hides noise (n shows it) and paints incidents red.
Lines show at once; the tag lands when Jev answers.
slack promises (or p) asks Jev which of your own messages promised a follow-up, and whether a later reply of yours in the same thread closed it.
If Jev cannot answer, you get one ⚠ typesafe unavailable notice and the plain views.
No live feed?
If the workspace refuses the RTM websocket, the open conversation is polled every 10 seconds (↻ in the status bar).
Every command takes --json (for scripts and agents) and -w <workspace>.
slack send '#general' "Deploy **v2.3** is out, see [notes](https://…) cc @bob"
slack send @bob - < message.md # markdown from stdin
slack send '#ops' --blocks payload.json # Block Kit
slack send <permalink> "replying in that thread"
slack send '#ops' "…" --dry-run # print the payload, send nothing
slack messages '#general' -n 50 --since 2d --threads
slack messages '#general' --follow # tail live
slack thread <permalink>
slack search "deploy failed" --in ops --from bob --after 2026-09-01
slack react <permalink> :tada:
slack channels [filter] [--all]
slack users [filter]
slack inbox # a list when piped or with --list
slack firehose -H 'error|failed' -H prod # live ticker, -H highlights a regex
slack firehose --hide-noise # drop what Jev tags as noise
slack promises --since 2w [--all] # follow-ups you promised, still open (alias: todo)
slack api conversations.info channel=C0123 # any Web API methodMessages are markdown by default and are sent as Block Kit rich_text:
**bold**, _italic_, ~~strike~~, `code`, fenced code, # heading, lists, > quotes, ---, [label](url), @user, #channel, :emoji:.
Use --raw to send Slack mrkdwn untouched.
~/.config/slack-tui/config.toml, every key optional:
[tui]
theme = "catppuccin" # dracula, catppuccin, catppuccin-latte, rosepine, rosepine-dawn, nord, tokyonight, monokai
highlight = "#2a2a2a" # fill under the selected row; by default only the ▎ bar marks it
images = true # inline thumbnails on Kitty, Ghostty, WezTerm and iTerm2
[links]
show_url = false # true prints `label (url)`; false keeps a clickable label
[firehose]
highlight = ["prod", "error|failed", "@bob"] # case-insensitive regexes
[typesafe]
enabled = false # true sends unread, live and your own message text to api.typesafe.ai to rank, tag and find promisesThe TypeSafe key comes from TYPESAFE_API_KEY, else the keychain service typesafe.
TYPESAFE_DISABLED=1 or ~/.agents/typesafe/disabled turns it off without touching the config.
:set theme=…, :set highlight=… and :set images=on|off write this file from the TUI.
slack loginstarts a throwaway browser profile and drives it over the DevTools protocol.- Once you are logged in, it reads the web client's session: the
xoxctoken and thedcookie. - It stores both in the macOS login keychain (service
slack-tui), closes the browser and deletes the profile.
The session never touches disk unencrypted.
It acts as you, with everything you can do in Slack: treat it like a password.
The keychain is accessed through /usr/bin/security, so rebuilding the binary never triggers an "allow access" prompt.
Unread badges, the inbox and sidebar sections use undocumented web-client endpoints (client.counts, subscriptions.thread.*, users.channelSections.list).
Slack can change them without notice.
cargo test # unit + end-to-end against a mock Slack API
cargo test -- --ignored # also the real keychain round trip
SLACK_TUI_DEBUG=1 slack login … # trace the browser capture| Variable | Effect |
|---|---|
SLACK_TOKEN, SLACK_COOKIE |
Bypass the keychain |
SLACK_WORKSPACE |
Default workspace |
SLACK_TUI_API_URL |
Point at another API host (tests use a mock) |
SLACK_TUI_CONFIG_DIR, SLACK_TUI_CACHE_DIR, SLACK_TUI_STATE_DIR |
Move the config, the cache and the inbox state |
VISUAL, EDITOR |
What ctrl-e opens to write a message, arguments allowed (EDITOR="code -w") |
NO_COLOR, COLUMNS |
Plain output, fixed width |
The app was called slack-cli before.
Its old folders and keychain entries move to slack-tui the first time they are used, and SLACK_CLI_* variables still work.
0 comments
log in to comment.