Content
# 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
```bash
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
| 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) |
### Write
| 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 |
### Workspaces
| 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 |
### MCP server
| Command | What it does |
| --- | --- |
| `spy mcp` | Run as an MCP server (stdio) exposing every read/write command as a tool |
### 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_WORKSPACE` in 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.
```bash
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:
```bash
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`):
```json
{
"mcpServers": {
"spy": {
"command": "/absolute/path/to/spy",
"args": ["mcp"],
"env": { "SPY_WORKSPACE": "acme" }
}
}
}
```
Claude Code (`claude mcp add`):
```bash
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's `Local 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.
## 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](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.
```