Enterprise Slack-native governance substrate for humans, Claude Code, and peer agents in shared channels. Declarative policy + signed audit + crash-safe loss-proof replies + five-layer prompt-injection defense.
Enterprise Slack-native governance substrate where humans, Claude Code sessions, and peer agents converse safely in shared channels. Every tool call passes through a declarative policy engine; every gate decision lands in a hash-chained, Ed25519-signed audit journal — per-thread session isolation, identity-aware permission gates, five-layer prompt-injection defense, and (new in v0.12.0) crash-safe sessions with loss-proof reply delivery.
A claude/channel implementation for Slack. Socket Mode (outbound WebSocket, no public URL) bridges Slack into a running Claude Code session via MCP stdio. Permission relay with Block Kit buttons, per-thread session isolation, hash-chained Ed25519-signed audit journal, declarative tier-aware policy engine, multi-agent coordination with circuit-breaker + !mute/!unmute operator verbs, streaming replies via chat.update, admin command hardening, lease-fenced crash recovery with an exactly-once reply outbox, and a five-layer prompt-injection defense. Three runtime options: Bun, Node.js, Docker.
Links: GitHub · GitHub Pages · Gist One-Pager · Release Notes
If you're using Claude Code, you're chained to the terminal. Step away from your desk and the conversation stops — worse, Claude may be waiting for permission to run a tool, and you can't approve it from your phone. Your phone has Slack, but Slack can't talk to your Claude session.
Multi-agent workflows make it worse. Once two or more AI agents collaborate (Claude in one terminal, Codex in another, a peer bot in a third), the coordination layer doesn't exist anywhere — and the security posture between them is undefined. Who can approve whose tool calls? What stops a runaway loop where Bot A's reply mentions Bot B who replies to Bot A? Who hard-stops a misbehaving peer agent?
And when the bridge process dies mid-turn — a deploy, an OOM, a crash — what happens to the reply Claude already committed to? A governance substrate that silently loses a decision or a reply is not trustworthy.
Anthropic shipped channels for Telegram and Discord, but not yet for Slack — the tool most teams already live in. This project fills that gap, including the permission relay for remote tool approvals, the multi-agent coordination primitives the broader ecosystem lacks, and a crash-safety layer that makes every reply loss-proof.
Eighteen production source modules (~15,100 lines of TypeScript) split between stateful runtime wiring (server.ts ~3,820) and pure, testable logic across lib.ts (~2,550), supervisor.ts (~1,810 — session state machine + crash recovery + outbox drain), journal.ts (~1,460), policy.ts (~820), slack-delivery.ts (~740 — durable reply delivery, new in v0.12.0), manifest.ts (~570), plus a dozen sibling modules (Ed25519 crypto, ACP adapter, policy dispatch, HMAC-nonce HITL, admin commands, streaming replies, audit-key loader + CLI, mute store, per-bot rate limiting). Four runtime dependencies (@modelcontextprotocol/sdk, @slack/web-api, @slack/socket-mode, zod) — no frameworks.
Inbound messages arrive via Socket Mode (Slack's outbound WebSocket — no public URL, works behind any firewall). Outbound replies go through the Slack Web API and stream via chat.update. Permission prompts render as Block Kit messages with Allow/Deny/Details buttons — tap your phone to approve a tool call from anywhere. Admin commands (!clear, !restart) are gated through the policy engine and journaled before execution; destructive verbs require an HMAC nonce the bot DMs you cross-channel (closes the EchoLeak class).
New in v0.12.0 — crash-safety. Each active session holds a per-turn fencing lease (monotonic token + heartbeat) so exactly one process owns a turn; a boot-time recovery sweep requeues work whose lease lapsed and quarantines anything it can't prove safe. Replies are recorded as durable obligations before the send and drained by a leased poller that retries transient Slack errors and dead-letters permanent ones — with idempotency keys so a redelivery after a lost ack never double-posts. This loss-proof guarantee covers single-message, chunked, streaming, and file-upload replies. File uploads re-run the outbound exfil guard on the exact bytes sent (read-once, TOCTOU-safe).
Security is defense-in-depth: inbound gate drops ungated messages before MCP, outbound gate restricts replies to delivered (channel, thread) pairs, file-exfiltration guard blocks state-directory leaks, a token-firewall blocks declared credential values from leaving in any outbound payload, system-prompt hardening tells Claude to refuse manipulation from messages, and tokens are locked down (0o600, atomic writes, never logged). Every security-relevant event appends to a hash-chained, Ed25519-signed audit journal that third-party verifiers can independently confirm.
| Aspect | Details |
|---|---|
| Who | Developers using Claude Code who collaborate via Slack, plus teams running multi-agent workflows |
| What | MCP channel server — bridges Slack events into Claude Code, relays permission prompts, isolates state per thread, signs the audit log, gates admin commands, routes multi-agent traffic, survives crashes |
| Where | Runs locally (your machine), connects to Slack via Socket Mode WebSocket |
| When | AFK coding, multi-agent ops, regulated workflows — approve tool calls from your phone, audit decisions cryptographically, coordinate Claude + Codex + peer bots, and never lose a reply to a crash |
| Why | Only Slack channel for Claude Code. Permission relay with Block Kit. No public URL. Five-layer prompt-injection defense. Ed25519-signed audit. Cross-channel HITL for destructive verbs. Multi-agent circuit breaker + !mute. Crash-safe, loss-proof replies. |
| Layer | Technology | Purpose |
|---|---|---|
| Runtime | Bun / Node.js / Docker | Flexible execution — pick your preference |
| Protocol | MCP (stdio) | Standard Claude Code channel transport |
| Connection | @slack/socket-mode v2 | Outbound WebSocket to Slack — no public URL |
| API | @slack/web-api v7 | Messages, file uploads, reactions, Block Kit, streaming chat.update |
| Validation | zod v3 | Schema validation for permission relay + audit journal + session/outbox state |
| Crypto | Node built-in Ed25519 | Audit-log signing — no new runtime deps |
| Canonicalization | RFC 8785 JCS (in-repo) | Byte-exact event serialization for cross-implementation verifiability |
| Durability | Transactional outbox + fencing lease | Crash-safe sessions; exactly-once reply delivery via idempotency keys |
| Security | Custom gate + allowlist + policy engine | 5-layer defense: inbound gate, outbound gate, exfiltration guard, system-prompt hardening, token lockdown |
- Crash-safe, loss-proof replies — lease-fenced sessions + boot recovery + a transactional reply outbox deliver every reply (single/chunked/streaming/file) exactly once, surviving a process crash or transient Slack failure
- Permission relay — approve/deny Claude Code tool calls remotely via Slack (Block Kit buttons or text fallback)
- No public URL — Socket Mode means outbound-only WebSocket, works behind firewalls and NAT
- Ed25519-signed audit journal — third-party verifiers independently confirm log integrity from a published public key (RFC 6962 CT-style key rotation)
- Cross-channel HITL for destructive verbs — admin
!restartrequires an HMAC nonce DMed out-of-band; closes the EchoLeak prompt-injection class - Multi-agent coordination — per-bot rate limit + channel-wide circuit breaker for N-bot rings, optional per-user session isolation, global backpressure,
!mute/!unmute+ read-only!agents/!mute-status/!rate-limitverbs - Streaming replies — long Claude outputs grow live via
chat.updatewith exactly-once journal finalization; now also crash-durable - Tier-aware policy engine — Admin > User > Workspace > Default precedence with cross-tier shadow detection
- Mention-to-engage channels — mention the bot once in a thread, then converse without re-mentioning (human-only stickiness)
- Per-thread session isolation — each Slack thread gets its own state file, supervisor, and policy scope
- Five-layer prompt-injection defense + fresh-clone onboarding (
/slack-channel:install) and three runtime options (Bun, Node.js, Docker)
claude-code-slack-channel is a production-oriented MCP server that bridges Slack workspaces to Claude Code sessions. The v0.12.0 implementation spans eighteen TypeScript modules totaling ~15,100 lines. Runtime core: server.ts (3,820 — stateful wiring), lib.ts (2,552 — pure functions: gate, guards, session/obligation types, idempotent-send helpers), supervisor.ts (1,810 — session state machine, fencing lease, crash-recovery sweep, outbox drain), journal.ts (1,461 — hash-chained signed audit log + redaction), policy.ts (818 — tier-aware policy engine), slack-delivery.ts (738 — durable reply delivery adapter, NEW), manifest.ts (573 — bot-manifest protocol). A dozen sibling modules cover Ed25519 crypto, the ACP adapter, policy dispatch, HMAC-nonce HITL, admin verbs, streaming, audit-key load/CLI, the mute store, and the peer-bot rate limiter. Four runtime dependencies, no frameworks, no build step for Bun.
v0.12.0 completes the crash-safety epic (ccsc-o7x) — the headline milestone. A per-turn fencing lease (monotonic token + heartbeat) guarantees exactly one process owns a turn; a superseded owner is driven into quarantine (no split-brain). A boot-time recovery sweep classifies in-flight work on restart: a lapsed heartbeat is requeued, a still-fresh or unreadable marker is orphaned into quarantine (fail-closed) — both journaled (session.recovery.requeued/orphaned). A transactional reply outbox records a durable obligation before the Slack send, drained by a leased poller that retries transient errors with exponential backoff and dead-letters permanent ones (always recording why). Idempotency keys stamped into Slack message metadata make redelivery after a lost ack a no-op — exactly-once visible delivery. The guarantee now spans every reply shape: single (ccsc-o7x.3), chunked (.4), streaming (.6), and file-upload (.5). File uploads re-run the outbound exfil guard on the exact bytes sent (read-once, closing a TOCTOU gap) and dedup within the obligation's delivery window. Also in v0.12.0: mention-to-engage channels, multi-agent hardening (channel circuit breaker, per-user session isolation, global maxConcurrentSessions backpressure, read-only operator verbs), and a Greptile AI-reviewer config. No breaking changes — all additive; lib.ts (the AGP-consumed kernel) gains only additive optional fields.
v0.11.0 shipped the token-firewall epic (ccsc-z0n): one SECRET_DECLARATIONS table drives the agent-facing placeholder, both exfiltration guards, and host-bound routing with no drift; assertNoSecretValues blocks a declared token value from leaving in any outbound payload, redactSecretValues scrubs one that would surface in a tool result. Relicensed MIT → Apache 2.0; Stryker mutation gate wired.
v0.10.0 shipped journal v2 Ed25519 signing with policy attestation, RFC 8785 JCS canonicalization interop, policy.deny context-stripping, tier-aware evaluate() with shadow detection, HMAC-nonce cross-channel HITL, admin command hardening, and the ACP adapter. v0.9.x / v0.8.0 / v0.7.0 / v0.6.0 closed the audit EventKind coverage gap, refactored complexity outliers (CI gate 85 → 30), added per-channel audit projection, and wired the declarative policy engine with multi-approver quorum.
| Category | Technology | Version | Purpose |
|---|---|---|---|
| Runtime | Bun | 1.2.x | Primary execution runtime (also Node.js via tsx) |
| Protocol | @modelcontextprotocol/sdk | 1.x | MCP server + stdio transport |
| Connection | @slack/socket-mode | 2.x | Outbound WebSocket to Slack (no public URL) |
| API | @slack/web-api | 7.x | Slack REST API (messages, files, reactions, interactions, chat.update, filesUploadV2) |
| Validation | zod | 3.x | Schema validation for permission relay, audit journal, session + outbox state |
| Crypto | Node built-in (Ed25519) | — | Audit-log signing; no new runtime deps |
| Canonicalization | RFC 8785 JCS (in-repo) | — | Byte-exact serialization for cross-implementation verifiability |
| Language | TypeScript | 5.x | Strict mode, type-checked |
| Container | Docker (oven/bun:1-slim) | — | Optional isolated runtime |
Slack workspace (cloud)
↕ WebSocket (Socket Mode — outbound only, no public URL)
18 production modules — server.ts (wiring) + lib.ts + supervisor.ts + journal.ts +
policy.ts + slack-delivery.ts + manifest.ts + crypto.ts + acp-adapter.ts +
policy-dispatch.ts + nonce-hitl.ts + admin.ts + stream-reply.ts +
audit-key-loader.ts + audit-key-cli.ts + mute-store.ts + peer-bot-rate-limit.ts
↕ stdio (MCP transport)
Claude Code session
⤷ durable reply outbox + fencing lease (crash recovery on restart)
⤷ optional: tmux exec for !clear / !restart admin verbs
⤷ optional: SOPS+age envelope around the audit signing key (boot-time decrypt)
| Layer | Function | Implementation |
|---|---|---|
| Inbound Gate | Drop unauthorized messages | gate() checks allowFrom + channel opt-in; bot messages dropped by default, per-channel allowBotIds opt-in; mute-store + rate-limit + channel circuit breaker gate cross-bot traffic; mention-to-engage stickiness is human-only |
| Outbound Gate | Restrict reply targets | assertOutboundAllowed() gates on delivered (channel, thread) pairs; streaming + the durable poller respect it |
| File Exfiltration Guard | Block state-dir + secret leaks | assertSendable() (path denylist + symlink resolution) + assertNoSecretValues() (value firewall); re-run on every file (re)upload on the exact bytes sent |
| Admin Verb Gate | Destructive ops require nonce | !restart mints an HMAC nonce, DMs it cross-channel, requires same-user same-channel redemption; no MCP tool name starts with admin. |
| Cryptographic Audit | Journal v2 — Ed25519 signed | Per-event signature over JCS-canonical bytes + policy-attestation digest; system.key_rotation carries old→new key; verifyJournal rejects v2→v1 rollback |
| Crash Safety | No lost decision or reply | Per-turn fencing lease + boot recovery sweep (requeue/quarantine, fail-closed); transactional reply outbox with leased retrying poller + idempotency keys = exactly-once delivery across all reply types |
| Token Security | Protect credentials | 0o600 permissions, atomic writes, never logged |
| Metric | Value |
|---|---|
| Test Count | 1,250 tests / 6,777 expect() calls (unit + fast-check property tests + Gherkin) |
| Acceptance Scenarios | 61 Gherkin scenarios across 7 feature files (engineer-owned, hash-pinned) |
| Line Coverage | ≥ 95% (CI floor) |
| Function Coverage | ≥ 95% (CI floor) |
| Mutation Score | Stryker baseline (lib + policy + manifest + journal) |
| Max Cyclomatic Complexity | 29 (CI gate: threshold 30) |
| TypeScript | Strict mode, zero errors |
| Dependencies | 4 production deps |
| Lines of Code | ~15,100 total (18 production modules) |
| Audit EventKinds | 40+ (added session.recovery.requeued/orphaned, session.activate_rejected, delivery dead-letters for crash-safety) |
| CI Gates | 9: typecheck → Biome → test → coverage floor 95% → dependency-cruiser → Gherkin lint → harness-hash verify → bun audit → crap-score ≤30 |
| Supply Chain | gitleaks v8, CodeQL, OpenSSF Scorecard, SHA-pinned GitHub Actions, bun audit |
What's working in v0.12.0
- Full MCP server with
claude/channel+claude/channel/permissioncapabilities - Crash-safe sessions — per-turn fencing lease + heartbeat, boot-time recovery sweep (requeue lapsed / quarantine unprovable, fail-closed), lease-loss → quarantine
- Loss-proof replies — transactional outbox, leased retrying/dead-lettering poller, idempotency keys; exactly-once across single / chunked / streaming / file-upload replies
- Per-thread session isolation; optional per-user session isolation within a shared thread
- Hash-chained, Ed25519-signed audit journal (v2) with policy attestation;
verifyJournalwalks mixed v1→v2 chains, rejects rollback, RFC 6962 CT-pattern key rotation - Tier-aware declarative policy engine (auto_approve / deny / require_approval; multi-approver quorum; context-stripping on deny)
- Admin commands (
!clear/!restart) gated through gate → policy → journal → exec; destructive verbs require cross-channel HMAC-nonce challenge - Multi-agent coordination — per-bot rate limit + channel circuit breaker, global
maxConcurrentSessionsbackpressure,!mute/!unmute+ read-only!agents/!mute-status/!rate-limit - Mention-to-engage channels (mention once, then converse; human-only)
- Streaming replies via
chat.update; per-channel audit projection (off/compact/full) - SOPS+age boot-time audit-key loading;
bun scripts/audit-key.ts {init,rotate} - Onboarding —
AGENTS.md,CONTRIBUTING.md,ROADMAP.md(+ Non-Goals),/slack-channel:install(8 modes),/slack-channel:{configure,access,policy} - Greptile AI-reviewer config (
.greptile/); full governance, 9-gate CI, zero open code-scanning alerts
Backlog (P3 only — no near-term blocking work)
- Durable AI-reviewer decision (Greptile config in place; interim reviewer is Gemini, which sunsets 2026-07-17)
ccsc-2oy— intermittentverifyJournal1000-event test flake under machine load (never blocks CI on fresh runners)- ADR-001 capability-token format (design shipped; remaining work forward-looking)
- Repo: github.com/jeremylongshore/claude-code-slack-channel
- License: Apache-2.0
- Latest Release: v0.12.0
- Test Count: 1,250 tests / 6,777 expects + 61 Gherkin scenarios
- Production Modules: 18 (~15,100 LoC)
- Headline: crash-safe sessions + loss-proof replies across every reply type
- @jeremylongshore — author, maintainer
- @maui-99 — security hardening review (v0.3.0)
- @jinsung-kang — clean shutdown fix (v0.3.1)
- @CaseyMargell — event dedup +
allowBotIds(v0.3.1, v0.4.0) - @gog5-ops — Node.js/tsx runtime fix (v0.5.0)
Full history follows Keep a Changelog + SemVer: CHANGELOG.md.
Added
- Crash-safety epic complete (
ccsc-o7x): per-turn fencing lease + heartbeat, boot-time recovery sweep (requeue/quarantine), transactional reply outbox with leased retrying/dead-lettering poller, and idempotency keys for exactly-once delivery — across single, chunked, streaming, and file-upload replies. - File-upload replies durable:
createFileSendDeps(read-once exfil guard on the exact bytes,filesUploadV2, delivery-window(filename,size)dedup);drainOutboxdead-letters exfil-blocked files on first failure. - Streaming replies crash-durable via a stream-finalize obligation; chunked replies durable via per-chunk obligations.
- Mention-to-engage channels (mention once, then converse; human-only stickiness).
- Multi-agent hardening: channel-wide circuit breaker, optional per-user session isolation, global
maxConcurrentSessionsbackpressure, read-only!agents/!mute-status/!rate-limitoperator verbs. - Greptile AI-reviewer config (
.greptile/);ROADMAP.mdwith explicit Non-Goals.
Changed
lib.tskernel gains additive optionalDeliveryObligationfields (upload,uploadedFileId) and anExfilBlockedErrortype; no breaking changes.- Production-dispatch journaling hardened so no policy decision path can silently skip the audit log.
- Token-firewall epic (
ccsc-z0n):SECRET_DECLARATIONSsingle source of truth;assertNoSecretValuesvalue-exfil guard;redactSecretValuestool-result scrub. Relicensed MIT → Apache 2.0; Stryker mutation gate.
- Journal v2 Ed25519 signing + policy attestation; RFC 8785 JCS interop;
policy.denycontext-stripping; tier-awareevaluate()+ shadow detection; HMAC-nonce cross-channel HITL; admin command hardening; ACP adapter.
This is a useful frame.
A lot of debates in this space get simpler once you ask where the control actually sits. Prompt. Tool call. Network edge. Human approval. Audit sink. Those layers overlap, but they do different jobs and fail in different ways.
When people talk past each other on agent governance, it is usually because they mean different layers.