Claude Skill

slack

Operate Slack workspaces from a terminal or agent: list channels, read messages, follow threads, search message history, list files, and verify inbound webhook signatures — with a bundled slack-cli script that is read-only by default and gates every send behind a --dry-run/--yes

LLM Mart · 0 points · 6 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download magnus919-agent-skills-slack-d0edebb.zip · 18 KB
Part of magnus919/agent-skills — 145 skills

Install

skills CLI npx skills add https://github.com/magnus919/agent-skills/tree/main/slack
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install magnus919-agent-skills@llmmart
Git git clone https://github.com/magnus919/agent-skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole magnus919/agent-skills collection as a plugin from our marketplace. Git is the plain clone.

README

Slack — Read and Post to Slack from the Terminal

Operate a Slack workspace without leaving your terminal or your agent's tool loop: list channels, read messages, follow threads, search history, find files, and verify that inbound webhook events are authentic.

Why Install This Skill

Most agents have no way to answer the basic question "what did the team say about X in Slack?" — so they guess, or you paste screenshots. This skill gives your agent a real, bounded read path into a workspace (channels, messages, threads, search, files) plus a safe write path: sending a message is a guarded mutation that requires a preview and an explicit confirmation, so the agent can triage and answer without ever posting by accident.

It ships slack-cli, a small Python script that speaks the Slack Web API with no third-party dependencies. Reads are capped (--limit), output is clean JSON for the agent or readable text for you, and --help works with no token and no network. Webhook verification is built in: any event endpoint can prove a request really came from Slack using the standard HMAC-SHA256 signature check.

What You Get

Directory Purpose
SKILL.md Agent-facing operating contract, mutation gates, and verification boundaries
references/ Dated source index and a web API operations reference (methods, scopes, pagination, webhook verification)
scripts/slack-cli Bounded, stdlib-only CLI: channels, messages, threads, search, files, webhook verify; --json, --limit, sends gated by --dry-run/--yes
tests/ 16 deterministic tests against a stub Slack API, covering the mutation gate and read-only contract
evals/evals.json Six output-quality evaluation cases for agent runs

Quick Start

# Help works with no token and no network
slack/scripts/slack-cli --help

# Find the channel ID from a name (reads are capped at --limit)
SLACK_TOKEN=xoxb-... slack/scripts/slack-cli --json --limit 10 channels list

# Read the latest messages in a channel
SLACK_TOKEN=xoxb-... slack/scripts/slack-cli --json messages list --channel C12345

# Follow a thread (parent ts from the message list)
SLACK_TOKEN=xoxb-... slack/scripts/slack-cli --json threads list --channel C12345 --ts 1712345678.000001

# Search history, bounded
SLACK_TOKEN=xoxb-... slack/scripts/slack-cli --json search messages --query "incident"

# Send only with a preview first, then explicit confirmation
SLACK_TOKEN=xoxb-... slack/scripts/slack-cli messages send --channel C12345 --text "on it" --dry-run
SLACK_TOKEN=xoxb-... slack/scripts/slack-cli messages send --channel C12345 --text "on it" --yes

# Verify an inbound webhook before trusting it
slack/scripts/slack-cli webhook verify --body-file body.json --signature "v0=..." --timestamp 1712345678

Triggers

Load this skill for slack operations: "what was said in #channel", reading or listing messages and channels, following or summarizing threads, searching Slack history, finding shared files, posting a message or thread reply (with confirmation), and verifying X-Slack-Signature webhook events. Do not load it for building Slack apps or bots, workspace administration (user provisioning, org settings), or other chat platforms like Discord or Teams.

Requirements

  • Python 3.9+ for slack-cli (stdlib only; --help and webhook verification need nothing else).
  • A Slack bot or user token (SLACK_TOKEN) with the scopes the read needs: channels:read, channels:history, groups:read, groups:history, search:read, files:read, and chat:write for sending. For webhook verify, the app signing secret (SLACK_WEBHOOK_SECRET).
  • Network access to api.slack.com for live reads and sends.

Skill manifest

Slack Operations

Use this skill to read and, with explicit confirmation, write Slack data through the Slack Web API: channels, messages, threads, search, files, and webhook signature verification. This is a tool skill for the Slack platform. Building Slack apps and bots is application development; workspace administration (user provisioning, org-level settings, SSO) lives in the Slack admin console. This skill owns the everyday agent workflow: knowing what was said, finding it later, and posting a reply when a human confirms.

Operating contract

  1. Read-only discovery before any mutation. List channels, read history, follow threads, search, and list files freely. The bundled slack-cli script makes reads without writing anything.
  2. Confirm the target, scope, and rollback path before acting. Sending a message or replying in a thread changes shared workspace state visible to everyone: it requires an explicit human directive naming the channel, plus --dry-run preview and --yes confirmation through slack-cli. There is no "unsend" for team members who already read it.
  3. Respect bounded reads. Slack paginates; never page past what the task needs. slack-cli --limit N caps every listing, and responses summarize records rather than dumping raw payloads.
  4. Verify webhooks before trusting them. Any handler that accepts Slack events must verify X-Slack-Signature and X-Slack-Request-Timestamp against the app signing secret, or anyone who can reach the endpoint can forge events. slack-cli webhook verify does this check.
  5. Keep evidence bounded. Quote short message excerpts and IDs; never paste full threads, tokens, or file contents into chat.

The slack-cli script

scripts/slack-cli is an agent-first, stdlib-only CLI over the Slack Web API. It covers the full issue scope: messages, channels, threads, search, files, and webhook verification.

