Slack notification onboarding
August 10, 2026 · View on GitHub
This is the managed Slack Socket Mode notification adapter. It is an SDK client: local GJC sessions continue to own loopback SDK endpoints, and Slack provides a per-session message thread for notifications and replies.
Prerequisites
Create a Slack app in the target workspace, enable Socket Mode, and create an app-level token with the Socket Mode connection scope. Install the app in the workspace and invite it to the selected channel. Configure only the scopes and event subscriptions the adapter needs:
chat:writeto post session roots, replies, and closure markerschannels:historyfor a public channel, or the corresponding history scope for the channel type in use- the message event subscription for the selected channel type
- Socket Mode enabled for Events API delivery
Keep the selected channel private to people authorized to see local session metadata. Do not add broad workspace scopes or use an app token for ordinary Web API calls.
Configure the adapter
gjc notify setup slack is non-interactive. It requires these flags:
--slack-bot-token--slack-app-token--slack-workspace-id--slack-channel-id--slack-authorized-user-idfor the single Slack user authorized to submit replies and/sdkcommands
Without --slack-authorized-user-id, the adapter remains outbound-only: every inbound envelope is acknowledged but denied before it can create a durable claim or reach an SDK endpoint. The user ID is an identifier, not a secret. It also accepts --redact. Provide secret values from an approved local secret mechanism, not shell history, committed configuration, tickets, screenshots, or chat. Setup writes:
notifications.enabled = truenotifications.slack.enabled = true(durable desired intent)notifications.slack.botTokennotifications.slack.appTokennotifications.slack.workspaceIdnotifications.slack.channelIdnotifications.slack.authorizedUserIdwhen configurednotifications.redact = truewhen requested
gjc notify status reports Slack completeness, repair/quarantine state, desired intent, effective enablement, destination identifiers, and masked token values. It is status output, not a credential recovery mechanism. A successful durable save is not rolled back when later daemon activation fails; the command reports the saved-but-runtime-degraded outcome and exits nonzero. In /settings, bot/app secret edits are explicit keep, replace, or remove; removing either required token turns Slack desired intent off without changing Telegram, Discord, or the global master.
Socket Mode, threads, and resume
The daemon validates the configured workspace, channel, and paired user before durably claiming an inbound effect or sending its Socket Mode acknowledgement. The durable claim records the paired actor identity, replay identity, protected-effect reference, and captured endpoint generation; it never records Socket Mode cursors, endpoint tokens, or message bodies. Rejected, bot-authored, unauthorized, and already-claimed envelopes are acknowledged without an SDK endpoint call.
Acknowledgement latency is therefore bounded by local durable-claim work rather than SDK availability or command execution. After the ACK, the worker dispatches the claimed effect asynchronously; a restart can replay the claim, and a retry cannot create a second injection. Do not treat an ACK as confirmation that the SDK operation completed.
Each session starts with one root message. Root creation uses a caller-generated client message ID and reconciliation lookup, preventing a duplicate root after an uncertain post. When a session closes, the daemon posts a closure marker. A resume starts a new immutable root, so replies to the old root are rejected and cannot steer the resumed session.
Events, retried deliveries, event contexts, and interaction/message identifiers are deduplicated in the durable claim before a reply is injected into the captured current endpoint generation. After a Socket Mode reconnect, Slack may redeliver an envelope; the new delivery is acknowledged after its claim is recognized and cannot cause a second injection.
Adopting an existing thread
Stock startup publishes a session's readiness immediately, so the daemon
surfaces the session and creates its own root before an operator could name an
existing one. Adopting an existing root therefore has an explicit, opt-in
three-phase lifecycle. Configuration is never part of it: the workspace and
channel come from gjc notify setup alone, and the operator supplies only a
session id and a thread timestamp.
prepare session authority → bind the existing root through the live daemon → activate readiness
- Prepare. Start the session prepared. A manually started session opts in
with
GJC_NOTIFY_BIND_EXISTING_THREAD=1in its environment; a broker lifecycle-managed session is prepared by its launch request instead (see below). Either way the session publishes its endpoint and registers with the broker exactly as usual, so its id and endpoint generation are discoverable authority, but it withholds the replayablesession_readysignal. An attached daemon has nothing to surface, so no root is posted. - Bind.
gjc notify bind-thread --session-id <id> --thread-ts <root>adopts the existing root through the running daemon owner, exactly as it does for any live session. The CLI never writes the mapping store itself: it proves the configured target and the exact current owner, then submits the mutation over the per-request chat-daemon command channel that owner serves in place. A reported success is accepted only after this process observes the exact mapping in the durable conversation store, so a stale or forgedstatus:"ok"answer is reported asbinding_outcome_unknownrather than as a success. - Activate.
gjc notify activate-thread --session-id <id>asksSessionRouterto validate the exact indexed endpoint generation and invoke the session host internally. The host authorizes readiness against the provider-owned mapping through a redacted{sessionId, endpointGeneration}gate: activation before the binding is applied is refused (not_bound) with no grace period, and activation is idempotent, so an exact retry answersalreadyrather than publishing a second readiness signal. The CLI never reads an endpoint URL/token or constructs an SDK client. When readiness is published, Slack adopts the bound root and posts zero replacement roots.
The opt-in is per session and explicit: only the exact value 1 prepares a
session, and a session without it keeps the stock immediate-ready root. The
existing global (notifications.enabled, GJC_NOTIFICATIONS=0) and per-session
opt-outs are unchanged and still authoritative.
Preparation has exactly two authorities and they never overlap. A manually
started session uses the environment opt-in above. A broker lifecycle-managed
session is prepared only by the session-scoped readiness: "deferred" intent on
its own session.create request: the child then publishes a distinct
session_prepared signal, the lifecycle wait completes on that instead of
readiness, and the create receipt reports readiness: "prepared". The
environment opt-in is refused for lifecycle-managed sessions, so an inherited
process-global flag can never silently defer a broker-created session.
Either authority additionally requires a configured, session-enabled Slack
target, because the activation gate is the existing-thread presentation
mapping. The mapping stays provider-owned, while SessionRouter alone proves
the current endpoint generation and performs activation. A preparation request
that cannot build the configured mapping gate fails closed — the lifecycle child
settles a startup failure and the environment opt-in throws — rather than
degrading to ordinary immediate readiness or handing back a prepared session
that could activate with no binding at all.
Through the Coordinator MCP surface the same three phases are
gjc_coordinator_start_session with prepare_existing_thread: true (which
rejects an initial prompt and returns the session at state prepared), the
unchanged gjc notify bind-thread command, and
gjc_coordinator_activate_session. The Coordinator never writes the mapping
store: it proves exact endpoint authority and delegates to the same activation
exchange the CLI uses, and durable session state only becomes ready once the
session itself proves activated/already. A prepared session refuses
gjc_coordinator_send_prompt until it is activated.
Trust boundary
The command channel proves correlation, never authorship: every field a response echoes is copied verbatim out of the plaintext request published beside it in the daemon's own owner-only command directory. GJC trusts same-UID local processes, so nothing here defends against a hostile process running as the same user; what it does guarantee is that a stale or forged answer with no matching durable mapping never becomes a reported success.
Operational safety
Treat rate limits, permission failures, and Socket Mode disconnects as transport failures. Let the managed daemon reconnect or reconcile; do not run a competing Socket Mode consumer against the same app/state, manually modify conversation state, persist delivery cursors, expose loopback endpoints, or use Slack as a general remote shell.
The adapter only sends notifications and routes SDK replies. It does not support provider registration, retaining endpoint credentials, or arbitrary remote control.
Verification boundary
Acceptance coverage uses an injectable fake Slack provider plus a production Session SDK host boundary proof. It covers durable-claim-before-acknowledgement for accepted, rejected, duplicate, and reconnect-redelivered envelopes; root-post reconciliation; event/retry/context/interaction dedupe; generation and restart isolation; rate-limit/permission/disconnect failures; and the prohibition on persisted Socket Mode cursors. No live Slack credentials or workspace is required.