slack-cli
Skill0 starsChanged 4 months agoMITView on GitHub
Reference skill for Slack operations via a local Python CLI (`bin/slack`) that wraps the Slack Web API with a user OAuth token (xoxp). Replaces the Slack MCP plugin to avoid 3k+ tokens of tool schemas. Covers mentions, DMs, threads, send/reply, search, file download, and a raw API escape hatch. Use when the user wants to send/search/read Slack from the terminal or agent without loading the MCP plugin.
- Reads credentials
7 other projects use a version of this.
Adopted by
Most recently taken up by
- larchanka/skillstheir own version, first seen Nov 1, 2026
- ericmjl/skillstheir own version, first seen Sep 26, 2026
- NiazMorshed2007/jcrtheir own version, first seen Sep 20, 2026
- slackapi/slack-skills-plugintheir own version, first seen Aug 31, 2026
- ulises-jeremias/agent-toolkittheir own version, first seen Aug 31, 2026
- hideki5123/agent-skill-settheir own version, first seen Jul 9, 2026
Other versions7 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 7
- slackapi/slack-skills-plugin★ 165same name
- larchanka/skills★ 50same name
- ericmjl/skills★ 41same name
- NiazMorshed2007/jcr★ 19same name
- ulises-jeremias/agent-toolkit★ 19same name
- abdullahkhawer/devops-skills★ 8same name
- hideki5123/agent-skill-set★ 2same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository14 other files it gives its agents.
- atddSkill
- brainstormingSkill
- ghSkill
- grafanaSkill
- issueSkill
- linearSkill
- matter-cliSkill
- obsidianSkill
- ponytailSkill
- skill-creatorSkill
- slack-styleSkill
- systematic-debuggingSkill
- test-driven-developmentSkill
- tuxedoSkill
The file
Show its rules and text
We split this skill into the rules it gives an agent
15 rules. 0 are common, 4 are distinctive, and 11 are very specific to this project.
0
Common
Most of the 7 edited versions we read keep these.
4
Distinctive
This author's own choices. Worth a look.
11
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 29
If the keychain entry is missing or stale, recreate it:
- Very specific: line 39
If a method returns missing_scope, add the scope under User Token Scopes at https://api.slack.com/apps/<APP_ID>/oauth, then reinstall …
- Distinctive: line 144
When detail is genuinely needed: put the verdict in the message, the detail in a thread reply or a file, and say what you left out so they …
- Distinctive: line 169
Sentence case: Start each sentence with a capital letter.
- Very specific: line 170
No contractions: Write cannot, not can't. Never lemme, gonna, pls, kinda, nah, yep, kk.
- Very specific: line 171
Keep the subject, the verb, and the articles: Write I will check the pull request.
- Very specific: line 172
One instruction per sentence: Do not join two instructions with and or then.
- Very specific: line 174
Active voice. Name the actor: Write The migration dropped the column.
- Very specific: line 175
Simple tenses only: Write I fixed it. Do not write I have fixed it.
- Very specific: line 179
Condition before command, with a comma: Write If the test fails, read the log.
- Very specific: line 181
State uncertainty as its own plain sentence: Write The cause is not confirmed.
- Very specific: line 182
Warnings put the command first and the risk second: Write Do not deploy this yet.
- Very specific: line 194
Emoji: Keep the :smile: softener at roughly the measured rate.
- Very specific: line 275
UIDis readonly in bash: If you ever drop back to a while read -r CHAN UID loop, rename to PEER_ID — bash refuses the assignment with … - Distinctive: line 299
If a workflow needs admin-only Slack tools that this CLI doesn't cover, temporarily flip the plugin back on:
“Common” is measured: kept by at least 6 in 10 of the 7 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Slack CLI Reference
Wraps the Slack Web API behind a single Python CLI: slack, on PATH via
~/.local/bin/slack → this skill's bin/slack.
ln -sfn "$CLAUDE_PLUGIN_ROOT/skills/slack-cli/bin/slack" ~/.local/bin/slackCLAUDE_PLUGIN_ROOT is version-pinned, so re-run that after every plugin update
(or point the link at a working checkout of this repo, which does not move). A stale
link is silent: slack still runs, just an older build. Check with
readlink ~/.local/bin/slack.
The CLI is a PEP 723 inline-script Python file run via uv run --script (shebang). First call installs requests to a uv-managed venv (~9ms thereafter).
Auth is a user OAuth token (xoxp) issued by the user's "Claude MCP" Slack app (App ID <APP_ID>, <org> workspace <WORKSPACE_ID>). Acts as the user — full search, DM access, posts as them.
Token
Stored in macOS Keychain under service slack-xoxp. The CLI fetches at runtime; no shell handling needed. Override for one-off calls with SLACK_XOXP=xoxp-… slack ….
If the keychain entry is missing or stale, recreate it:
security add-generic-password -a "$USER" -s slack-xoxp -w 'xoxp-…' -UGranted user scopes
search:read, channels:history, groups:history, im:history, mpim:history, channels:read, groups:read, chat:write, reactions:write, users:read, users:read.email, files:read.
If a method returns missing_scope, add the scope under User Token Scopes at https://api.slack.com/apps/<APP_ID>/oauth, then reinstall (token rotation is off, so the xoxp string stays the same — no keychain update needed). react (reactions:write) and channels (channels:read/groups:read) are already granted.
Usage
slack whoami # auth.test (sanity check)
slack mentions [-n 20] # @mentions of self, newest first
slack dms [-n 15] [--group] [--no-resolve] # latest msg per 1:1 DM (--group adds mpim)
slack history <channel> [-n 30] [--since 2d] [--cursor X] [--links] [--activity] [--no-resolve] [--warm] # channel timeline, newest first
slack thread <channel> <thread-ts> [-n 50] [--since 2d] [--cursor X] [--links] [--activity] [--no-resolve] [--warm] # thread, oldest first
slack reply <channel> <thread-ts> [txt] [--raw] # post reply in thread
slack send <channel> [txt] [--raw] # post top-level message
# send/reply take Markdown (auto-converted to Slack mrkdwn; --raw to skip),
# --text-file <path|-> (body from file or stdin; preferred — see below),
# --blocks-file <path|-> (Block Kit JSON; text = notification fallback),
# --unfurl / --no-unfurl (force/suppress link unfurling)
slack search '<query>' [-n 20] # slack-search-bar syntax
slack channels [--types …] [--sort name|popularity] [-n 100] # list channels; warms the #name→id cache
slack users '<query>' [-n 20] # find users by name/handle/email (over the cache)
slack react <channel> <ts> <emoji> [--remove] # add/remove a reaction (needs reactions:write)
slack file <file-id> [-o <path>] # download a file by id (default: ./<name>; -o - for stdout)
slack upload <channel> <path…> [-t txt] [--text-file P] [--title T] [--thread-ts TS] [--raw]
# attach one or more local files to a single message (needs files:write).
# Several paths land as one message, not one message each. The initial comment
# goes through the same Markdown→mrkdwn conversion and @handle resolution as
# `send`. --title is single-file only. A U… resolves to an existing DM only —
# pass the D… (or message them first) if you have never DMed them.
slack raw <method> [k=v …] [--post] [--force] # escape hatch: any Web API method (--force: skip mrkdwn guardrail)
slack style scrape|stats|exemplars # build the personal voice corpus (see `slack-style` skill)
slack <cmd> --json # raw API JSON instead of formatted output<channel> accepts a C…/G…/D… id, U… (for DMs), or a #name / @user — names resolve to IDs through a persistent cache (see below). <thread-ts> is the epoch.microseconds string from a permalink (p1778164397756779 → 1778164397.756779).
Name resolution & persistent cache
history/thread/send/reply/react accept #channel-name and @user-name as the channel arg; users and the output labeling resolve IDs back to names. Resolution is backed by an on-disk cache at ~/.cache/slack-cli/{users,channels}.json (override base via $XDG_CACHE_HOME), so it is near-free after the first populate and survives the per-run rate cap. Warm it once:
slack channels # bulk-loads every channel name→id (and caches it)
slack history "#engineering" -n 20 # now resolvable by name, no extra calls--no-resolve ignores the cache for output (raw U…/#C…); name→id input still resolves (you can't call the API with a #name). --warm does one users.list walk up front for very busy channels. --since 2d (30m/12h/2d/1w) bounds reads to a recent window. --links appends a ↗ permalink per message (built locally, zero API). --activity includes join/leave/topic system messages, which are hidden by default.
Output format:
mentions/dms/search— numbered table; first column[N], last column the permalink (mentions/search) or text (dms). Reference rows by index when iterating ("open thread #3").<@U…>/<#C…>inside snippets are resolved to@Name/#channel.history/thread— clean per-message block (MCP-parity, ~8× smaller than raw JSON):
Head line carries the raw[1781030359.145779] <person> (@<handle>) · Jun 10 19:17 (16h) done @Ruslan the reviews are in, thanks! :+1: 2 📎 1 file(s): error.log ↳ Thread: 4 replies, 2 people, latest 15h ago[ts]— copy it straight intoslack thread/slack reply. Names,@-mentions,#-channels resolved; timestamps in your Slack-profile TZ; reactions:name: N; files by name (not block trees); thread parents summarized.historyis newest-first,threadoldest-first. When more remains, a finalnext: --cursor <X>line is printed — pass it back via--cursorto page.--no-resolveskips all name lookups (rawU…/#C…, nousers.info/conversations.infocalls) — use for big/fast pulls or after hitting a rate cap.--warmbulk-prefetchesusers.listonce before resolving — cheaper than N per-user lookups on a busy channel.
--json(every command) — raw Slack response, unchanged, suitable for| jq.dms --jsonrows carrychannel_id(D…) +user_id(peer'sU…, null for group) — so you can reply to a DM straight from the listing without a second lookup.
Markdown is auto-converted to Slack mrkdwn
send/reply take Markdown and convert it to Slack's mrkdwn before posting, because Slack does not use CommonMark — bold is one *star* (not **), italic is _underscore_, links are <url|label>, and there are no headings. Without conversion, **bold** renders with literal asterisks (the classic bug). Conversions:
| You write (Markdown) | Sent to Slack |
**bold**, __bold__ | *bold* |
*italic* | _italic_ |
***bolditalic*** | *_bolditalic_* |
[label](url) | <url|label> (any | in the URL is %7C-encoded so the link doesn't split; ) inside the URL is kept) |
# Heading | *Heading* (bold line) |
- item, + item | • item |
~~strike~~ | ~strike~ |
`code`, ```fenced``` | preserved as-is (not touched) |
Code spans/fences and link URLs are protected, so **/_ inside them survive. Pass --raw to skip conversion and send verbatim Slack mrkdwn (use it when you're hand-writing *slack bold* / <url|label> yourself).
Posting =
send/reply, neverslack rawor--rawfor agent-written Markdown. The converter only runs onsend/replywithout--raw. If you post CommonMark (**bold**,[label](url)) through theslack raw chat.postMessageescape hatch or with--raw, it ships verbatim and Slack renders the literal**/[]()— this is the exact bug to avoid. To make this hard to do by accident,slack raw chat.{postMessage,update,postEphemeral,scheduleMessage}refuses atext=containing CommonMark unless you pass--force, andsend/reply --rawprints a warning. For rich messages, use--blocks-fileonsend/reply(text still converts) rather than dropping to raw.The converter is not idempotent — running it over already-converted mrkdwn corrupts it (
*bold*→_bold_). Convert once, on the way out. That's why raw never auto-converts.
Mentions that actually ping
send/reply resolve @handle in the body to a real Slack mention (<@Uid>) so the person gets pinged — runs even with --raw (it's resolution, not formatting). Rules:
AUTHORING RULE (agent MUST follow): when writing a mention, ALWAYS use the exact handle (
@alice.king) or a quoted full name (@"Sam Eliwa"). NEVER write a bare first name (@Sam) or an unquoted@First Last. Reason: a bare first name silently matches a handle equal to that word — at<org>@<handle>is Sam<surname>(user@example.com), not whichever Sam you meant — and this workspace has ambiguous names. An unquoted@First Lastonly works ifFirst Lastis an exact real-name match; one token off and it degrades to the bare-first-name landmine. When unsure of the handle, runslack users <name>first, or paste<@ID>yourself.
- Exact match only —
@<handle>(handle) or@"Sam <surname>"(quoted full name). Matched case-insensitively against the user cache. @here/@channel/@everyone→ broadcast entities (<!here>etc.).- Ambiguous/unknown (e.g.
@Taylorwhen 7 people match, or a bare first name) is left as literal text with a stderr warning — a non-ping beats a mis-ping. Use the exact handle or paste<@ID>yourself. - Not a mention:
@mid-token (user@example.com) and@xinside`code`/```fences are never resolved.
Find a handle with slack users <name>. The cache is warmed by slack channels / slack users; re-run those if a freshly-renamed handle won't resolve.
Write in the user's voice, not yours
AUTHORING RULE (agent MUST follow): before any
send/reply, read~/.claude/slack-voice/voice.mdand write the message in that voice. If the file is missing, say so once (run /slack-style to build it) and keep the message short anyway.The register is ASD-STE100, not the one recorded in
voice.md. Read the next section before drafting. It names exactly which parts ofvoice.mdit supersedes and which parts still bind.
- Default length is 1–3 lines. The people receiving these messages are busy and did not ask for a report. A wall of agent prose makes them do the work you skipped — read nomeatproxy.com once if this feels excessive.
- When detail is genuinely needed: put the verdict in the message, the detail in a thread reply or a file, and say what you left out so they can ask ("full trace if you want it").
- Never claim more than was verified. Short does not mean confident; if something is unchecked, one clause says so.
- Build or refresh the profile with the
slack-styleskill (slack style scrape|stats|exemplars).
Register: ASD-STE100 Simplified Technical English
REGISTER RULE (agent MUST follow): write every message in Simplified Technical English. This supersedes the Register and Exemplars sections of
voice.md, which record a lowercase, contraction-heavy register that is no longer the output target.
voice.md still binds on everything else. Those parts do not conflict with STE:
voice.md section | status |
| Length (1–3 lines, ~200 char ceiling) | binds. It is stricter than the STE sentence caps. Take the stricter one. |
| Escape hatch (verdict here, detail in a thread, say what you left out) | binds |
Anti-patterns (em dashes, bullet walls, *Bold lead-in.*, closing reassurance, restating the question, unprompted jargon) | binds. STE agrees with every one of them. |
| Never point at a path on your own machine | binds |
Register (lowercase starts, lemme, gonna, pls, run-on commas) | superseded |
| Exemplars (the message bank) | superseded. Copy their length and their structure. Do not copy their wording. |
The rules, applied to a Slack message:
- Sentence case. Start each sentence with a capital letter. End it with a period.
- No contractions. Write
cannot, notcan't. Neverlemme,gonna,pls,kinda,nah,yep,kk. - Keep the subject, the verb, and the articles. Write
I will check the pull request.Do not writechecking the PR. - One instruction per sentence. Do not join two instructions with
andorthen. - Maximum 20 words in an instruction, 25 in a description. The 1–3 line ceiling is stricter and still applies.
- Active voice. Name the actor. Write
The migration dropped the column.Do not writeThe column was dropped. - Simple tenses only. Write
I fixed it.Do not writeI have fixed it.Do not use-ingverb forms. - Approved modals are
can,will, andmust. Banned:should,would,may,might,could. Forshould, writemustif the thing is required, and delete it if the thing is optional. - One word, one meaning. Pick one of check / verify / confirm. Reuse that one word in every message.
- Noun clusters of three words maximum. Break a longer stack with a preposition.
- Condition before command, with a comma. Write
If the test fails, read the log. - Delete words that carry no fact:
simply,just,seamlessly,robust,powerful,leverage,in order to. - State uncertainty as its own plain sentence. Write
The cause is not confirmed.Never writeit might have been caused by. - Warnings put the command first and the risk second. Write
Do not deploy this yet. The migration is not applied. - American spelling.
Where STE pushes against the length ceiling. One instruction per sentence produces more sentences. Write those sentences as continuous prose. A Slack reply is a message, not a form with fields, so nothing in it is prefixed with a heading word. If the message needs headings to stay readable, it is too long. Cut it to the verdict and one next step. Move the detail to a thread reply. The ~200 character ceiling outranks the urge to enumerate.
STE is silent on these, so they are unchanged:
- Emoji. Keep the
:smile:softener at roughly the measured rate. - Raw links. Paste the URL. Do not write
[label](url). - Mentions. The authoring rule above still applies without change.
- Code blocks and pasted logs. They are not prose. Never rewrite them.
Worked rewrites, on the shapes that occur most:
Examples below are invented, not quoted from any corpus. See "Privacy" in the
slack-style skill: real messages never enter this repo.
| intent | old register | STE |
| ack | kk. lemme grab that | Understood. I will start this now. |
| status | its up but still flaky | The service is deployed. It is still unstable. |
| ask | can u paste the stack trace pls | Please paste the stack trace. |
| uncertainty | hm thats odd. lemme poke at it | The result is unexpected. The cause is not confirmed. I will investigate. |
| pushing back | nah lets not, too much surface :smile: | No. That change adds too much risk. :smile: |
| being wrong | scratch that, wrong branch | I was wrong. I read the wrong branch. The correct value is 3. |
Sending text: prefer --text-file
For anything beyond a trivial one-liner, pass the body via --text-file <path> (or --text-file - for stdin) instead of the inline positional. Inline text goes through the shell, so backticks, $(...), quotes, and newlines get mangled or trigger command substitution — the same class of failure that mandates gh ... --body-file over --body. The file path is read verbatim, no shell interpretation. Write the body with a Write tool to a tmpfile, then point --text-file at it.
# robust: multi-line / backticks / quotes survive intact
slack send D0AGABWHTSA --text-file /tmp/msg.md
slack reply C0… 1778164397.756779 --text-file - <<'EOF'
done — see `Program.cs:477`. shipped in #709.
EOF
# rich message via Block Kit — text is the notification fallback, blocks are the body.
# Use this instead of dropping to `slack raw chat.postMessage` (which won't convert text).
slack send "#engineering" "Deploy summary" --blocks-file /tmp/blocks.json --no-unfurlCommon patterns
# triage mentions, then dive into one
slack mentions -n 20
slack thread C0AQ7M5R36W 1778164397.756779
# read a channel by name, then page older (copy the printed next: --cursor)
slack channels # one-time: warm the #name→id cache
slack history "#engineering" -n 30
slack history "#engineering" -n 30 --cursor bmV4dF90czoxNzcy…
# what happened in the last day, with citeable links, skip the join noise
slack history "#engineering" --since 1d --links
# search a channel
slack search 'in:#engineering deploy after:2026-04-01' -n 20
# find a user's id by name, then DM them
slack users alice # → row with U… id
slack send '@"Alice King"' "ping — got a sec?" # quoted full name (or handle @alice.king) — never bare @Alice
# discover file IDs in a thread, then download
slack thread C0… 1778… --json | jq -r '.messages[].files[]? | .id + "\t" + .name'
slack file F0B3R03TR4G # writes ./id_argus_a6000.pub
slack file F0B2UKBUC86 -o ~/.ssh/ # writes ~/.ssh/id_argus_a6000
slack file F0… -o - # stream to stdout for piping
# DM a user by email (UID is readonly in bash — use PEER_ID)
PEER_ID=$(slack raw users.lookupByEmail email=alice@example.com | jq -r '.user.id')
slack send "$PEER_ID" "ping — got a sec?"
# reply to a DM straight from the listing (channel_id is in the json)
CH=$(slack dms --json | jq -r '.[] | select(.peer=="<person>") | .channel_id')
TS=$(slack dms --json | jq -r '.[] | select(.peer=="<person>") | .ts')
slack reply "$CH" "$TS" --text-file /tmp/reply.md
# react to a message (needs reactions:write scope)
slack react "#engineering" 1714… thumbsup
slack react "#engineering" 1714… thumbsup --removeSlack search syntax mirrors the search bar: from:@user, in:#channel, before:, after:, has:link, etc. is:mention is not a self-mention filter — it matches the literal word "mention". Use <@USER_ID> (or just slack mentions) instead.
Pitfalls
UIDis readonly in bash. If you ever drop back to awhile read -r CHAN UIDloop, rename toPEER_ID— bash refuses the assignment withfailed to change user ID: operation not permitted.- rtk hook wraps
curlin Claude Code sessions and appends[Tee log: …]to stdout, breakingjq. The Python CLI doesn't shell out to curl, so it's immune. If you do call curl directly, usertk proxy curl …. search.messagesis rate-limited tier 2 (~20/min).users.info/conversations.infoare tier 3 (~50/min). Name resolution writes through to the persistent~/.cache/slack-cli/cache, so after aslack channels+ a first warm pass it's near-zero lookups thereafter. Within a single run an in-memory cache also dedups every id. If you do hit the cap on a cold/busy channel:--no-resolve(raw ids, zero lookups),--warm(oneusers.listwalk instead of N per-user calls), or re-run after a minute. The cache never expires automatically — re-runslack channelsif a freshly-renamed channel won't resolve.- Timestamps render in the token owner's Slack-profile timezone (
users.infotz, fetched once per run and cached; local TZ fallback). Format isJun 11 14:32 (2h)— absolute local time plus relative age. as_useris deprecated; xoxp posts as the token owner automatically.- DMs: post to user ID directly (
slack send U… …); no need to open IM first. - File URLs need bearer auth, not cookies.
https://files.slack.com/files-pri/…returns HTTP 200 with a Slack login page (HTML) if you fetch it withoutAuthorization: Bearer $xoxp. The CLI handles this — barecurldoes not. Easy to mistake for a successful download. - Sensitive content in Slack is already exposed. SSH private keys, API tokens, .env files, credentials posted as Slack attachments live in channel history, search index, mobile caches, and any workspace export. Downloading them later doesn't undo that — treat as compromised and rotate.
Escape hatch
For Web API methods the CLI doesn't wrap natively (canvases, admin scopes, etc.), use:
slack raw <method> key=val key=val # GET-style query params
slack raw <method> key=val --post # POST as JSON bodyThis is a thin shim over requests.get/post — same auth, same .ok enforcement, just no formatting. Pipe through jq.
Do not use raw to post messages. raw chat.{postMessage,update,postEphemeral,scheduleMessage} does not convert Markdown, so it ships literal **/[](). As a guard it refuses a text= containing CommonMark unless you add --force. Use slack send/slack reply (which convert, and take --blocks-file for Block Kit) instead.
Re-enabling the MCP plugin
If a workflow needs admin-only Slack tools that this CLI doesn't cover, temporarily flip the plugin back on:
# enable
jq '.enabledPlugins."slack@claude-plugins-official" = true' ~/.claude/settings.json | sponge ~/.claude/settings.json
# or use /pluginSource
- CLI: this skill's
bin/slack— single Python file, ~1300 lines, argparse subcommands. Reads share oneformat_messageformatter; name resolution is backed by in-memory + on-disk (~/.cache/slack-cli/) user/channel caches with#name/@user→ id lookup. - Token retrieval:
security find-generic-password -a "$USER" -s slack-xoxp -w(orSLACK_XOXPenv override). - Adding a subcommand: edit the script, add an
argparsesubparser +cmd_*function. No packaging, no install step.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