slack/scripts/slack-cli --help                          # no token or network needed
slack/scripts/slack-cli --json --limit 10 channels list
slack/scripts/slack-cli --json messages list --channel C12345
slack/scripts/slack-cli --json threads list --channel C12345 --ts 1712345678.000001
slack/scripts/slack-cli --json search messages --query "incident"
slack/scripts/slack-cli --json files list --limit 5
slack/scripts/slack-cli messages send --channel C12345 --text "on it" --dry-run   # preview
slack/scripts/slack-cli messages send --channel C12345 --text "on it" --yes       # confirmed
slack/scripts/slack-cli webhook verify --body-file body.json --signature "v0=..." --timestamp 1712345678

Exit codes: 0 success, 1 API error or failed verification, 2 usage error. Sends are guarded: without --dry-run or --yes the script refuses with exit 1 and never calls the API. Reads are bounded by --limit (default 20, max 100).

Operating loop

  1. Scope the workspace surface: which channel(s) are relevant, what the question is (what was said, who said it, when), and whether any action is a mutation.
  2. Read with bounds: channels list to find IDs, messages list/threads list for history, search messages for cross-channel discovery. All read-only.
  3. Triage the answer: map the question to evidence (thread replies for context, search for the exact phrase, files list for shared artifacts).
  4. Act with confirmation: only a human directive to send, previewed with --dry-run and confirmed with --yes.
  5. Verify: confirm the posted message ts/channel in the response, or for webhooks confirm the signature check result before trusting the event.

Messages, channels, threads

  • Channels (conversations.list): public and private channels, archived state, member counts. Channel IDs (C...) are the stable key for every other call — resolve names to IDs before use.
  • Messages (conversations.history): newest-first history in a channel, one page at a time. Message records carry ts (the ID), user, and text. Use --cursor from the response metadata to page deliberately.
  • Threads (conversations.replies): replies keyed by the parent ts; the parent message is the first result. Thread replies keep thread_ts set to the parent.
  • Sending (chat.postMessage): the only mutation in this skill's surface. Always preview with --dry-run, confirm with --yes, and pass --thread-ts to reply in a thread instead of starting a new message. Verify the returned ts and channel.

Search and files

  • Search (search.messages): full-text search across visible history with Slack's search syntax (from:, in:, quoted phrases, before:/after:). Results include the matching channel; total tells you how many matches exist while matches stays bounded by --limit.
  • Files (files.list): files shared in the workspace, filterable by channel or user, with permalinks and sizes. Downloading file content is out of scope for the CLI (bounded reads); use it to find the file, then fetch the permalink with an authenticated request when a human asks for the content.

Webhook verification

Slack signs every HTTP request to your event/command/interactivity endpoints. To verify:

  1. Take the raw request body (exactly as received — do not re-encode).
  2. Check X-Slack-Request-Timestamp is within ~5 minutes of now (replay protection).
  3. Compute v0=HMAC_SHA256(signing_secret, "v0:" + timestamp + ":" + body) and compare with X-Slack-Signature using a constant-time comparison.
  4. Reject with 401 if the timestamp is stale or the signature mismatches.

slack-cli webhook verify --body-file body.json --signature "v0=..." --timestamp <unix> runs exactly this check against SLACK_WEBHOOK_SECRET (or --secret) and reports a constant-time-verified result. Always verify before trusting event payloads — unverified webhook endpoints accept forged events.

Reference routing

Load when Reference
Sources, scope tables, refresh procedure references/00-source-index.md
API method surface, pagination, scopes, and webhook verification details references/01-web-api-operations.md

Included artifacts

  • scripts/slack-cli: bounded, stdlib-only CLI (messages, channels, threads, search, files, webhook verify; --json; --limit; send gated by --dry-run/--yes).
  • tests/test_slack_cli.py: 16 deterministic tests against a stub Slack API, including the mutation gate and the read-only contract.
  • references/: dated source index + web API operations reference.
  • evals/evals.json: six output-quality evaluation cases for agent runs.

Verification boundary

Claim Minimum evidence
A channel exists and its name slack-cli channels list --json returns it with its C... ID
A message was sent slack-cli messages send --yes returns the new ts and channel, and messages list shows it
A search found matches slack-cli search messages --query "..." --json returns total_matches with bounded matches
A webhook is authentic slack-cli webhook verify exits 0 with verified: true for the exact body/signature/timestamp
A file exists slack-cli files list --json returns its F... ID and permalink

Hard boundaries

  • Never send a message or thread reply without a human directive, --dry-run preview, and --yes confirmation — Slack posts are public, durable, and unreadable-back.
  • Never trust an inbound webhook without signature and timestamp verification.
  • Never page reads past --limit; never dump full threads, tokens, or file contents into chat.
  • This skill operates the Slack Web API. It does not build Slack apps (application development), manage users/org settings (admin console), or cover alternative channels platforms (that is their own tooling).

When not to use

  • Building Slack apps or bots (Block Kit, Bolt, slash-command apps, OAuth flow design) — that is application development; see backend-engineering for service design.
  • Workspace administration (user provisioning, deprovisioning, org-level settings, SSO/SAML, data exports at the org level) — that is the Slack admin console, not the Web API.
  • Other team-chat platforms (Discord, Mattermost, Teams) — each has its own tooling; this skill covers Slack only.
  • Company-wide policy on messaging or channel governance — that is organizational policy, not an API operation.
