Skip to content

Releases: 0xCVH/paperclip-plugin-slack-socket

v0.7.0 — Slack user allowlist

Choose a tag to compare

@0xCVH 0xCVH released this 01 Aug 19:55

Adds allowedSlackUserIds: a list of Slack user IDs permitted to interact with the bot. A user not on the list is ignored silently — no reply, no ephemeral, no reaction handling, no approval decision. Nothing at all.

Enforcement sits at the gateway wiring, so it covers every inbound surface: DMs, @mentions, thread replies, the /paperclip slash command, approval button clicks, and ask_human emoji/thread responses. Denials are logged and counted (slack.access.denied, tagged by surface).

An empty list (the default) means the allowlist is disabled and any workspace member can use the bot — so upgrading changes nothing until you populate it. Slack user IDs come from a member's profile → "Copy member ID" (e.g. U01ABC2DEF3).

Matching trims whitespace and ignores case; a blank user id against a non-empty list is denied (fail closed).

177 tests, including a per-surface denial test for each of the five paths — each verified to fail if its guard is removed.

🤖 Generated with Claude Code

v0.6.0 — extract the agent's reply from <slack_reply> tags

Choose a tag to compare

@0xCVH 0xCVH released this 01 Aug 19:32

v0.5.0 asked agents not to narrate their reasoning. Agents then narrated about the preamble — quoting the instruction back and explaining that they should follow it — before answering. Prompt nudging alone is not enough, because the model writes its planning as ordinary output text and the host's done event carries the whole turn as finalText.

Observed output made line/paragraph heuristics unsafe too: the real answer was jammed onto the end of the last reasoning sentence with no separator (…in a natural, conversational way.Hey! What's up?).

v0.6.0 asks the agent to wrap its reply in <slack_reply>…</slack_reply> and posts only that content. This is not a heuristic — it's an explicit marker the agent controls:

  • last complete tag pair wins (a model may echo the instruction, tags and all, before its real reply);
  • an unclosed opening tag falls back to everything after it;
  • no tags at all → the full text is posted, exactly as before, so an agent that ignores the instruction is no worse off than in 0.5.0.

Extraction applies only to the final reply, not to live streamed partials (which can legitimately be mid-tag). If you customise chatPromptPreamble, keep the tag instruction or extraction won't apply.

163 tests.

🤖 Generated with Claude Code

v0.5.0 — frame Slack turns as conversation

Choose a tag to compare

@0xCVH 0xCVH released this 01 Aug 19:22

Agents woken by this plugin were replying with their heartbeat deliberation before the actual answer — narrating the wake payload, the execution contract, and what they "should" do, then finally answering. That text is the agent's canonical final reply, so the plugin cannot strip it: any heuristic that guesses which sentences are "thinking" would eventually delete real content.

What the plugin does control is the prompt it sends. Paperclip's heartbeat scaffolding frames every wake as autonomous work execution, which is what pushes agents into that narration. v0.5.0 prepends a short framing preamble to each Slack chat turn:

You are replying to a person in a Slack thread. Answer them directly and conversationally, in your own voice. Do not narrate your reasoning, restate the wake payload or execution contract, or list what you should do — just reply. Keep it concise and readable as a chat message.

Configurable via the new chatPromptPreamble setting; set it to an empty string to send the user's message verbatim with no framing. Mention-stripping still happens before framing.

Pairs with v0.4.1, which started passing reason: "slack_chat_message" so the agent is no longer woken with reason: unknown.

154 tests.

🤖 Generated with Claude Code

v0.4.0 — post only the agent's final reply by default

Choose a tag to compare

@0xCVH 0xCVH released this 01 Aug 19:05

Fixes a content leak. The plugin streamed every raw stdout chunk from the agent adapter into the Slack thread as it arrived. The SDK guarantees only that the done event carries "the canonical final user-facing assistant reply" — chunks are raw adapter output. With the claude_local adapter that meant two things were posted into Slack before being replaced by the real answer:

  • agent-runtime housekeeping ([paperclip] ACPX session "acpx:v2:…" does not match the current agent/cwd/mode/runtime identity…), and
  • the model's internal reasoning — chain-of-thought visible to everyone in the channel.

Now: only the agent's final reply is posted. The _Thinking…_ placeholder stays until the reply is ready, then is replaced (still with Markdown→mrkdwn conversion and long-message splitting).

Live streaming is available as an explicit opt-in via the new streamPartialReplies setting, whose description states the caveat. With it on, lines matching [paperclip] … are filtered out; internal reasoning is not filtered, which is precisely why the default is off.

148 tests.

🤖 Generated with Claude Code

v0.3.0 — fix Slack socket created inside the host invocation context

Choose a tag to compare

@0xCVH 0xCVH released this 01 Aug 18:51

Fixes all Slack chat failing with the worker referenced a missing, expired, or unknown invocation scope.

The bug (introduced in 0.2.0). Node's AsyncLocalStorage propagates into sockets created inside it — every later callback from that socket runs in the captured store. The plugin SDK wraps host→worker calls that carry an invocation (like configChanged) in that storage, and echoes the current store's invocation id on every worker→host call. 0.2.0 moved Bolt gateway creation out of setup() and into onConfigChanged, so the Slack WebSocket was created inside the configChanged invocation context. Every subsequent Slack event echoed the id of an invocation the host had long since completed, and the host denied the call.

The fix. Config applies now run on a pump loop started in setup(), whose continuations execute in the clean context — so the socket is created outside any invocation. onConfigChanged enqueues and returns a promise that resolves once the config is applied. A regression test asserts, using real AsyncLocalStorage, that the gateway is constructed with an undefined store.

Also: a first-time configuration no longer needs a plugin restart.

142 tests.

🤖 Generated with Claude Code

v0.2.0 — proactive company scope fixes

Choose a tag to compare

@0xCVH 0xCVH released this 01 Aug 18:20

Fixes a bug that prevented the plugin from starting at all on a real Paperclip instance.

The bug. This plugin is a proactive plugin: its work runs in Socket Mode callbacks, timers and setup() — outside any host-issued invocation. In that context Paperclip gates config.get and secrets.resolve on company scope, resolving it only from an explicit companyId in the call params. The plugin passed it on neither, so Test Connection failed with is not allowed to perform "secrets.resolve": company context is required and the Socket Mode connection never opened.

Fixes

  • Config now comes from the host's onConfigChanged push (delivered at startup and on every save, with no RPC) and is cached in the worker; ctx.config.get() is no longer called on any runtime path. Config changes apply live, without a worker restart.
  • Every ctx.secrets.resolve call passes { companyId }.
  • instanceConfigSchema secret-ref fields accept the { type: "secret_ref", secretId, version } object the settings form stores (also in 0.1.1).
  • Single-tenant binding: one worker process is shared by every company that configures a plugin, so the plugin now binds to the first company it is configured for and fails closed on a config for a different company (claimed synchronously, so overlapping pushes can't both bind). Event subscriptions are filtered by that company.
  • Config is fully validated and both secrets resolved before any teardown, so a bad save can't drop a working connection.

134 tests.

🤖 Generated with Claude Code

v0.1.0

Choose a tag to compare

@0xCVH 0xCVH released this 01 Aug 13:13

Initial release — published to npm as paperclip-plugin-slack-socket.

Slack Socket Mode plugin for Paperclip: per-thread agent chat with streamed replies and Markdown→mrkdwn conversion, configurable notifications (threaded issue updates), interactive approvals, ask_human agent tool, /paperclip slash command, event dedup, token log-redaction. No public URL required.

🤖 Generated with Claude Code