Slack watch
A push subscription to replies in specific Slack threads — DM threads and channel threads both. The script holds one Socket Mode WebSocket, acks every event, and prints one line per new reply — nothing else. It knows nothing about tasks or questions; it filters a thread list you give it and reports. The caller (a session, a skill) decides what a reply means and reads it via the Slack MCP (slack_read_thread).
macOS. Three tokens live in the Keychain; the script reads them and never prints them. It does not print message content or event payloads — the underlying message.im feed is a DM firehose. DMs are watched with the developer's user token; channels are watched with the bot token, which only sees channels the Honk bot was invited into (so no firehose there). A single Socket Mode connection carries both the bot and user events.
1. Ready
From this skill's directory (the folder that contains this SKILL.md):
scripts/slack-watchis executable.- Keychain has all three services, metadata only (no
-w):security find-generic-password -s slack-orchestrator-app-tokensecurity find-generic-password -s slack-orchestrator-user-tokensecurity find-generic-password -s slack-orchestrator-bot-token
- Node 22+ is on
PATH(node --version) — the script uses the built-inWebSocketandfetch, no npm deps.
If 2 fails: give the user Setup below and stop. Do not store the tokens yourself.
Local session only — the Keychain and the socket both live on this Mac. If you are a cloud agent, stop and tell the developer to run this in local Claude Code or Cursor.
Done when: every check passes, or the run has stopped for setup (or cloud).
2. Run
Each watched thread is one CHANNEL:THREAD_TS argv item — the channel id (a DM is D…, a channel C…, a private group G…) and the parent ts, both from the slack_send_message / permalink you already hold. The script routes each by its id prefix; channel threads only report if the Honk bot is a member of that channel.
Launch so each stdout line wakes this session. Use the recipe whose tools you have.
Claude Code
Monitor with persistent: true. The sandbox cannot read the Keychain or reach slack.com — allow both on that Monitor.
Monitor(
command: "scripts/slack-watch C0123ABCD:1700000000.000200 D0456WXYZ:1700000100.000700",
description: "Slack reply watcher",
persistent: true,
)
Give the absolute path to scripts/slack-watch if Monitor's working directory is not this skill's directory.
Cursor (local IDE)
Background Shell (block_until_ms: 0) with notify_on_output and required_permissions: ["all"] (Keychain + slack.com). Cursor floors notification debounce at 5s — two NEW lines in one burst may arrive as one wake; on wake, read the terminal output and handle every NEW line, not only the last.
Shell(
command: "<absolute>/scripts/slack-watch C0123ABCD:1700000000.000200 D0456WXYZ:1700000100.000700",
description: "Slack reply watcher",
block_until_ms: 0,
required_permissions: ["all"],
notify_on_output: { pattern: "^NEW |^RECONNECTED", reason: "Slack reply" },
)
Use the absolute path. Smoke-check the terminal output file once so a failed start is not silent.
Output lines
NEW channel=<C…> thread_ts=<ts> ts=<ts>— a new reply landed (or was missed during downtime and reconciled on reconnect). Read it withslack_read_threadon thatthread_ts. Our own messages — the developer's and the Honk bot's, including agent follow-ups posted as either — and the thread's pre-existing history are not reported.RECONNECTED— the socket re-opened after a drop; a catch-up for every watched thread has just run, so anyNEWlines around it are the replies missed while it was down.
Diagnostics (auth or reconnect failures) go to stderr — Claude's Monitor output file, or Cursor's terminal file; Read it if the watcher seems quiet.
Changing the watched set
All state lives in the process (thread list, per-thread last-seen ts, event_id dedupe). There is no state file. To add or remove a thread, stop and relaunch with the new argument list.
- Claude Code:
TaskStopon that Monitor, then launch again. - Cursor: kill the PID in that shell's header,
AwaitShellso the completion ping is consumed, then launch again.
Re-spawning re-baselines to "from now" — earlier replies are already in the session transcript, so nothing is double-reported.
Done when: the watcher is running and reporting, or the run has stopped for setup.
Setup
One private Slack app in the Blockscout workspace (api.slack.com/apps → your app), carrying three tokens. Enable the bot user Honk 🪿 (honk) under App Home first — it is both the channel-reading identity and the disclosure identity.
- App-level token (
xapp-), scopeconnections:write— created under Socket Mode → toggle on → generate token. Opens the WebSocket. - User token (
xoxp-), User Token Scopesim:history+chat:write. Under Event Subscriptions → on behalf of users, subscribemessage.imonly — notmessage.channels/message.groups(that is a firehose on a user token). Reads DM replies and posts in the developer's own voice. - Bot token (
xoxb-), Bot Token Scopeschannels:history(addgroups:historyfor private channels) +chat:write. Under Event Subscriptions → bot events, subscribemessage.channels(andmessage.groupsfor private channels). Bot events fire only for channels the bot is a member of, so invite Honk to each question channel (/invite @Honk) — that is what scopes the channel feed instead of firehosing.
Store all three in the Keychain yourself (the script never writes them):
security add-generic-password -U -s slack-orchestrator-app-token -a "$USER" -w # prompts for xapp-…
security add-generic-password -U -s slack-orchestrator-user-token -a "$USER" -w # prompts for xoxp-…
security add-generic-password -U -s slack-orchestrator-bot-token -a "$USER" -w # prompts for xoxb-…