Project

slk

0.0
The project is in a healthy, maintained state
Manage your Slack status, presence, DND, read messages, and more from the terminal. Pure Ruby, no dependencies.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies
 Project Readme

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 slk

Requires 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.portable

Configuration is stored in %APPDATA%\slk\ on Windows.

Setup

Run the setup wizard:

slk config setup

You'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 status

Getting 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 cleared

Presence

slk presence              # Show current presence
slk presence away         # Set to away
slk presence active       # Set to active

Do 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 DND

Messages

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 JSON

Threads

slk thread <url>                # View a thread
slk thread subscribe <url>      # Follow a thread without replying
slk thread unsubscribe <url>    # Stop following it

Subscribing 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 only

Displays 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 too

Unread 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 read

Presets

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 command

Built-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 workspace

Cache Management

slk cache status          # Show cache status
slk cache populate        # Pre-populate user cache
slk cache clear           # Clear all caches

Huddles

slk huddles            # Active huddles on the primary workspace
slk huddles --all      # Every workspace
slk huddles --json     # Call id, channel, and participants

Slack 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 output
acme: 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.csv

Progress 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 workspaces

Token Encryption

Optionally encrypt your tokens with age using an SSH key:

slk config set ssh_key ~/.ssh/id_ed25519

Tokens 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.json or tokens.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 test

Releasing

  1. Update version in lib/slk/version.rb
  2. Update CHANGELOG.md: add a ## [X.Y.Z] - YYYY-MM-DD heading under ## [Unreleased] so the pending entries move into the release
  3. Run bundle install so Gemfile.lock picks up the new version
  4. Verify: bundle exec rake test && bundle exec rubocop
  5. 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
  6. 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
  7. Publish to RubyGems. fnox exec supplies GEM_HOST_API_KEY from fnox.toml (a stale key in ~/.gem/credentials gets "Access Denied"); MFA is required, and GEM_HOST_OTP_CODE skips the prompt:
    fnox exec -- sh -c 'GEM_HOST_OTP_CODE="$(op item get Rubygems --otp)" gem push pkg/slk-X.Y.Z.gem'
  8. 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.