slk - Slack CLI
A command-line interface for Slack. Manage your status, presence, DND, read messages, and more from the terminal.
Pure Ruby. No dependencies.
Installation
gem install slkRequires Ruby 3.2+.
Windows
# Install Ruby (if needed) via RubyInstaller or Chocolatey
winget install RubyInstallerTeam.Ruby.3.3
# or: choco install ruby
# Install slk
gem install slk
# (Optional) Install age for encrypted token storage
choco install age.portableConfiguration is stored in %APPDATA%\slk\ on Windows.
Setup
Run the setup wizard:
slk config setupYou'll need a Slack token. Get one from:
- User token (xoxp-): https://api.slack.com/apps → OAuth & Permissions
- Bot token (xoxb-): Create a Slack App with bot scopes
- Session token (xoxc-): Extract from browser (requires cookie too)
Usage
Status
slk status # Status, presence, DND and what's queued next
slk status --brief # Status text only (one call per workspace)
slk status --json # Machine-readable, for scripts and statuslines
slk status "Working from home" :house: # Set status with emoji
slk status "In a meeting" :calendar: 1h # Set status for 1 hour
slk status clear # Clear statusGetting the status also answers the two questions it usually raises — can anyone reach me, and is this about to change on its own:
$ slk status
work
:computer: Working [away] [dnd until 3:00pm]
Scheduled:
:paw_prints: Vet Appt (Mon Aug 3 1:30pm -> 3:30pm)
side-project
(no status set)
Only the exceptional states are labelled: an active, notifiable workspace says
nothing, so the one that differs stands out. [dnd until ...] covers both a
manual snooze and the configured DND hours — from the outside they are the same
thing — and slk dnd breaks out which.
Each of those is a separate call, so slk status costs up to four per
workspace. --no-scheduled drops the schedule lookup (the one internal
endpoint), --brief drops all three, and a lookup that fails warns and leaves
that part out rather than failing the command.
JSON output
slk status --json (and slk status scheduled --json) prints one entry per
workspace — always an array, even for a single workspace, so a script that did
not pass -w can still parse it. null means not checked (skipped by a flag,
or the lookup failed), which is not the same as checked-and-empty: a statusline
that read a failed DND lookup as "DND off" would say the opposite of the truth.
Every timestamp appears twice, as Slack's epoch and as ISO 8601:
# "away, quiet until 3:00pm"
slk status -w work --json --no-scheduled |
jq -r '.[0] | "\(.presence.presence)\(if .dnd.active then ", quiet until " + (.dnd.until | strflocaltime("%-I:%M%p")) else "" end)"'Statuses can also be scheduled to turn on later (Slack allows up to 5 at a time):
slk status schedule "Vet Appt" :paw_prints: 1:30p-3:30p # Bare times; rolls to tomorrow if past
slk status schedule "OOO" :palm_tree: 2026-08-04 9:00-17:00 # Explicit date
slk status schedule "Heads down" :no_bell: 11p-1a --with-dnd # Overnight, pausing notifications
slk status scheduled # List pending (all workspaces)
slk status unschedule CS0BMQDDGWTU # Cancel one (finds the owning workspace)A single am/pm carries across the range, so 1-3p is 1pm to 3pm — unless that
would invert it, leaving 9-5p as 9am to 5pm. Anything the am/pm cannot settle
is read as a 24-hour time, which is usually right but occasionally means the
opposite of what you typed: 9-5 and 9a-5 both come out as 9am until 5am the
next morning. Windows that land that way — guessed, crossing midnight, and over
12 hours — are rejected rather than scheduled, so add the second am/pm (9a-5p)
or use 24-hour times (9:00-17:00).
Overnight windows are fine when they say so. A meridiem on both sides (11p-1a,
8p-9a) or a 24-hour time on either side (20:00-09:00, 20:00-6) settles the
range, and a pm start is enough on its own: 9p-5 is 9pm to 5am.
A start-end range writes the date once and the end can only reach the next
day, so multi-day windows use --start / --end instead. Each takes
[YYYY-MM-DD ]TIME, and omitting --end schedules a status with no expiry:
slk status schedule "OOO" :palm_tree: --start "2026-08-12 8a" --end "2026-08-14 5p"
slk status schedule "Heads down" :no_bell: --start 2p # Stays until clearedPresence
slk presence # Show current presence
slk presence away # Set to away
slk presence active # Set to activeDo Not Disturb
slk dnd # Show DND status
slk dnd 1h # Enable DND for 1 hour
slk dnd on 30m # Enable DND for 30 minutes
slk dnd off # Disable DNDMessages
slk messages general # Read channel messages
slk messages @username # Read DM with user
slk messages general -n 50 # Show 50 messages
slk messages general --json # Output as JSONThreads
slk thread <url> # View a thread
slk thread subscribe <url> # Follow a thread without replying
slk thread unsubscribe <url> # Stop following itSubscribing treats existing replies as read, so only new replies show up under Threads in slk unread (and in Slack's own Threads view). Requires a session (xoxc) token.
Activity
slk activity # Show recent activity feed
slk activity -n 50 # Show 50 items
slk activity -m # Show message previews
slk activity --reactions # Filter: reactions only
slk activity --mentions # Filter: mentions only
slk activity --threads # Filter: thread replies onlyDisplays your recent activity feed including:
- Reactions to your messages
- Mentions (@user, @channel, @here, etc.)
- Thread replies
- Bot messages (reminders, notifications)
Use --show-messages (or -m) to preview the actual message content for each activity.
Unread
slk unread # Show unread counts
slk unread clear # Mark all as read
slk unread clear general # Mark channel as read
slk unread clear "#ifams review" # Named group DMs work by name tooUnread DMs, group DMs, channels, and threads are all listed. A named group DM shows under its name (#ifams review); others show the people in them.
Catchup (Interactive Triage)
slk catchup # Interactively review unread channels
slk catchup --batch # Non-interactive, mark all as readPresets
slk preset list # List all presets
slk preset meeting # Apply preset
slk preset add # Add new preset (interactive)
slk meeting # Shortcut: use preset name as commandBuilt-in presets: meeting, lunch, focus, brb, clear
Workspaces
slk workspaces list # List configured workspaces
slk workspaces add # Add a workspace
slk workspaces primary # Show/set primary workspaceCache Management
slk cache status # Show cache status
slk cache populate # Pre-populate user cache
slk cache clear # Clear all cachesHuddles
slk huddles # Active huddles on the primary workspace
slk huddles --all # Every workspace
slk huddles --json # Call id, channel, and participantsSlack has no huddle list. slk huddles reads huddle_state from users.list
and groups people who share a call id. A huddle with no channel is one Slack
did not attach to a conversation. People outside the workspace, including some
Slack Connect guests, are not in the roster and will not appear. The roster is
fetched live and is not cached. --all scans every workspace at the same time.
One connection can only read one roster, so each workspace gets its own.
People who are not in a huddle are dropped as each page arrives. slk who
shows the same state on the profile card, read live so the hour-long profile
cache cannot leave someone in a huddle they have already left.
Deactivations
slk deactivations # 25 most recent departures
slk deactivations 90d # Everyone who left in the last 90 days
slk deactivations 2026-01-01 -n 0 # All departures this year
slk deactivations --chart # Departures per month
slk deactivations --tenure # Add how long each person stayed
slk deactivations 1y --csv # Spreadsheet export of a year of departures
slk deactivations --grep engineer # Filter by name, handle, title, email, or ID
slk deactivations --bots # Include deactivated bots and app users
slk deactivations --json # Machine-readable outputacme: 16 deactivated since 30d (664 active accounts)
360 full members · 120 multi-channel guests · 184 single-channel guests
2026-09-14 Dana Whitfield Platform Support
2026-09-11 Priya Raghunathan UX Researcher
2026-09-09 Sam Okonkwo Senior Software Engineer
roster cached 5m ago; --refresh to update
Dates come from each account's updated field — the last change Slack recorded
for that user. For a deactivated account that change is almost always the
deactivation, but an admin editing a departed profile afterwards moves the date
forward, so treat it as "last touched" rather than a payroll record.
The active-account count excludes bots and deactivated accounts; full members and
both types of Slack guests are shown separately. It is not an employee headcount.
The same breakdown is available in --json as active_full_members,
active_multi_channel_guests, and active_single_channel_guests (alongside the
existing active_members total).
The roster costs one API call per 1000 members, so the result is cached for six
hours; --refresh re-fetches it.
Tenure
--tenure adds how long each person was here, taken from the workspace's
"Start Date" profile field:
acme: 3 deactivated since 30d (664 active accounts)
360 full members · 120 multi-channel guests · 184 single-channel guests
2026-09-14 Dana Whitfield 1mo Platform Support
2026-09-11 Priya Raghunathan 2y 7mo UX Researcher
2026-09-09 Sam Okonkwo 4y 10mo Senior Software Engineer
This one is slow the first time. Start dates are not in the roster — they cost
one users.profile.get per person, and Slack rate-limits that endpoint to
roughly eight calls a minute, so a screenful takes about three minutes. The
command says so before it starts, only looks up the rows it is about to show,
and caches each answer the moment it arrives: interrupting it keeps the work
already paid for, and the second run is instant.
Blank means nobody filled the field in. Months are whole months, so somebody who started on the 20th and left on the 3rd has not completed that month.
CSV
--csv writes every match — not just the screenful -n would show, since a
truncated export is a wrong answer that looks like a right one. Combine it with
--tenure for started_on, tenure_months and a readable tenure column:
slk deactivations 1y --tenure --csv > departures.csvProgress and warnings go to stderr, so the redirect above captures only data.
Global Options
-w, --workspace NAME # Use specific workspace
--all # Apply to all workspaces
-v, --verbose # Show debug output
-q, --quiet # Suppress output
--json # Output as JSON (where supported)Multi-Workspace
Configure multiple workspaces and switch between them:
slk workspaces add # Add another workspace
slk status -w work # Check status on 'work' workspace
slk status "OOO" --all # Set status on all workspacesToken Encryption
Optionally encrypt your tokens with age using an SSH key:
slk config set ssh_key ~/.ssh/id_ed25519Tokens will be stored encrypted in ~/.config/slk/tokens.age.
Configuration
Files are stored in XDG-compliant locations (or %APPDATA%/%LOCALAPPDATA% on Windows):
-
Config:
~/.config/slk/(Windows:%APPDATA%\slk\)-
config.json- Settings -
tokens.jsonortokens.age- Workspace tokens -
presets.json- Status presets
-
-
Cache:
~/.cache/slk/(Windows:%LOCALAPPDATA%\slk\)-
users-{workspace}.json- User cache -
channels-{workspace}.json- Channel cache
-
Development
# Clone the repo
git clone https://github.com/ericboehs/slk.git
cd slk
# Run from source
ruby -Ilib bin/slk --version
# Run tests
rake testReleasing
- Update version in
lib/slk/version.rb - Update
CHANGELOG.md: add a## [X.Y.Z] - YYYY-MM-DDheading under## [Unreleased]so the pending entries move into the release - Run
bundle installsoGemfile.lockpicks up the new version - Verify:
bundle exec rake test && bundle exec rubocop - Commit version, changelog, and lockfile together, then push and wait for CI:
git commit -am "chore(release): prepare vX.Y.Z" git push origin master gh run watch --exit-status - Build, tag, and push the tag:
bundle exec rake build git tag -a vX.Y.Z -m "Version X.Y.Z" git push origin vX.Y.Z
- Publish to RubyGems.
fnox execsuppliesGEM_HOST_API_KEYfromfnox.toml(a stale key in~/.gem/credentialsgets "Access Denied"); MFA is required, andGEM_HOST_OTP_CODEskips the prompt:fnox exec -- sh -c 'GEM_HOST_OTP_CODE="$(op item get Rubygems --otp)" gem push pkg/slk-X.Y.Z.gem'
- Create the GitHub Release with the changelog entry as notes:
awk '/^## \[X.Y.Z\]/{f=1;next} /^## \[/{f=0} f' CHANGELOG.md | sed '/./,$!d' > /tmp/notes.md gh release create vX.Y.Z --title vX.Y.Z --notes-file /tmp/notes.md
License
MIT License. See LICENSE for details.