SKILL.md
agent-slack
JSON in, JSON out, no interactivity. Lists are NDJSON (one object per line, then {"@pagination":…} / {"@referencedusers":…} meta lines). Entity gets — user get, channel get, usergroup get, emoji get — accept 1..N ids and emit NDJSON by default: one result line per id in input order, either the record or {"@unresolved":{"id","reason","fixableby","hint"?}} for an id that couldn't be resolved. Item-level misses exit 0; only a command-level failure (auth, network) exits 1. --format json on a single get returns the pretty object; --format json on multi collapses to {"data":[…],"@unresolved":[…]}. message get, message draft get, workflow get, and canvas get are single-arg and now emit NDJSON by default (one line); --format json|yaml returns the pretty object. config get takes 1..N keys and emits NDJSON (one line per key, or {"@unresolved":…} per miss). Errors are a single JSON object on stderr: {"error":"…","fixableby":"agent|human|retry","hint"?:"…","retryafterseconds"?:N}. fixableby=agent → fix the input and retry; human → credentials/permissions need a person; retry → transient failure, wait and re-run (retryafterseconds gives the recommended back-off when present).
Safety. Read and search freely. Do not send, edit, delete, react, schedule, invite, create channels, or add/remove emoji unless the user explicitly asked for that action. Destructive commands — message edit|delete, message draft delete, message scheduled cancel, channel new|invite, emoji add|remove — require --yes; without it they return a description of what would happen. Show that to the user before retrying with --yes.
This page covers the common paths inline. Every domain has complete, always- current detail in agent-slack <domain> usage — pull that (or the bundled references at the bottom) when a task needs a domain not shown here.
Setup (once)
agent-slack auth import-desktop # from Slack Desktop — best, no need to quit
# …or, if you don't run Slack Desktop:
agent-slack auth import-browser firefox # chrome|brave|firefox|zen|opera|safari
agent-slack auth test # am I set up? → who I am + which workspace
agent-slack auth list # configured credential sets (aliases)
agent-slack auth set-default acme # if several workspaces (alias or URL)
If no import works, agent-slack auth add --workspace-url <url> --form opens a native OS dialog so the human enters the token without it appearing in chat. Never ask the user to paste a token into the conversation, and never read credentials out of the store yourself; every command authenticates internally. Env override: SLACKTOKEN (+ SLACKCOOKIED + SLACKWORKSPACE_URL for xoxc browser tokens); expired browser tokens self-heal from Slack Desktop. With several workspaces, commands use the default; pass --workspace <alias> (or any unique substring) to target another (a message permalink carries its own workspace and overrides it). Credential sets are alias-keyed — several aliases may hold the same workspace URL (e.g. two humans in one Slack). Full menu — per-browser caveats, bot tokens, cURL import: agent-slack auth usage.
Reading
agent-slack message get "https://acme.slack.com/archives/C…/p1770165109628379" # quote it — a bare & truncates in the shell
agent-slack message get "#general" --ts "1770165109.628379"
agent-slack message list "#general" --limit 25
agent-slack message list "#general" --thread-ts "1770165109.628379" # whole thread
agent-slack unreads --counts-only
message get returns a permalink, a thread summary {ts,length}, and downloads attachments (local paths in files[].path; --no-download to skip; lists stay metadata-only — add --download or agent-slack file download F…). Reads resolve referenced users/channels/usergroups to profiles by default (referenced_* maps); tune with --resolve none|cached|auto|fresh. --include-reactions adds reactions. Targeting rules (permalink vs #channel vs U… DM, --ts): [references/targets.md](references/targets.md). These are the common reads; reaction filters, body-length caps, and the full flag set are in agent-slack message usage.
The message body is the content field — one rendered-Markdown string merging Slack's raw text, blocks, and attachment/app-card unfurls. There is no text field in output. A row without content genuinely has no text body; re-fetching another way won't reveal more (--full shows the raw payload if you must check).
Files over MCP (agent-slack mcp): an MCP client has no filesystem, so the local paths above come back as fetchable references ({"@type":"file","root":"cache","path":"<teamid>/<userid>/downloads/F….png"}) and you read them with the bridge's built-in fs tool — no host path needed. Pass the path from the reference verbatim; downloads nest under the identity (<teamid>/<userid>/downloads/), so don't assume a bare downloads/ prefix:
fs get cache <team_id>/<user_id>/downloads/F0BD….png # returns the bytes (images inline as image blocks)
fs find cache -e png # discover downloaded images
fs ls cache <team_id>/<user_id>/downloads # list a directory
get refuses files over a small inline limit. In plain-CLI use the paths are real and Read-able directly. Detail: [references/commands/message.md](references/commands/message.md).
Searching
agent-slack search messages "deploy failed" --channel "#ops" --after 2026-06-01
agent-slack search files "architecture diagram" --content-type image
File hits download automatically and report local paths you can Read.
Writing
agent-slack message send "#general" "ship it :rocket: — [release notes](https://acme.com/releases/4.2)"
agent-slack message send U05BRPTKL6A "ping" # DM auto-opens
agent-slack message send "<permalink>" "see the [run](https://ci.acme.com/123)" # reply in thread
agent-slack message send "#general" "see attached" --attach ./report.md
agent-slack message react add "<permalink>" :eyes:
agent-slack message edit "<permalink>" "fixed wording" --yes # edit/delete gated
agent-slack message delete "<permalink>" --yes
Message text is standard Markdown — bold, italic/italic, , `strike code , fenced code, - bullets, 1. numbers, > quotes. Indent a list item to nest it (any depth; bullets and numbers mix freely, and a sub-list doesn't restart the numbering above it). Links: write label for a labeled hyperlink; an unlabeled [url](url) or <url> renders as Slack's inline link chip (the scheme-stripped pill the composer makes from a pasted URL). Don't drop a truly bare URL in and hope it renders nicely — it won't auto-link. Two gotchas: text is underline (our extension, not bold) and \ escapes a literal marker. Mentions auto-resolve: @here/@channel, @U… ids, and bare @name/@group handles. Pass --slack-markdown for Slack's native mrkdwn (bold*, <url|label>); reads return Markdown too. Read [references/formatting.md](references/formatting.md) before composing any formatted message — full table of links, mentions, escaping, and the --slack-markdown dialect. Scheduling, forwarding, and the draft hand-off flow: agent-slack message usage`.
Waiting for a reply
ts=$(agent-slack message send "#team" "deploy blocked — proceed or hold?" | jq -r .ts)
agent-slack message await "#team" --since "$ts" --events message,reaction --timeout 30m
That is the pattern for asking a human something. A person answers in one of three ways — a message in the channel, a reply threaded on your message, or just an emoji reaction on it — and you cannot predict which. The call above catches all three. Verified live: the human threaded her first answer and reacted with a custom :letsdothis: emoji.
Two things make it correct rather than lucky:
--sinceis what stops you missing the answer. Pass thetsyour send
returned. It is exclusive, and it makes the command check what already arrived before it started listening — otherwise a fast reply lands in the gap between sending and waiting and is never seen. Replies threaded on that message count too, not just channel-level ones.
--events message,reaction— reactions are opt-in. Leave the reaction
name unfiltered and judge it yourself: ✅ ✔️ ☑️ 👍 🎉, a workspace's custom :approved:, or a plain "yes" all mean approval. If you do narrow with --reaction, read skipped — it carries the ❌ that would otherwise look like silence, and "rejected" must never be read as "no answer yet".
A timeout is not an error: exit 0, {"received": false}, and a cursor to pass as the next --since, so looping loses nothing.
agent-slack message stream --channel "#alerts" --duration 30m --idle-timeout 10m
That is the pattern for watching a channel — deploys, alerts, an incident room. NDJSON, one event per line, @summary with per-channel cursors at the end. Always bounded, so it returns.
App posts count as messages and carry author.botid with no author.userid — most alert traffic is apps, so never key on user_id alone. Both commands drop the socket's bookkeeping noise (typing, read marks, badges) and never re-emit a thread's parent when a reply arrives. Neither polls or spends rate-limit budget. stream needs browser auth; await falls back to polling on a bot token. Full flags: [references/commands/message.md](references/commands/message.md).
Finding people & channels
agent-slack channel list # compact; --full for raw
agent-slack channel get "#general" "#ops" # NDJSON default (one line per id; @unresolved for misses)
agent-slack channel members "#general" --resolve auto # who's in it
agent-slack user get @alice @bob # NDJSON default; @unresolved per miss; exit 0
agent-slack user dm-open @alice @bob # group DM channel id
Other domains
Each has full detail in agent-slack <domain> usage — read it only when the task needs that domain (so finding a user never makes you load emoji, etc.):
| Domain | For | Detail |
|---|---|---|
usergroup |
subteams (@group): list / get / members |
agent-slack usergroup usage |
emoji |
custom emoji: list / get / search; add / remove (--yes) |
agent-slack emoji usage |
message draft · scheduled |
hand-off drafts for a human; scheduled sends | agent-slack message usage |
workflow |
discover and run Slack workflows | agent-slack workflow usage |
canvas |
fetch a canvas as Markdown | agent-slack canvas usage |
later |
saved-for-later (Slack's Later tab) | agent-slack later usage |
file |
point-pull a file (F…) seen in any output |
agent-slack file usage |
cache · config |
inspect / warm / purge the cache; persist TTLs | agent-slack cache usage |
api |
raw Slack API escape hatch (api call <method>) |
agent-slack api usage |
The resolution cache (channel/user/handle/workflow/emoji lookups) fills automatically as you work and is transparent — reach for cache only to pre-warm (cache warm) or clear it.
More detail
- Live, always-current:
agent-slack usage(overview) and
agent-slack <domain> usage (per-domain — the authoritative source).
- Bundled deep-dives:
[references/commands.md](references/commands.md) — full command map, split per domain, with flags and --yes gates · [references/targets.md](references/targets.md) — targeting and multi-workspace · [references/formatting.md](references/formatting.md) — Markdown, mentions, --slack-markdown · [references/output.md](references/output.md) — NDJSON/meta contract, --full, payload shapes, cache.