A macOS + Linux Slack CLI that auto-authenticates from your already-signed-in Slack desktop app. No OAuth, no app install, no token entry.
$ spy auth
authenticated as Tom Harris @ Acme
workspace: acme (T024D6SRW80)
user id: U01ABCDEF
url: https://acme.slack.com/
$ spy ch
# general (412 members) C012ABCDEF
# random (388 members) C013BCDEFG
🔒 leadership (12 members) C014CDEFGH
...
$ spy r general 5
[2026-05-25 09:14:22] Anjali Patel:
Coffee chat 10:30?
[2026-05-25 09:18:51] Marcus Lee [3 replies]:
Deploy looks green — anyone object to rolling forward?
...
- macOS or Linux (the auth flow reads the Slack desktop app's local data)
- Slack desktop, signed in to at least one workspace:
- macOS — App Store or direct-download build
- Linux — native (
.deb/.rpm), Snap, or Flatpak build
- Go 1.21+ to build from source
git clone https://github.com/tomharris/spy
cd spy
go build -o bin/spy ./cmd/spy
# put bin/spy somewhere on your PATH, or:
go install ./cmd/spyOn macOS, the first command you run triggers a Keychain prompt asking permission to read the "Slack Safe Storage" entry. Click Always Allow — otherwise every subsequent invocation will prompt again.
On Linux, if your login keyring is locked you may get a one-time unlock dialog the first time spy reads a v11-encrypted cookie. Cookies with the older v10 ("peanuts") encryption need no prompt at all.
| Command | Alias | What it does |
|---|---|---|
spy auth |
Verify auth and print workspace/user info | |
spy channels |
ch |
List public + private channels |
spy users |
u |
List workspace users |
spy dms |
dm |
List DM conversations |
spy read <channel|@user> [count] |
r |
Read recent messages |
spy thread <channel> <ts> [count] |
t |
Read replies in a thread |
spy search <query...> [count] |
Search messages workspace-wide | |
spy pins <channel> |
pin |
List pinned items |
spy activity |
a |
Unread + mention counts everywhere |
spy unread |
ur |
Same as activity but unreads-only, excludes muted |
spy starred |
star |
VIP users + starred items |
spy saved [count] |
sv |
Saved-for-later items (--all includes completed) |
| Command | Alias | What it does |
|---|---|---|
spy send <channel|@user> <msg...> |
s |
Send a message |
spy react <channel> <ts> <emoji> |
Add an emoji reaction | |
spy draft <channel> <msg...> |
Save a draft (appears in Slack UI) | |
spy draft thread <channel> <ts> <msg...> |
Save a draft thread reply | |
spy draft user <user> <msg...> |
Save a draft DM | |
spy draft drop <draft_id> |
Delete a draft | |
spy drafts |
List active drafts |
| Command | What it does |
|---|---|
spy workspaces |
List every signed-in workspace |
spy workspaces use <id> |
Set the default workspace |
spy workspaces refresh |
Re-extract tokens from the Slack app |
| Command | What it does |
|---|---|
spy mcp |
Run as an MCP server (stdio) exposing every read/write command as a tool |
Anywhere a command takes a <channel> it accepts: a channel name (general, #general), a user handle (@anjali, anjali), a user ID (U01…), or a channel/DM ID directly (C01…, D01…).
spy read supports --ts (show raw Slack timestamps — useful to copy for thread/react), --threads (auto-expand all threads inline), and --from/--to YYYY-MM-DD (strict — invalid dates fail loudly instead of being silently coerced).
If you're signed in to multiple workspaces, spy will not pick one for you. You must either:
- Pass
--workspace <domain|team_id>(or-w) on each invocation, or - Set
SPY_WORKSPACEin your environment, or - Run
spy workspaces use <id>to persist a default.
Identifiers match exactly against either the URL subdomain (acme from acme.slack.com) or the team ID (T024D6SRW80). Fuzzy matching is intentionally absent — auth ambiguity should never be silent.
spy -w acme ch # one-off
SPY_WORKSPACE=acme spy ch # per-shell default
spy workspaces use acme # persistent defaultEach workspace has its own cache directory, so caches don't cross-contaminate when you switch.
Every command accepts --json for scripting:
spy ch --json | jq '.channels[] | select(.is_private) | .name'
spy r general 100 --json --from 2026-05-20 | jq '.messages | length'spy mcp runs over stdio and exposes every read/write command as a tool, so you can wire it up to Claude Desktop, Claude Code, or any other MCP-aware client. The server picks one workspace at startup using the same rules as the CLI; launch separate processes if you want more than one.
Claude Desktop (macOS: ~/Library/Application Support/Claude/claude_desktop_config.json; Linux: ~/.config/Claude/claude_desktop_config.json):
{
"mcpServers": {
"spy": {
"command": "/absolute/path/to/spy",
"args": ["mcp"],
"env": { "SPY_WORKSPACE": "acme" }
}
}
}Claude Code (claude mcp add):
claude mcp add spy /absolute/path/to/spy mcp -e SPY_WORKSPACE=acmeThe server registers tools for every read command (auth, channels, users, dms, read, thread, search, pins, activity, unread, starred, saved, drafts_list) and every write command (send, react, draft_channel, draft_thread, draft_user, draft_drop). Tool arguments mirror the CLI flags; channel references accept the same forms (names, @handle, user IDs, channel IDs).
The Slack desktop app stores its session as two pieces:
- A per-workspace
xoxc-…token in a LevelDB store under the Slack data directory'sLocal Storage/leveldb/. - An account-level
xoxd-…session cookie in a SQLite Cookies file, encrypted with AES-128-CBC.
spy copies those files to /tmp (Slack holds exclusive locks on the originals while running), decrypts the cookie in pure Go, scans LevelDB for token candidates, and validates each one via Slack's auth.test to learn the team identity. Tokens are cached at ~/.local/spy/workspaces/<team_id>/workspace.json (mode 0600) and reused on subsequent runs. If a token goes stale, spy re-extracts on the next invalid_auth.
Only two things differ by platform — where the Slack directory lives, and how the cookie's AES key is obtained:
| Slack data directory | Cookie AES key | |
|---|---|---|
| macOS | ~/Library/Application Support/Slack (direct download), or the com.tinyspeck.slackmacgap App Store container |
the "Slack Safe Storage" password from the login Keychain (PBKDF2-SHA1, 1003 iterations) |
| Linux | ~/.config/Slack (native), or the Snap / Flatpak equivalents |
v10 cookies use Chromium's hardcoded "peanuts" key; v11 cookies use the "Slack Safe Storage" key from the freedesktop Secret Service (GNOME Keyring, KWallet, …) read over D-Bus (PBKDF2-SHA1, 1 iteration) |
The only shellout anywhere is security find-generic-password on macOS to read the Keychain entry. The Linux Secret Service lookup is pure-Go D-Bus, and everything else — the cookie decrypt, the LevelDB scan, the Web API client — is pure Go on both platforms.
Resolved user and channel lists are cached per-workspace for 5 minutes at ~/.local/spy/workspaces/<team_id>/{users,channels}.json. Pass --refresh to bypass them. Cold lookup against a workspace with ~4000 users takes ~500ms; warm lookups are ~80ms.
spy reads files that already exist on your machine and never sends them anywhere except to Slack's own API, using credentials you already authorized when you signed in to the Slack app. There is no telemetry and no third-party service in the loop. The on-disk cache contains your tokens — treat ~/.local/spy/ like you would any other credential store.
This project is not affiliated with or endorsed by Slack Technologies.
Released under the MIT License.
MIT License
Copyright (c) 2026 Tom Harris
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.