Files (agent-skills)
  • evals
    • evals.json 5.9 KB
      {
        "schema_version": 1,
        "skill_name": "slack",
        "evals": [
          {
            "id": "channel-discovery",
            "prompt": "A user asks: 'Find the channel where the on-call team discusses incidents and post its name and ID. I believe it is something like #incidents or #oncall.'",
            "expected_output": "Run slack-cli channels list with a bounded --limit and match the channel name case-insensitively against the user's guesses, returning the exact channel name, its C-prefixed ID, whether it is private, and its member count. If more than one candidate matches, list all candidates with IDs and ask the user to disambiguate rather than guessing. Reads only — no messages are posted.",
            "assertions": [
              "The response resolves a channel name to its C-prefixed channel ID using slack-cli channels list",
              "Reads are bounded with --limit and the response never pages past the cap",
              "Multiple candidates are listed for the user to disambiguate instead of picking arbitrarily",
              "The operation is read-only and no message is sent"
            ]
          },
          {
            "id": "message-history-read",
            "prompt": "A user asks: 'What did people say in #deployments in the last few hours? Summarize the discussion. Do not post anything.'",
            "expected_output": "Run slack-cli messages list --channel with the resolved channel ID and a bounded --limit, then summarize the returned messages in chronological order: who said what, and the timestamps. Quote short excerpts with message ts values so the user can jump to the source. The summary explicitly notes the cap (e.g. 'last 20 messages') and offers to page further with a cursor if needed. Nothing is posted.",
            "assertions": [
              "The channel name is resolved to an ID before reading history",
              "Messages are read with a bounded --limit and summarized in chronological order",
              "Short excerpts are quoted with their ts identifiers",
              "The response states the read cap and explicitly does not post anything"
            ]
          },
          {
            "id": "thread-follow-up",
            "prompt": "A user asks: 'Someone replied in the thread under message 1712345678.000001 in #support. What did they say and what is the open question?'",
            "expected_output": "Run slack-cli threads list --channel with the resolved support channel ID and --ts 1712345678.000001, bounded by --limit, and summarize the replies in order: each reply's author, text, and ts. The parent message is identified as the first result. The response flags the open question if one is visible, quotes short excerpts, and does not reply into the thread without an explicit request.",
            "assertions": [
              "The thread is read via slack-cli threads list with the parent ts",
              "Replies are summarized in order with authors and ts values",
              "The parent message is identified as the first result of the replies call",
              "The agent does not post into the thread without explicit confirmation"
            ]
          },
          {
            "id": "search-history",
            "prompt": "A user asks: 'Search Slack history for when we last discussed the database migration and who was involved. I do not know which channel.'",
            "expected_output": "Run slack-cli search messages --query for terms like 'database migration' with a bounded --limit, and report each match: channel, author, timestamp, and a short excerpt. The response uses the total_matches count to indicate how many hits exist beyond the cap, offers to narrow the search with Slack syntax (in:, from:, before:/after:) if the hit list is large, and stays read-only.",
            "assertions": [
              "Slack search is run with a bounded --limit and the query is derived from the user's topic",
              "Each match reports channel, author, timestamp, and a short excerpt",
              "The total match count is used to suggest narrowing the query",
              "The operation is read-only"
            ]
          },
          {
            "id": "guarded-send",
            "prompt": "A user asks: 'Post to #incidents that we are investigating the checkout failure and will update in 30 minutes.'",
            "expected_output": "The agent resolves #incidents to its channel ID, then presents a preview of the exact message (channel, text, and that it will be posted as a new message, not a thread reply) via slack-cli messages send --dry-run and asks the user to confirm. Only after explicit confirmation does it run messages send --yes and report the returned ts and channel. If the user only asked to draft or preview, nothing is posted.",
            "assertions": [
              "The exact message is previewed with --dry-run before any send",
              "The send happens only after explicit user confirmation, via --yes",
              "The response reports the posted message ts and channel as delivery evidence",
              "No API call is made without confirmation"
            ]
          },
          {
            "id": "webhook-verification",
            "prompt": "A user runs an endpoint that receives Slack events and asks: 'An event just arrived with X-Slack-Request-Timestamp 1712345678 and X-Slack-Signature v0=... The raw body is in body.json. Is it really from Slack?'",
            "expected_output": "Run slack-cli webhook verify with the exact raw body file, the signature header value, and the timestamp, using the app signing secret (SLACK_WEBHOOK_SECRET or --secret). Explain that verification computes HMAC-SHA256 over 'v0:' + timestamp + ':' + the exact raw body and compares constant-time with the header. A pass reports verified: true and the event may be processed; a fail or stale-timestamp error means the request must be rejected with 401 and treated as forged.",
            "assertions": [
              "The exact raw body, signature, and timestamp are passed to slack-cli webhook verify",
              "The HMAC-SHA256 verification construction (v0:timestamp:body) is explained",
              "A verified result is distinguished from a rejection with 401 on failure or stale timestamp",
              "The response does not re-encode or reformat the body before verification"
            ]
          }
        ]
      }
      
  • references
    • 00-source-index.md 1.2 KB
      # Slack — Source Index
      
      > **Last Updated:** 2026-08-03
      
      This skill is a distilled operating layer over Slack's public developer documentation. Facts and method names in this skill are grounded in the sources below; refresh this index when Slack ships API changes.
      
      | Topic | Source | URL |
      |---|---|---|
      | Web API overview and conventions | Slack Web API documentation | https://api.slack.com/web |
      | Method catalog (conversations, chat, search, files) | Slack Method Reference | https://api.slack.com/methods |
      | Scopes and tokens | Slack Token & Scopes docs | https://api.slack.com/authentication/token-types |
      | Webhook signing (signature verification) | Verifying requests from Slack | https://api.slack.com/authentication/verifying-requests-from-slack |
      | Pagination | Paging through collections | https://docs.slack.dev/web/using-the-web-api/#pagination |
      
      ## Refresh procedure
      
      - Re-check the method reference when a call returns `method_not_supported` or a scope error for a documented method.
      - Re-check the signing-verification page before changing anything in `webhook verify`; the `v0` signing scheme is a security boundary.
      - Update `research_checked` in `SKILL.md` frontmatter and this file's `Last Updated` when you verify the sources again.
      
    • 01-web-api-operations.md 3.7 KB
      # Slack Web API Operations
      
      > **Last Updated:** 2026-08-03
      
      Operational detail for the Slack Web API surface the skill owns: methods, scopes, pagination, error handling, and webhook signature verification. The bundled `slack-cli` implements this reference; use this document when a call behaves unexpectedly or you need the exact scope/method contract.
      
      ## Method surface
      
      All calls POST form-encoded fields to `https://slack.com/api/<method>` with the token as `Authorization: Bearer` and read the JSON response; `ok: false` plus `error` is the error shape.
      
      | Operation | Method | Required scopes | Notes |
      |---|---|---|---|
      | List channels | `conversations.list` | `channels:read`, `groups:read` | `types` selects public/private/IM/MPIM; `exclude_archived` filters |
      | Read history | `conversations.history` | `channels:history`, `groups:history` | Newest first; returns `messages` + `response_metadata.next_cursor` |
      | Thread replies | `conversations.replies` | `channels:history`, `groups:history` | Parent message is the first result; `ts` is the parent timestamp |
      | Post message | `chat.postMessage` | `chat:write` | Guarded mutation; `thread_ts` replies in a thread |
      | Search messages | `search.messages` | `search:read` | Supports `in:`, `from:`, `before:`/`after:`, quoted phrases |
      | List files | `files.list` | `files:read` | Optionally filter by channel or user |
      
      ## Pagination and bounded reads
      
      - Every listing method returns at most `limit` results per page (max 100 for most) plus `response_metadata.next_cursor`.
      - **Bounded-read rule:** request only what the task needs; `slack-cli --limit` caps at the request level. If a task needs more, page explicitly with `--cursor`, and stop when the question is answered.
      - Search returns `total` (total matches) alongside the bounded `matches` array — report the total, return only the cap.
      
      ## Error handling
      
      - `ok: false` with an `error` string: `invalid_auth` (token bad/expired), `missing_scope` (token lacks the scope — the exact scope is in the response `needed`/`provided` fields), `channel_not_found`, `not_in_channel`, `ratelimited` (429 — back off and retry with `Retry-After`).
      - `is_ratelimited: true` in a 200 response: the method was throttled; slow down.
      - Never retry a failed send blindly: `chat.postMessage` can be retried with the same `text` (it is not idempotent in the strict sense), so confirm state via `conversations.history` before re-sending.
      
      ## Webhook signature verification
      
      Slack signs every outbound HTTP request to your endpoints (events, slash commands, interactivity). Algorithm per [Verifying requests from Slack](https://api.slack.com/authentication/verifying-requests-from-slack):
      
      1. Take the **exact raw request body** bytes — any re-encoding (JSON pretty-print, charset change) breaks the signature.
      2. Reject if `|now - X-Slack-Request-Timestamp| > 300` seconds (replay window).
      3. Compute `v0=HMAC_SHA256(signing_secret, "v0:" + timestamp + ":" + body)`.
      4. Compare with `X-Slack-Signature` using a constant-time comparison (`hmac.compare_digest`).
      
      The app signing secret lives in the Slack app settings (Basic Information → App Credentials → Signing Secret) and is distinct from the bot token. Never log the secret, the signature comparison, or full webhook bodies; `slack-cli webhook verify` reports a boolean result.
      
      ## Token and scope hygiene
      
      - Bot tokens (`xoxb-`) act as the app; user tokens (`xoxp-`) act as a user. Scope needs differ: reading public channels needs `channels:history`; reading private channels needs `groups:history` on a bot that has been added to the channel.
      - Store tokens in the environment (`SLACK_TOKEN`), never in code, chat, or commit messages. Revoke and rotate a token that leaks — it is a credential, not a config value.
      
  • scripts
    • slack-cli 16.5 KB · in bundle
  • tests
    • test_slack_cli.py 11 KB
      #!/usr/bin/env python3
      """Deterministic tests for slack/scripts/slack-cli.
      
      Runs the script as a subprocess so the tests exercise the real CLI surface
      (--help, --json, --limit, mutation gate, exit codes, JSON payloads). A local
      stdlib HTTP server stubs the Slack Web API methods, so no external network or
      Slack workspace is needed. Also asserts the read-only contract: the script
      never opens files in write mode, and the mutation gate refuses to send without
      --dry-run or --yes.
      """
      import json
      import os
      import socket
      import subprocess
      import sys
      import threading
      import unittest
      import urllib.parse
      from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
      from pathlib import Path
      
      ROOT = Path(__file__).resolve().parent.parent
      SCRIPT = ROOT / "scripts" / "slack-cli"
      
      VALID_CHANNEL = {"id": "C123", "name": "general", "is_channel": True, "is_private": False, "num_members": 42}
      VALID_MESSAGE = {"ts": "1712345678.000001", "user": "U1", "type": "message",
                       "channel": "C123", "text": "hello from tests", "thread_ts": ""}
      
      
      class StubSlackServer:
          """Minimal read-only stub of the Slack Web API surface."""
      
          def __init__(self):
              self.posted = []  # (method, fields) recorded by the stub
              handler = self._make_handler()
              self.server = ThreadingHTTPServer(("127.0.0.1", 0), handler)
              self.port = self.server.server_address[1]
              self.thread = threading.Thread(target=self.server.serve_forever, daemon=True)
      
          def _make_handler(self):
              stub = self
      
              class Handler(BaseHTTPRequestHandler):
                  def do_POST(self):  # noqa: N802
                      length = int(self.headers.get("Content-Length", "0"))
                      raw = self.rfile.read(length)
                      fields = {k: v for k, v in urllib.parse.parse_qsl(raw.decode("utf-8"))}
                      method = self.path.strip("/")
                      stub.posted.append((method, fields))
                      self.send_response(200)
                      self.send_header("Content-Type", "application/json")
                      self.end_headers()
                      if method == "conversations.list":
                          payload = {"ok": True, "channels": [VALID_CHANNEL],
                                     "response_metadata": {"next_cursor": ""}}
                      elif method == "conversations.history":
                          payload = {"ok": True, "messages": [VALID_MESSAGE],
                                     "response_metadata": {"next_cursor": ""}}
                      elif method == "conversations.replies":
                          payload = {"ok": True, "messages": [dict(VALID_MESSAGE, thread_ts="1712345678.000001")],
                                     "response_metadata": {"next_cursor": ""}}
                      elif method == "chat.postMessage":
                          payload = {"ok": True, "ts": "1712345679.000002", "channel": fields.get("channel", ""),
                                     "message": {"ts": "1712345679.000002", "user": "U1", "type": "message",
                                                 "text": fields.get("text", "")}}
                      elif method == "search.messages":
                          payload = {"ok": True,
                                     "messages": {"total": 1, "matches": [dict(VALID_MESSAGE, channel="C123")]}}
                      elif method == "files.list":
                          payload = {"ok": True, "files": [{"id": "F1", "name": "notes.md", "title": "notes",
                                                            "filetype": "text", "size": 512}],
                                     "response_metadata": {"next_cursor": ""}}
                      else:
                          payload = {"ok": False, "error": "method_not_supported"}
                      self.wfile.write(json.dumps(payload).encode("utf-8"))
      
                  def log_message(self, *args):  # silence stderr
                      pass
      
              return Handler
      
          def __enter__(self):
              self.thread.start()
              return self
      
          def __exit__(self, *exc):
              self.server.shutdown()
              self.server.server_close()
      
      
      def run_script(env, *args):
          return subprocess.run(
              [sys.executable, str(SCRIPT), *args],
              capture_output=True,
              text=True,
              timeout=30,
              env=env,
          )
      
      
      def base_env(stub):
          env = dict(os.environ)
          env["SLACK_TOKEN"] = "xoxb-test-token"
          env["SLACK_API_BASE"] = f"http://127.0.0.1:{stub.port}/"
          return env
      
      
      def load_json(proc):
          return json.loads(proc.stdout)
      
      
      class SlackCliTests(unittest.TestCase):
          def test_help_lists_json_and_bounded_reads(self):
              proc = run_script(dict(os.environ), "--help")
              self.assertEqual(proc.returncode, 0)
              self.assertIn("--json", proc.stdout)
              self.assertIn("--limit", proc.stdout)
      
          def test_help_works_without_token(self):
              env = dict(os.environ)
              env.pop("SLACK_TOKEN", None)
              proc = run_script(env, "channels", "list", "--help")
              self.assertEqual(proc.returncode, 0)
      
          def test_channels_list_json(self):
              with StubSlackServer() as stub:
                  proc = run_script(base_env(stub), "--json", "--limit", "5", "channels", "list")
              self.assertEqual(proc.returncode, 0, proc.stderr)
              data = load_json(proc)
              self.assertTrue(data["ok"])
              self.assertEqual(data["channels"][0]["id"], "C123")
      
          def test_limit_is_bounded_in_request(self):
              with StubSlackServer() as stub:
                  run_script(base_env(stub), "--json", "--limit", "3", "channels", "list")
                  methods = [m for m, _ in stub.posted]
                  fields = dict(stub.posted[methods.index("conversations.list")][1])
              self.assertEqual(fields.get("limit"), "3")
      
          def test_messages_send_requires_confirmation(self):
              with StubSlackServer() as stub:
                  proc = run_script(base_env(stub), "messages", "send", "--channel", "C123",
                                    "--text", "hello")
              self.assertEqual(proc.returncode, 1)
              self.assertIn("refusing to send", proc.stderr)
              self.assertEqual(stub.posted, [], "no API call may be made without confirmation")
      
          def test_messages_send_dry_run_does_not_post(self):
              with StubSlackServer() as stub:
                  proc = run_script(base_env(stub), "--json", "messages", "send", "--channel", "C123",
                                    "--text", "hello", "--dry-run")
              self.assertEqual(proc.returncode, 0, proc.stderr)
              data = load_json(proc)
              self.assertTrue(data["dry_run"])
              self.assertEqual(stub.posted, [], "dry-run must not reach the API")
      
          def test_messages_send_with_yes_posts(self):
              with StubSlackServer() as stub:
                  proc = run_script(base_env(stub), "--json", "messages", "send", "--channel", "C123",
                                    "--text", "hello", "--yes")
              self.assertEqual(proc.returncode, 0, proc.stderr)
              data = load_json(proc)
              self.assertEqual(data["ts"], "1712345679.000002")
              methods = [m for m, _ in stub.posted]
              self.assertIn("chat.postMessage", methods)
      
          def test_messages_list(self):
              with StubSlackServer() as stub:
                  proc = run_script(base_env(stub), "--json", "messages", "list", "--channel", "C123")
              self.assertEqual(proc.returncode, 0, proc.stderr)
              self.assertEqual(load_json(proc)["messages"][0]["text"], "hello from tests")
      
          def test_threads_list(self):
              with StubSlackServer() as stub:
                  proc = run_script(base_env(stub), "--json", "threads", "list",
                                    "--channel", "C123", "--ts", "1712345678.000001")
              self.assertEqual(proc.returncode, 0, proc.stderr)
              self.assertEqual(load_json(proc)["thread_ts"], "1712345678.000001")
      
          def test_search(self):
              with StubSlackServer() as stub:
                  proc = run_script(base_env(stub), "--json", "search", "messages", "--query", "hello")
              self.assertEqual(proc.returncode, 0, proc.stderr)
              data = load_json(proc)
              self.assertEqual(data["total_matches"], 1)
              self.assertEqual(data["matches"][0]["channel"], "C123")
      
          def test_files_list(self):
              with StubSlackServer() as stub:
                  proc = run_script(base_env(stub), "--json", "files", "list")
              self.assertEqual(proc.returncode, 0, proc.stderr)
              self.assertEqual(load_json(proc)["files"][0]["id"], "F1")
      
          def test_missing_token_errors_cleanly(self):
              env = dict(os.environ)
              env.pop("SLACK_TOKEN", None)
              env["SLACK_API_BASE"] = "http://127.0.0.1:1/"
              proc = run_script(env, "--json", "channels", "list")
              self.assertEqual(proc.returncode, 1)
              self.assertIn("SLACK_TOKEN", proc.stdout)
      
          def test_webhook_verify_valid_signature(self):
              import hashlib
              import hmac
              import time
              secret = "signing-secret"
              body = b'{"event": {"type": "message"}}'
              timestamp = str(int(time.time()))
              base = f"v0:{timestamp}:".encode() + body
              signature = "v0=" + hmac.new(secret.encode(), base, hashlib.sha256).hexdigest()
              body_file = ROOT / "tests" / "webhook-body.json"
              body_file.write_bytes(body)
              try:
                  proc = run_script(dict(os.environ), "--json", "webhook", "verify",
                                    "--body-file", str(body_file), "--signature", signature,
                                    "--timestamp", timestamp, "--secret", secret)
              finally:
                  body_file.unlink(missing_ok=True)
              self.assertEqual(proc.returncode, 0, proc.stderr)
              self.assertTrue(load_json(proc)["verified"])
      
          def test_webhook_verify_rejects_bad_signature(self):
              import time
              body_file = ROOT / "tests" / "webhook-body.json"
              body_file.write_bytes(b'{"event": {}}')
              try:
                  proc = run_script(dict(os.environ), "--json", "webhook", "verify",
                                    "--body-file", str(body_file),
                                    "--signature", "v0=" + "0" * 64,
                                    "--timestamp", str(int(time.time())), "--secret", "signing-secret")
              finally:
                  body_file.unlink(missing_ok=True)
              self.assertEqual(proc.returncode, 1)
              self.assertIn("does not match", proc.stdout)
      
          def test_webhook_verify_rejects_stale_timestamp(self):
              stale = str(int(__import__("time").time()) - 600)
              body_file = ROOT / "tests" / "webhook-body.json"
              body_file.write_bytes(b"{}")
              try:
                  proc = run_script(dict(os.environ), "--json", "webhook", "verify",
                                    "--body-file", str(body_file),
                                    "--signature", "v0=" + "0" * 64,
                                    "--timestamp", stale, "--secret", "s")
              finally:
                  body_file.unlink(missing_ok=True)
              self.assertEqual(proc.returncode, 1)
              self.assertIn("replay window", proc.stdout)
      
          def test_read_only_contract_no_write_opens(self):
              source = SCRIPT.read_text()
              writes = [line for line in source.splitlines()
                        if line.strip().startswith("open(") and ("'w'" in line or '"w"' in line)]
              self.assertEqual(writes, [], "script must never open files in write mode")
      
      
      if __name__ == "__main__":
          unittest.main()
      
  • README.md 3.6 KB
    # Slack — Read and Post to Slack from the Terminal
    
    Operate a Slack workspace without leaving your terminal or your agent's tool loop: list channels, read messages, follow threads, search history, find files, and verify that inbound webhook events are authentic.
    
    ## Why Install This Skill
    
    Most agents have no way to answer the basic question "what did the team say about X in Slack?" — so they guess, or you paste screenshots. This skill gives your agent a real, bounded read path into a workspace (channels, messages, threads, search, files) plus a safe write path: sending a message is a guarded mutation that requires a preview and an explicit confirmation, so the agent can triage and answer without ever posting by accident.
    
    It ships `slack-cli`, a small Python script that speaks the Slack Web API with no third-party dependencies. Reads are capped (`--limit`), output is clean JSON for the agent or readable text for you, and `--help` works with no token and no network. Webhook verification is built in: any event endpoint can prove a request really came from Slack using the standard HMAC-SHA256 signature check.
    
    ## What You Get
    
    | Directory | Purpose |
    |---|---|
    | `SKILL.md` | Agent-facing operating contract, mutation gates, and verification boundaries |
    | `references/` | Dated source index and a web API operations reference (methods, scopes, pagination, webhook verification) |
    | `scripts/slack-cli` | Bounded, stdlib-only CLI: channels, messages, threads, search, files, webhook verify; `--json`, `--limit`, sends gated by `--dry-run`/`--yes` |
    | `tests/` | 16 deterministic tests against a stub Slack API, covering the mutation gate and read-only contract |
    | `evals/evals.json` | Six output-quality evaluation cases for agent runs |
    
    ## Quick Start
    
    ```bash
    # Help works with no token and no network
    slack/scripts/slack-cli --help
    
    # Find the channel ID from a name (reads are capped at --limit)
    SLACK_TOKEN=xoxb-... slack/scripts/slack-cli --json --limit 10 channels list
    
    # Read the latest messages in a channel
    SLACK_TOKEN=xoxb-... slack/scripts/slack-cli --json messages list --channel C12345
    
    # Follow a thread (parent ts from the message list)
    SLACK_TOKEN=xoxb-... slack/scripts/slack-cli --json threads list --channel C12345 --ts 1712345678.000001
    
    # Search history, bounded
    SLACK_TOKEN=xoxb-... slack/scripts/slack-cli --json search messages --query "incident"
    
    # Send only with a preview first, then explicit confirmation
    SLACK_TOKEN=xoxb-... slack/scripts/slack-cli messages send --channel C12345 --text "on it" --dry-run
    SLACK_TOKEN=xoxb-... slack/scripts/slack-cli messages send --channel C12345 --text "on it" --yes
    
    # Verify an inbound webhook before trusting it
    slack/scripts/slack-cli webhook verify --body-file body.json --signature "v0=..." --timestamp 1712345678
    ```
    
    ## Triggers
    
    Load this skill for `slack` operations: "what was said in #channel", reading or listing messages and channels, following or summarizing threads, searching Slack history, finding shared files, posting a message or thread reply (with confirmation), and verifying `X-Slack-Signature` webhook events. Do not load it for building Slack apps or bots, workspace administration (user provisioning, org settings), or other chat platforms like Discord or Teams.
    
    ## Requirements
    
    - Python 3.9+ for `slack-cli` (stdlib only; `--help` and webhook verification need nothing else).
    - A Slack bot or user token (`SLACK_TOKEN`) with the scopes the read needs: `channels:read`, `channels:history`, `groups:read`, `groups:history`, `search:read`, `files:read`, and `chat:write` for sending. For `webhook verify`, the app signing secret (`SLACK_WEBHOOK_SECRET`).
    - Network access to `api.slack.com` for live reads and sends.
    
  • SKILL.md 9.3 KB
    ---
    name: slack
    description: >-
      Operate Slack workspaces from a terminal or agent: list channels, read
      messages, follow threads, search message history, list files, and verify
      inbound webhook signatures — with a bundled slack-cli script that is
      read-only by default and gates every send behind a --dry-run/--yes
      confirmation. Use when an agent needs to read or post Slack data, triage
      incidents, or answer questions about what was said in a workspace. Do not
      use for building Slack apps or bots (that is application development) or
      workspace administration like user provisioning and org settings (that is
      the Slack admin console).
    license: MIT
    compatibility: >-
      The bundled slack-cli script runs on Python 3.9+ with only the standard
      library. --help, channel/message/thread/search/file reads, and webhook
      signature verification need no network; live reads require a Slack bot/user
      token with the right scopes and network access to api.slack.com.
    metadata:
      source: https://api.slack.com/web
      source_index: references/00-source-index.md
      research_checked: "2026-08-03"
    ---
    
    # Slack Operations
    
    Use this skill to read and, with explicit confirmation, write Slack data through the Slack Web API: channels, messages, threads, search, files, and webhook signature verification. This is a **tool skill** for the Slack platform. Building Slack apps and bots is application development; workspace administration (user provisioning, org-level settings, SSO) lives in the Slack admin console. This skill owns the everyday agent workflow: knowing what was said, finding it later, and posting a reply when a human confirms.
    
    ## Operating contract
    
    1. **Read-only discovery before any mutation.** List channels, read history, follow threads, search, and list files freely. The bundled `slack-cli` script makes reads without writing anything.
    2. **Confirm the target, scope, and rollback path before acting.** Sending a message or replying in a thread changes shared workspace state visible to everyone: it requires an explicit human directive naming the channel, plus `--dry-run` preview and `--yes` confirmation through `slack-cli`. There is no "unsend" for team members who already read it.
    3. **Respect bounded reads.** Slack paginates; never page past what the task needs. `slack-cli --limit N` caps every listing, and responses summarize records rather than dumping raw payloads.
    4. **Verify webhooks before trusting them.** Any handler that accepts Slack events must verify `X-Slack-Signature` and `X-Slack-Request-Timestamp` against the app signing secret, or anyone who can reach the endpoint can forge events. `slack-cli webhook verify` does this check.
    5. **Keep evidence bounded.** Quote short message excerpts and IDs; never paste full threads, tokens, or file contents into chat.
    
    ## The slack-cli script
    
    `scripts/slack-cli` is an agent-first, stdlib-only CLI over the Slack Web API. It covers the full issue scope: messages, channels, threads, search, files, and webhook verification.
    
    ```bash
    slack/scripts/slack-cli --help                          # no token or network needed
    slack/scripts/slack-cli --json --limit 10 channels list
    slack/scripts/slack-cli --json messages list --channel C12345
    slack/scripts/slack-cli --json threads list --channel C12345 --ts 1712345678.000001
    slack/scripts/slack-cli --json search messages --query "incident"
    slack/scripts/slack-cli --json files list --limit 5
    slack/scripts/slack-cli messages send --channel C12345 --text "on it" --dry-run   # preview
    slack/scripts/slack-cli messages send --channel C12345 --text "on it" --yes       # confirmed
    slack/scripts/slack-cli webhook verify --body-file body.json --signature "v0=..." --timestamp 1712345678
    ```
    
    Exit codes: 0 success, 1 API error or failed verification, 2 usage error. Sends are guarded: without `--dry-run` or `--yes` the script refuses with exit 1 and never calls the API. Reads are bounded by `--limit` (default 20, max 100).
    
    ## Operating loop
    
    1. **Scope the workspace surface**: which channel(s) are relevant, what the question is (what was said, who said it, when), and whether any action is a mutation.
    2. **Read with bounds**: `channels list` to find IDs, `messages list`/`threads list` for history, `search messages` for cross-channel discovery. All read-only.
    3. **Triage the answer**: map the question to evidence (thread replies for context, search for the exact phrase, files list for shared artifacts).
    4. **Act with confirmation**: only a human directive to send, previewed with `--dry-run` and confirmed with `--yes`.
    5. **Verify**: confirm the posted message `ts`/channel in the response, or for webhooks confirm the signature check result before trusting the event.
    
    ## Messages, channels, threads
    
    - **Channels** (`conversations.list`): public and private channels, archived state, member counts. Channel IDs (`C...`) are the stable key for every other call — resolve names to IDs before use.
    - **Messages** (`conversations.history`): newest-first history in a channel, one page at a time. Message records carry `ts` (the ID), `user`, and `text`. Use `--cursor` from the response metadata to page deliberately.
    - **Threads** (`conversations.replies`): replies keyed by the parent `ts`; the parent message is the first result. Thread replies keep `thread_ts` set to the parent.
    - **Sending** (`chat.postMessage`): the only mutation in this skill's surface. Always preview with `--dry-run`, confirm with `--yes`, and pass `--thread-ts` to reply in a thread instead of starting a new message. Verify the returned `ts` and channel.
    
    ## Search and files
    
    - **Search** (`search.messages`): full-text search across visible history with Slack's search syntax (`from:`, `in:`, quoted phrases, `before:`/`after:`). Results include the matching channel; `total` tells you how many matches exist while `matches` stays bounded by `--limit`.
    - **Files** (`files.list`): files shared in the workspace, filterable by channel or user, with permalinks and sizes. Downloading file *content* is out of scope for the CLI (bounded reads); use it to find the file, then fetch the permalink with an authenticated request when a human asks for the content.
    
    ## Webhook verification
    
    Slack signs every HTTP request to your event/command/interactivity endpoints. To verify:
    
    1. Take the raw request body (exactly as received — do not re-encode).
    2. Check `X-Slack-Request-Timestamp` is within ~5 minutes of now (replay protection).
    3. Compute `v0=HMAC_SHA256(signing_secret, "v0:" + timestamp + ":" + body)` and compare with `X-Slack-Signature` using a constant-time comparison.
    4. Reject with 401 if the timestamp is stale or the signature mismatches.
    
    `slack-cli webhook verify --body-file body.json --signature "v0=..." --timestamp <unix>` runs exactly this check against `SLACK_WEBHOOK_SECRET` (or `--secret`) and reports a constant-time-verified result. Always verify before trusting event payloads — unverified webhook endpoints accept forged events.
    
    ## Reference routing
    
    | Load when | Reference |
    |---|---|
    | Sources, scope tables, refresh procedure | `references/00-source-index.md` |
    | API method surface, pagination, scopes, and webhook verification details | `references/01-web-api-operations.md` |
    
    ## Included artifacts
    
    - `scripts/slack-cli`: bounded, stdlib-only CLI (messages, channels, threads, search, files, webhook verify; `--json`; `--limit`; send gated by `--dry-run`/`--yes`).
    - `tests/test_slack_cli.py`: 16 deterministic tests against a stub Slack API, including the mutation gate and the read-only contract.
    - `references/`: dated source index + web API operations reference.
    - `evals/evals.json`: six output-quality evaluation cases for agent runs.
    
    ## Verification boundary
    
    | Claim | Minimum evidence |
    |---|---|
    | A channel exists and its name | `slack-cli channels list --json` returns it with its `C...` ID |
    | A message was sent | `slack-cli messages send --yes` returns the new `ts` and channel, and `messages list` shows it |
    | A search found matches | `slack-cli search messages --query "..." --json` returns `total_matches` with bounded matches |
    | A webhook is authentic | `slack-cli webhook verify` exits 0 with `verified: true` for the exact body/signature/timestamp |
    | A file exists | `slack-cli files list --json` returns its `F...` ID and permalink |
    
    ## Hard boundaries
    
    - Never send a message or thread reply without a human directive, `--dry-run` preview, and `--yes` confirmation — Slack posts are public, durable, and unreadable-back.
    - Never trust an inbound webhook without signature and timestamp verification.
    - Never page reads past `--limit`; never dump full threads, tokens, or file contents into chat.
    - This skill operates the Slack Web API. It does not build Slack apps (application development), manage users/org settings (admin console), or cover alternative channels platforms (that is their own tooling).
    
    ## When not to use
    
    - **Building Slack apps or bots** (Block Kit, Bolt, slash-command apps, OAuth flow design) — that is application development; see [backend-engineering](../backend-engineering/SKILL.md) for service design.
    - **Workspace administration** (user provisioning, deprovisioning, org-level settings, SSO/SAML, data exports at the org level) — that is the Slack admin console, not the Web API.
    - **Other team-chat platforms** (Discord, Mattermost, Teams) — each has its own tooling; this skill covers Slack only.
    - **Company-wide policy on messaging or channel governance** — that is organizational policy, not an API operation.
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related