spy
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?
...
Requirements
- 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
Install
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/spy
On 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.
Commands
Read
Write
Workspaces
MCP server
Channel references
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…).
Read flags
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).
Workspaces
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 default
Each workspace has its own cache directory, so caches don't cross-contaminate when you switch.
JSON output
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'
Use as an MCP server
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=acme
The 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).
How it works
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:
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.
Cache
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.
Privacy / scope
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.
License
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.