claude-code-slack-channel v0.12.0
The Slack-native governance substrate for Claude Code — the kernel other governance tooling builds on. Humans, Claude Code sessions, and peer agents converse safely in shared channels: every tool call passes through a declarative, tier-aware policy engine; every decision lands in a hash-chained, Ed25519-signed audit journal you can verify offline. Per-thread session isolation, identity-aware permission gates, operator admin commands with cross-channel approval, multi-agent loop control + backpressure, lease-fenced sessions that survive a process crash with loss-proof reply delivery, and defense-in-depth against prompt injection. The policy/journal/relay kernel here is the substrate that the companion agent-governance-plane (AGP) reimplements for cross-runtime governance.
Links: Roadmap & Scope · Gist One-Pager · GitHub Pages · Release Notes
Research Preview — Channels require Claude Code v2.1.80+ and
claude.ailogin.
How It Works
Slack workspace (cloud)
↕ WebSocket (Socket Mode — outbound only, no public URL)
server.ts (local MCP server, spawned by Claude Code)
↕ stdio (MCP transport)
Claude Code session
Socket Mode means no public URL needed — works behind firewalls, NAT, anywhere.
Quick Start
For AI-assisted setup: run /slack-channel:install and your AI assistant will walk you through every step below. The install skill also supports doctor (health check), verify (round-trip test), repair (auto-fix), manifest (one-click Slack-app import), reset, tour, and uninstall modes — see skills/install/SKILL.md.
Prerequisites
Before step 1, confirm you have:
- Bun ≥ 1.0 — install with
curl -fsSL https://bun.sh/install | bash. Node.js / Docker fallbacks are documented in Option B / Option C below. - Claude Code ≥ v2.1.80 — see https://docs.claude.com/claude-code/install for upgrade.
claude.ailogin — this is a Research Preview constraint. API-key-only auth (ANTHROPIC_API_KEYset with noclaude.aisession) does NOT work for Channels. Runclaude loginto complete the browser flow.
1. Create a Slack App
- Go to api.slack.com/apps → Create New App → From scratch
- Socket Mode: Settings → Socket Mode → Enable → Generate App-Level Token (
xapp-...) withconnections:writescope - Event Subscriptions: Enable → Subscribe to bot events:
message.im— DMsmessage.channels— public channelsmessage.groups— private channelsapp_mention— @ mentions
- Bot Token Scopes (OAuth & Permissions):
chat:write— send messageschannels:history— read public channelsgroups:history— read private channelsim:history— read DMsreactions:write— add reactionsfiles:read— download shared filesfiles:write— upload filesusers:read— resolve display names
- Install to Workspace → Copy Bot Token (
xoxb-...)
2. Configure Tokens
/slack-channel:configure xoxb-your-bot-token xapp-your-app-token
3. Run
Pick your runtime:
Option A: Bun (recommended)
bun install
# Current (claude-code-plugins marketplace):
claude --channels plugin:slack-channel@claude-code-plugins
# Future (after upstream approval):
# claude --channels plugin:slack-channel@claude-plugins-official
Option B: Node.js / npx
npm install
# In .mcp.json, change command to: "npx", args: ["tsx", "server.ts"]
claude --channels plugin:slack-channel@claude-code-plugins
Option C: Docker
docker build -t claude-slack-channel .
# In .mcp.json, change command to: "docker", args: ["run", "--rm", "-i", "-v", "~/.claude/channels/slack:/state", "claude-slack-channel"]
claude --channels plugin:slack-channel@claude-code-plugins
3.5. Add the bot to your Slack channel
This step is the most common silent failure. Slack installs apps to the workspace without joining any channels. The pairing DM in step 4 works because DMs auto-route, but a channel test message in step 5 will hit silence — the bot literally isn't in the channel to see the event.
- Open Slack → navigate to the channel where you want the bot to live
- Click the channel name at the top → Integrations tab
- Click Add an App → select your bot
- Confirm: the bot now appears in the channel's member list
Private channels need explicit invitation. Repeat per channel if the bot needs to be in more than one.
4. Pair Your Account
- DM the bot in Slack — you'll get a 6-character pairing code
- In your terminal:
/slack-channel:access pair <code> - You're connected. Chat away.
5. Verify it's working
In the channel where you added the bot in step 3.5, send:
@<bot-name> hello
The bot should reply within 10 seconds. If you get silence, run /slack-channel:install doctor for a structured diagnosis, then /slack-channel:install repair to auto-fix what's fixable. See Troubleshooting for the top failure modes.
Troubleshooting
The five silent-failure modes that cover ~95% of fresh-install issues:
- Bot is not in the channel — see step 3.5 above. Channel test messages hit silence because the bot can't see the event.
- Claude Code version too old — run
claude --version; need ≥ v2.1.80. claude.ailogin missing —ANTHROPIC_API_KEYalone is not accepted (Research Preview constraint). Runclaude login.- Bun not installed —
bun: command not found. Install withcurl -fsSL https://bun.sh/install | bashor use the Node.js fallback in Option B. - Wrong file permissions on
.env— must be0600. Runchmod 0600 ~/.claude/channels/slack/.env, or run/slack-channel:install repair.
Full troubleshooting matrix (10 failure modes): skills/install/references/troubleshooting.md.
Policy Engine (v0.6.0+)
Author rules in access.json.policy to automate permission decisions for Claude Code tool calls. Three rule effects, evaluated strictest-tier-first then first-applicable within a tier (detailed below):
{
"policy": [
{
"id": "safe-reads-in-ops",
"effect": "auto_approve",
"match": { "tool": "Read", "channel": "C_OPS_DOCS" }
},
{
"id": "no-shell",
"effect": "deny",
"match": { "tool": "Bash" },
"reason": "Shell execution is not permitted."
},
{
"id": "dangerous-writes",
"effect": "require_approval",
"match": { "tool": "Write", "channel": "C_DEPLOY" },
"approvers": 2,
"ttlMs": 300000
}
]
}
auto_approve— skip the Block Kit prompt; the tool call runs immediately and apolicy.allowevent is journaled.deny— the reason is posted back into the originating thread and the call is rejected.policy.denyis journaled.require_approval— route through human approver(s).approvers: 2requires two distinct Slackuser_ids (NIST two-person integrity; the same user cannot double-satisfy quorum by clicking twice). A single deny from any allowlisted user rejects the request immediately regardless of quorum count.- Successful approvals grant a TTL window scoped to
(rule, channel, thread)so a chain of similar calls doesn't re-prompt. - Parse errors in
access.json.policyare fatal at boot — policy is safety-critical, silent degradation is not offered. Missing or emptypolicyis fine (first-install path).
Tier-aware evaluation: rules carry a tier (admin → user → workspace → default). Evaluation is strictest-tier-wins — a higher tier's deny cannot be overridden by a lower tier's auto_approve, so a workspace-level guardrail can't be relaxed by a per-user rule. Within a tier, first-applicable ordering holds.
When a call is denied, the result returned to Claude is context-stripped to a bare { behavior: 'deny' } — the rule id and reason are journaled and posted to the thread, but not handed back to the model, so a denied tool call can't be reverse-engineered into a rephrase-and-retry loop.
Full schema reference: ACCESS.md. Decision procedure: 000-docs/policy-evaluation-flow.md. Release scope and what was deliberately deferred: 000-docs/v0.6.0-release-plan.md.
Access Control
See ACCESS.md for the full schema.
/slack-channel:access policy allowlist # Only pre-approved users
/slack-channel:access add U12345678 # Add a user
/slack-channel:access remove U12345678 # Remove a user
/slack-channel:access channel C12345678 # Opt in a channel (default: mention-to-engage)
/slack-channel:access channel C12345678 --ambient # Ambient: Claude hears every message
/slack-channel:access status # Show current config
Interaction modes. A newly opted-in channel defaults to mention-to-engage (requireMention: true): people converse freely and Claude only sees messages that @-mention it — and once a human mentions the bot in a thread, they can keep talking in that thread without re-mentioning (peer agents must @-mention every time). Pass --ambient for a dedicated bot channel where Claude hears everything, or --allow <ids> to restrict which humans are heard. See ACCESS.md.
Multi-agent coordination
Channels can opt in to cross-bot message delivery by listing trusted bot user IDs in allowBotIds. Useful when multiple Claude Code instances (or other bots you operate) need to coordinate in a shared channel — e.g., an ops-monitor agent and an engineering agent in #incidents. Default is no cross-bot delivery: every bot message is dropped at the gate. See ACCESS.md for the full schema and security tradeoffs.
Example access.json entry:
{
"channels": {
"C_INCIDENTS": {
"requireMention": false,
"allowFrom": ["U_OPS_BOT", "U_ENG_BOT", "U_HUMAN"],
"allowBotIds": ["U_OPS_BOT", "U_ENG_BOT"]
}
}
}
Self-echoes from this bot are always filtered regardless of allowBotIds. Peer bots cannot approve permission prompts — the permission relay gates on the top-level allowFrom, not the channel policy.
For the full multi-agent recipe — registering a second bot, configuring allowBotIds mutually, mention-driven addressing, the per-bot rate limit + channel-wide circuit breaker + global session backpressure, per-user session isolation, the !mute/!unmute write verbs and !agents/!mute-status/!rate-limit read-only verbs, common failure modes, what you DON'T get — see 000-docs/multi-agent-channels.md.
This is a prompt-injection vector by design. The security model is documented threat-model-first in SECURITY.md: (1) the adversary and the four principals, (2) the mitigations below — each with its own stated limit, and (3) an explicit list of what this does NOT protect against (same-UID host compromise, in-process token exposure, supply-chain, Slack/Anthropic platform bugs). Start there for the full picture; the defense-in-depth layers are:
- Sender gating: Every inbound message hits a gate. Ungated messages are silently dropped before reaching Claude.
- Outbound gate: Replies only work to channels that passed the inbound gate.
- File exfiltration guard: Cannot send
.env,access.json,audit.log, or other state files through the reply tool. - Prompt-injection defense: System instructions explicitly tell Claude to refuse pairing/access requests from Slack messages — peer-bot messages carry the same risk as human messages.
- Bot filtering:
bot_idmessages are dropped by default. Channels that host multiple cooperating agents can opt in to specific peers viaallowBotIds; self-echoes are always filtered viabot_id/bot_profile.app_id/usertriple-check. - Link unfurling disabled: All outbound messages set
unfurl_links: false, unfurl_media: false. - Token security:
.envischmod 0o600, never logged, never in tool results. - Static mode: Set
SLACK_ACCESS_MODE=staticto freeze access at boot (no runtime mutation). - Signed audit journal: Every tool-call decision is written to a hash-chained, Ed25519-signed journal (RFC 8785 JCS canonical form). The chain is verifiable offline against a published public key — see Audit Signing.
- Admin-command hardening: Operator verbs (
!clear,!restart) route through gate → policy → journal → execute and require a server-minted HMAC nonce confirmed from a second channel. Claude cannot self-invoke them — no MCP tool name begins withadmin.. This closes the EchoLeak / operator-coercion class (CVE-2025-32711, threat T11): a prompt injected into one channel cannot drive a privileged action, because confirmation must come from a channel the attacker doesn't control. - Multi-agent loop control + backpressure: A per-
(channel, bot_id)sliding-window rate limit (default 10 msg/60s) breaks A→B→A runaway loops; a channel-wide circuit breaker trips a 3+-bot ring (A→B→C→A) the pairwise limiter misses; an optional globalmaxConcurrentSessionscap sheds load before file handles / memory are exhausted (journalingsession.activate_rejected). Operators can!mute <@bot>/!unmute <@bot>a peer and inspect state read-only with!agents,!mute-status,!rate-limit. Optional per-user session isolation (perUserSessions) gives each sender their own bridge session within a shared thread. - Crash-safe sessions + loss-proof replies: Each active session holds a 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 cannot prove safe. Replies are recorded as durable obligations before the send and drained by a leased, retrying, dead-lettering poller with idempotency keys — so a transient Slack error or a process crash redelivers exactly once instead of silently dropping the reply. Live for single-message, chunked, and streaming replies (file-upload durability is in progress — see Reliability & crash-safety).
Each layer's limit, and an explicit "what this does NOT protect against" section, are in SECURITY.md. Trust boundaries, per-primitive attack surface, and threats T1–T11 are documented in 000-docs/THREAT-MODEL.md.
Audit Signing
Every decision the gate makes is journaled to ~/.claude/channels/slack/audit.log — hash-chained and, as of v0.10, Ed25519-signed — so a third party can verify the log against a public key without trusting the host.
# Verify a journal end-to-end (hash chain + signatures):
bun server.ts --verify-audit-log ~/.claude/channels/slack/audit.log
# Operator key lifecycle (Ed25519 over RFC 8785 JCS):
bun audit-key-cli.ts init # generate keypair, encrypt private half to .env, print public key
bun audit-key-cli.ts rotate # fresh keypair, re-sign chain head, archive the old public key
bun audit-key-cli.ts verify # verify the log against active + archived public keys
bun audit-key-cli.ts show # print active public key + key id
The key is loaded at boot from SOPS+age-encrypted .env. Run with --no-audit-signing to fall back to hash-chain-only. Design + rotation lifecycle: 000-docs/audit-journal-architecture.md and 000-docs/key-management.md.
Reliability & crash-safety
A governance substrate that loses a decision — or a reply — is not trustworthy. The runtime is built so a process crash never silently drops work:
- Fencing lease per turn. Each active session holds a monotonic-token + heartbeat lease, so exactly one process owns a turn even if an old one resurrects. A fenced write from a superseded owner is rejected and the handle is driven into quarantine (no split-brain).
- Boot-time recovery sweep. On startup the supervisor reads every session file and classifies in-flight work: a lapsed heartbeat (owner provably dead) is requeued; a still-fresh marker or an unreadable file is orphaned into quarantine and fails closed until a human clears it. Both are journaled (
session.recovery.requeued/session.recovery.orphaned). - Transactional reply outbox. A reply is recorded as a durable
DeliveryObligationbefore the Slack send, drained by a leased poller that retries transient errors with bounded exponential backoff and dead-letters permanent ones (a dead-letter always records why — never a silent black hole). - Exactly-once delivery. Each obligation carries a deterministic idempotency key stamped into Slack message metadata; a redelivery after a lost ack is recognised and skipped. Live for single-message, chunked, and streaming replies; file-upload durability is landing in two parts (building blocks + design merged; production wiring in progress), because Slack file shares carry no app metadata and need a thread-scan dedup plus an exfil-guard that re-validates the bytes on every (re)upload.
Operator knobs (all optional, sensible defaults): SLACK_SESSION_LEASE_TTL_MS (lease TTL, default 30s), SLACK_DELIVERY_POLL_MS (outbox drain interval, default 15s), SLACK_MAX_CONCURRENT_SESSIONS (global backpressure cap, default unlimited). Design: 000-docs/session-state-machine.md and the crash-safety addenda in 000-docs/ADR-002.
Companion tooling
agent-governance-plane (AGP) is a companion governance tool that builds on this substrate. AGP reimplements ("adapt-and-harden") a pinned baseline of CCSC's governance kernel — the policy engine, the hash-chained signed journal, the Slack relay, and nonce-bound HITL — under its own typed contracts to govern agent runtimes beyond a single Slack bridge. It is not a byte-for-byte vendored copy: AGP's ADR 040 superseded the original vendor approach for v0, tracking CCSC by a documented drift review against a pinned commit instead. The relationship is one-way by design: CCSC is the substrate and has zero knowledge of AGP; AGP is the downstream consumer. If you need governance across multiple agent runtimes (not just Claude Code in Slack), AGP is the layer to look at; if you need a governed Slack channel for Claude Code, CCSC is all you need. The substrate-extraction contract is in AGP's 000-docs/009-AT-ADR-... (mechanism updated by its 040-AT-ADR-substrate-boundary-reconciliation), and the patterns shared between the two are recorded in 000-docs/ADR-002.
Testing & Quality
Security-critical code earns a hard gate. ci.yml runs nine checks in sequence on every PR — a red on any one blocks merge:
| Gate | What it enforces |
|---|---|
bun run typecheck |
TypeScript strict, no any escape hatches |
| Biome lint | curated rule set |
bun test |
1,251 tests / 6,782 assertions across unit + property + Gherkin suites |
| coverage floor | ≥ 95% line + function (scripts/coverage-floor.sh) |
| dependency-cruiser | architecture invariants (e.g. policy.ts may never import manifest.ts) |
| Gherkin lint | acceptance-feature style, --strict |
| harness-hash verify | SHA-256 tamper check on pinned .feature + arch-rule files |
bun audit |
dependency CVEs at --audit-level=high |
| crap-score | cyclomatic-complexity ceiling (30) |
Out of band: CodeQL, gitleaks secret scanning, OpenSSF Scorecard, and manual Stryker mutation testing (bunx stryker run, baselines in 000-docs/MUTATION_REPORT.md). The seven Wall-1 acceptance primitives (61 scenarios across 7 .feature files) live in features/*.feature and run against the real code via a hand-rolled Gherkin runner.
bun test # full suite
bun run typecheck # strict type check
bunx @biomejs/biome check .
Development
# Dev mode (bypasses plugin allowlist):
claude --dangerously-load-development-channels server:slack
One-Pager & System Analysis
Full project one-pager and operator-grade system analysis
Changelog
Every user-visible change is recorded in CHANGELOG.md (Keep a Changelog format). Release notes: GitHub Releases.
Contributing
See CONTRIBUTING.md for branching, commits, PR workflow, and the quality-gate checklist. AI assistants helping with contributions should read AGENTS.md first.
Contributors
- @jeremylongshore — author, maintainer
- @maui-99 — security hardening review (v0.3.0)
- @jinsung-kang — clean shutdown on client disconnect (v0.3.1)
- @CaseyMargell — event deduplication fix (v0.3.1), cross-bot delivery via
allowBotIds(v0.4.0)
Support
This is open-source software maintained by Jeremy Longshore. If it saves you time, consider sponsoring on GitHub or buying a coffee — it funds continued development and keeps the project independent.
License
Apache License 2.0 — see LICENSE.
No comments yet
Be the first to share your take.