fix(slack): delivery correctness — threading, MCP blocklist, context injection#527
Open
leighstillard wants to merge 4 commits intochenhg5:mainfrom
Open
fix(slack): delivery correctness — threading, MCP blocklist, context injection#527leighstillard wants to merge 4 commits intochenhg5:mainfrom
leighstillard wants to merge 4 commits intochenhg5:mainfrom
Conversation
Adds a structured "## Slack Message Context" block that the Slack
platform populates with channel_id, channel_name, user_id, user_name,
message_ts, and (when present) thread_ts. The block is threaded through
core.Message.PlatformContext, stored on the engine's queued pending
messages, and prepended to the agent prompt by buildSenderPrompt.
Motivation: without this, the agent does not know which Slack
channel/thread the current conversation lives in, so MCP tool calls
back into Slack (reply, react, post to canvas, etc.) cannot be
routed correctly and thread-reply semantics fall back to
channel-level posts. Providing the context as a structured block up
front lets the agent address the right channel and preserve threading
without having to infer it.
Wiring:
- core/message.go: new PlatformContext field on core.Message.
- core/engine.go:
- interactiveState.pendingMessages entries gain a platformContext
field and the enqueue/dequeue sites copy it through.
- buildSenderPrompt now takes (content, userID, platform,
sessionKey, platformContext). The platform-context block is
prepended unconditionally when present, then the existing
`[cc-connect sender_id=... platform=... chat_id=...]` header is
appended on top of that when injectSender is enabled. If neither
applies, the original content is returned unchanged.
- All three buildSenderPrompt call sites (fresh message, queued
replay after session crash, continue-after-shutdown) now pass
the platformContext through.
- platform/slack/slack.go:
- New slackMessageContext() helper builds the structured block.
- Populated at the three Slack Message paths where we construct a
core.Message: real message events, app_mention events, and slash
commands.
- Channel/user name resolution is hoisted out of the struct literal
into named locals so both the ChatName and the PlatformContext
block can reuse the same resolved values without double lookups.
The feature is additive. No existing call paths change behaviour when
PlatformContext is empty — non-Slack platforms keep working unchanged.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2a99696 feat(slack): inject channel/thread context block added a fifth parameter (platformContext string) to buildSenderPrompt and updated every production call site, but left the four test call sites in core/engine_test.go unchanged: core/engine_test.go:6402 TestBuildSenderPrompt_Enabled core/engine_test.go:6413 TestBuildSenderPrompt_Disabled core/engine_test.go:6423 TestBuildSenderPrompt_EmptyUserID core/engine_test.go:6462 TestBuildSenderPrompt_DifferentPlatforms The feat/run-as-user branch therefore had a broken test suite for a short period — `go build` still succeeded because the three production call sites in core/engine.go were all updated, so the live binary was fine, but `go test ./core/...` failed with "not enough arguments". Pass an empty platformContext "" at each test call site. The existing test assertions about the `[cc-connect sender_id=... chat_id=...]` header remain valid because buildSenderPrompt's new prefix behaviour is a no-op when platformContext is empty. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Platform.Send previously posted un-threaded messages, while Reply already threaded correctly via slack.MsgOptionTS. The asymmetry was invisible in most platforms but exposed on Slack specifically because the core message path (core/engine.go, core/streaming.go, core/progress_compact.go, core/relay.go) overwhelmingly calls Send rather than Reply for progress, tool-use events, and queued message replay. The net effect was that the agent's final reply would thread (via Reply), but every intermediate progress update, tool-call announcement, and streamed partial would land un-threaded in the main channel — the "giant mess" failure mode where each new thought_balloon and wrench spam the channel as standalone messages. Lift the threading logic from Reply into Send: when rc.timestamp is present, append slack.MsgOptionTS before PostMessageContext. For genuinely standalone posts that don't have a triggering ts (slash commands, ReconstructReplyCtx reply contexts), timestamp is empty and Send falls back to its previous non-threaded shape. In cc-connect's normal Slack flow every triggering user message has its ts captured into replyContext.timestamp at the incoming-event site, so this effectively threads the entire conversation — final reply, progress, tool noise, errors, notifications — under the user's original message. One thread per conversation. Main channel stays quiet. Parallel conversations in the same channel naturally fork into separate threads without any per-session book-keeping beyond the session key that already carries the channel + user, which also sets up multi-thread sessioning as a later improvement. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…efault The claude.ai Slack, Gmail, and Google Calendar MCP connectors are cloud-managed and tied to the authenticated claude.ai account's OAuth token rather than to the cc-connect bot identity. When the agent calls one of their tools, the resulting API call happens AS the account owner — so (for Slack) messages post with "Sent using @claude" attributed to the human account instead of the cc-connect bot, and cc-connect's own delivery path (thread_ts, progress streaming, tool suppression, etc.) is bypassed entirely. This is especially bad in sandboxed deployments where several Unix users share one claude.ai account via file-copied credentials: every sandbox user inherits access to every connector authorised on the shared account, even though the human only intended to authorise the connector for their own claude.ai use. cc-connect owns message delivery on every platform it bridges to, and those connectors are a foot-gun nobody should be firing from inside a cc-connect session. Disallow them unconditionally in the claudecode agent's spawn args. Per-project disallowed_tools config still applies and can extend this list, but cannot override the baseline. Observed failure mode that motivated the fix: a recent credentials refresh gave the sandbox user a fresh OAuth token that (unlike the stale one it replaced) had the claude.ai Slack connector authorised. The next Slack session started routing replies through mcp__claude_ai_Slack__slack_send_message, posting them as the human user with "Sent using @claude", in the main channel, outside any thread, with every ":thought_balloon:" and ":wrench:" tool call landing as separate messages. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
3 tasks
chenhg5
approved these changes
Apr 9, 2026
Owner
chenhg5
left a comment
There was a problem hiding this comment.
LGTM. Four distinct correctness fixes — the MCP blocklist (Slack/Gmail/Calendar) is the most security-sensitive, and the threading fix via MsgOptionTS addresses a real delivery bug. buildSenderPrompt signature change is backwards-compatible (new param is added, all call sites updated). All CI pass.
PR: #527
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Four interlocking fixes to how cc-connect delivers messages on the Slack platform. Individually they look like small edges; together they close the failure mode where a cc-connect session would (a) post as the human user's claude.ai account instead of the bot, (b) drop intermediate events into the main channel instead of a thread, and (c) have the in-progress PlatformContext feature blocked behind a broken CI.
Commits
feat(slack): inject channel/thread context block into agent promptAdds a structured `## Slack Message Context` block (channel_id, channel_name, user_id, user_name, message_ts, thread_ts) to `core.Message.PlatformContext`. Threaded through `buildSenderPrompt` and populated by the Slack platform at all three message sites (real messages, app_mentions, slash commands). Lets the agent address the right channel/thread when making Slack-side decisions.
fix(test): update buildSenderPrompt call sites for PlatformContext paramUnblocks CI: the PlatformContext commit added a fifth parameter to `buildSenderPrompt` and updated every production call site, but missed the four test call sites in `core/engine_test.go`. Pass an empty `platformContext` in each test — the existing assertions about the sender header remain valid because the new prefix path is a no-op when platformContext is empty.
feat(slack): thread Send replies off the triggering message ts`Platform.Send` previously posted un-threaded messages while `Reply` already threaded correctly via `slack.MsgOptionTS`. The asymmetry was invisible on most platforms but exposed on Slack because the core message path (`core/engine.go`, `core/streaming.go`, `core/progress_compact.go`, `core/relay.go`) overwhelmingly calls `Send` rather than `Reply` for progress and tool-use events. The result: the final agent reply would thread, but every intermediate progress update, tool-call announcement, and streamed partial would land un-threaded in the main channel. Lift the threading logic from `Reply` into `Send`: when `rc.timestamp` is present, append `slack.MsgOptionTS` before `PostMessageContext`.
feat(claudecode): disable claude.ai cloud-managed MCP connectors by defaultThe claude.ai Slack, Gmail, and Google Calendar MCP connectors are cloud-managed and tied to the authenticated claude.ai account's OAuth token. When the agent calls one of their tools, the API call happens AS the account owner — so Slack messages post with "Sent using @claude" attributed to the human account, not the cc-connect bot, and cc-connect's own delivery path (thread_ts, progress streaming, tool suppression) is bypassed entirely. Disallow them unconditionally in the claudecode agent's spawn args via `--disallowedTools`. Per-project config still applies and can extend this baseline but cannot override it.
Test plan
Notes
Each commit is independently revertible. The order matters: the test fix must land no later than the PlatformContext commit to keep CI green on any intermediate state.
🤖 Generated with Claude Code