agentchannels
v0.2.0
Published
Connect Claude Managed Agents to messaging channels like Slack, Discord, Teams, and more
Maintainers
Readme
Agent Channels
Agent Channels (ach) is a CLI that bridges your communicatino channels, such as Slack, to agents like Claude Managed Agents. Mention the bot in any channel or DM and each thread becomes a multi-turn streaming session with your agent — tools, vaults, and all.
Agent Channels is right for you if
- ✅ You've built (or want to build) a Claude Managed Agent and need to put it in front of a team
- ✅ You want to build agents once and provide them across multiple channels
- ✅ You want to provide your agents to your colleagues through Slack, without building a separate app for them
- ✅ You don't want to build messy connectors between agents and communication channels yourself
- ✅ You want multi-turn conversations per thread, not one-shot Q&A bots
- ✅ You want streaming responses that appear in real time, not 30-second waits for a wall of text
Quick Start
Set up a Claude Managed Agent on Slack in three commands:
# Install
brew install agentchannels/tap/ach
# Set up agent, environment, vault, and Slack app — one interactive wizard
ach init slack
# Start the bot
ach serveThat's it. ach init slack selects or creates your Claude Managed Agent and Environment, optionally links a Vault, configures your Slack app, and writes everything to .env. Then ach serve picks it all up automatically.
Prefer Claude Code? Install the plugin (see Installation below), then run
/agentchannels:init-slackand/agentchannels:serveinside Claude Code. Jump to Use from Claude Code for details.
Installation
Install directly into Claude Code — no git clone needed:
claude plugin marketplace add agentchannels/agentchannels
claude plugin install agentchannels@agentchannelsThen use /agentchannels:init-slack and /agentchannels:serve inside Claude Code. See Use from Claude Code for what each skill does.
To update: claude plugin update agentchannels@agentchannels
brew install agentchannels/tap/achnpm install -g agentchannelsRequires Node.js >= 18.
npx agentchannels init slack
npx agentchannels serveRequires Node.js >= 18.
git clone https://github.com/anthropics/agentchannels.git
cd agentchannels
pnpm install && pnpm build
pnpm link --globalPrerequisites
- Anthropic API key with Managed Agents access — console.anthropic.com
- Slack workspace where you can create apps
Configuration
agentchannels splits its configuration in two:
.envholds secrets — your Anthropic API key and Slack tokens.agentchannels.config.tsdescribes the agent backend — which Claude Managed Agent to talk to, or any other Backend implementation you wire up. Theach init slackwizard generates this file for you.
Environment variables (.env)
| Variable | CLI flag | Description |
|---|---|---|
| ANTHROPIC_API_KEY | — | Anthropic API key. Read by the generated config file. |
| SLACK_BOT_TOKEN | --slack-bot-token | Slack bot token (xoxb-...) |
| SLACK_APP_TOKEN | --slack-app-token | Slack app-level token (xapp-...) for Socket Mode |
| SLACK_SIGNING_SECRET | --slack-signing-secret | Slack signing secret (optional under Socket Mode) |
Agent config (agentchannels.config.ts)
The wizard scaffolds something like:
import {
defineConfig,
ManagedAgentBackend,
BackendRegistry,
} from "agentchannels";
const registry = new BackendRegistry();
export default defineConfig({
resolveBackend: (ctx) =>
registry.resolve(
ctx.threadKey,
() =>
new ManagedAgentBackend({
clientConfig: { apiKey: process.env.ANTHROPIC_API_KEY ?? "" },
agentId: "agent_…",
environmentId: "env_…",
}),
),
});Pass --config <path> to ach serve to point at a different file.
See Backend interface below for how to plug in a custom Backend (e.g. a local Claude CLI process) instead of ManagedAgentBackend.
CLI Reference
ach init slack
Interactive wizard that handles complete setup in one flow:
- Anthropic API key — validated up front; invalid keys re-prompt instead of crashing the wizard
- Claude Managed Agent — pick from the list, create a new one, or paste an existing agent ID
- Environment — pick from the list, create a new one, or paste an existing environment ID
- Vaults (optional) — multi-select from the list, paste comma-separated IDs, or skip (provides MCP OAuth credentials to sessions)
- Slack app — three modes: automatic (creates the app via the Slack API), guided (walks you through api.slack.com), or manual (paste tokens you already have)
Smart about existing state: if .env still contains legacy CLAUDE_AGENT_ID / CLAUDE_ENVIRONMENT_ID / CLAUDE_VAULT_IDS keys (from older versions), each is validated against the API and offered for reuse. Stale IDs (agent deleted since last run, invalid vault) are flagged and you're re-prompted only for the affected slots. When your account has no agents or environments yet, the wizard jumps straight to the create flow.
The wizard writes Slack tokens and ANTHROPIC_API_KEY to .env, and the chosen agent / environment / vault IDs to agentchannels.config.ts. After running this command, ach serve needs no flags.
Non-interactive mode (CI / scripting):
ach init slack --non-interactive \
--anthropic-api-key sk-ant-... \
--claude-agent-id agent_... \
--claude-environment-id env_... \
--claude-vault-ids vault_a,vault_b \
--slack-bot-token xoxb-... \
--slack-app-token xapp-... \
--slack-signing-secret ...Every ID is validated silently. If any required value is missing or invalid, the command exits non-zero and names the failing field.
ach serve
Starts the bot. Loads agentchannels.config.ts (override with --config <path>), connects to Slack via Socket Mode (no public URL needed), listens for @mentions and DMs, calls your resolveBackend(ctx) hook per thread, and streams responses back. Press Ctrl+C to stop.
Use from Claude Code
Once the Claude Code plugin is installed, two slash commands are available. They wrap the CLI but let Claude gather your credentials conversationally — no terminal takeover, no remembering flags.
/agentchannels:init-slack
Walks you through complete setup — agent, environment, vault, and Slack app — in one conversation. The wizard:
- Validates your
ANTHROPIC_API_KEYfirst so Slack setup never happens against a broken agent. - Lists your Claude Managed Agents; lets you pick one or create a new one.
- Lists your Claude Environments; lets you pick one or create a new one.
- Optionally selects a Vault for MCP OAuth credentials.
- Handles Slack app creation — Claude asks which path you want:
- Automatic — you paste a Slack Refresh Token (
xoxe-...); Claude runsach init slack --non-interactive --slack-refresh-token ..., which creates the app via the Slack API and opens your browser for workspace install. - Manual — you already have bot token, app token, and signing secret; Claude passes them as inline env vars to
ach init slack --non-interactiveso they don't appear inps.
- Automatic — you paste a Slack Refresh Token (
Writes all IDs and credentials to .env on success.
/agentchannels:serve
Verifies CLAUDE_AGENT_ID and CLAUDE_ENVIRONMENT_ID are set, then launches ach serve in the background so the bridge keeps running while you keep using Claude Code. Claude can check whether the process is still alive via ps.
Prereq: Make sure you've run
/agentchannels:init-slackfirst — it creates your Claude Managed Agent, Environment, and Slack credentials in one flow and writes all IDs to.env.
How the skills work under the hood
The plugin ships two SKILL.md files that instruct Claude to (1) gather credentials using AskUserQuestion, then (2) invoke the existing CLI via Bash with --non-interactive. There is no MCP server — the skills are thin glue over the CLI, so every CLI flag and env var is usable from Claude Code too.
Deploy
Railway
ach deploy railwayInteractive wizard that creates a project, pushes your env vars, and deploys the Docker image. Requires a Railway API token.
Docker
docker run -d \
--env-file .env \
ghcr.io/agentchannels/agentchannels:latestOther platforms
agentchannels uses Socket Mode (WebSocket), so it works anywhere that runs persistent processes — Fly.io, Render, any VPS. Not recommended for serverless (Lambda, Vercel) since agent responses can take 30+ seconds.
Backend interface
agentchannels is library-first — ach serve is a thin wrapper around an extension point you can target from your own code. The core abstraction is Backend: one instance per thread, streaming agent responses as AgentStreamEvents.
interface Backend {
sendMessage(text: string, opts?: { signal?: AbortSignal }): AsyncIterable<AgentStreamEvent>;
abort(): void;
dispose?(): Promise<void> | void;
isTransient?(error: unknown): boolean;
}Required vs. optional events
The streaming bridge consumes a discriminated union of events. Only three are required; the rest unlock richer Slack UI when your backend can produce them:
| Event | Required? | What it drives |
|---|---|---|
| text_delta | ✅ | Streamed message body |
| done | ✅ | Closes the streaming response |
| error | ✅ | Surfaces a user-visible error |
| thinking | optional | "Thinking…" task indicator |
| tool_use | optional | Plan-mode task entry per tool call |
| tool_result | optional | Marks the matching task complete |
| status | optional | Observability only |
A text-only backend works on day one; you can add tool tracking later without touching agentchannels.
Worked example: a custom Backend
This sketch is what a sibling project (e.g. aweek) might ship to bridge a Slack thread to a local Claude CLI session:
// my-cli-backend.ts
import type { Backend, AgentStreamEvent } from "agentchannels";
import { spawn, type ChildProcess } from "node:child_process";
export interface MyCliBackendOptions {
cwd: string;
agentSlug: string;
}
export class MyCliBackend implements Backend {
private current?: ChildProcess;
constructor(private readonly opts: MyCliBackendOptions) {}
async *sendMessage(text: string, { signal }: { signal?: AbortSignal } = {}): AsyncIterable<AgentStreamEvent> {
const proc = spawn("claude", ["--cwd", this.opts.cwd, "--agent", this.opts.agentSlug], {
stdio: ["pipe", "pipe", "pipe"],
});
this.current = proc;
signal?.addEventListener("abort", () => proc.kill("SIGTERM"));
proc.stdin.end(text);
for await (const chunk of proc.stdout) {
yield { type: "text_delta", text: String(chunk) };
}
yield { type: "done" };
}
abort(): void {
this.current?.kill("SIGTERM");
}
isTransient(err: unknown): boolean {
return /ECONNRESET|EPIPE/.test(String(err));
}
}Wire it into agentchannels.config.ts:
import { defineConfig, BackendRegistry } from "agentchannels";
import { MyCliBackend } from "./my-cli-backend.js";
const registry = new BackendRegistry({ ttlMs: 30 * 60_000 });
export default defineConfig({
resolveBackend: (ctx) =>
registry.resolve(ctx.threadKey, () =>
new MyCliBackend({
cwd: process.cwd(),
agentSlug: ctx.channelId, // or whatever your routing logic decides
}),
),
});Run ach serve against this config and every Slack thread streams from your CLI process instead of a managed agent — no agentchannels code changes required.
BackendRegistry and withRetry
agentchannels stays stateless across messages: every inbound message calls resolveBackend again, so consumers own the per-thread cache. BackendRegistry is a small reference helper for the common case (Map<threadKey, Backend> with optional TTL eviction). For transient-error retries before the first event lands, the bridge wraps your backend with withRetry, which respects Backend.isTransient(err) to decide what to retry.
How It Works
┌─────────────────┐ ┌──────────────────────┐ ┌────────────────────┐
│ │ │ │ │ │
│ Slack thread │◀────▶│ ach serve (you) │◀────▶│ Backend (any │
│ │ │ │ │ impl you wire) │
│ @mention │ │ Socket Mode │ │ │
│ reply in │─────▶│ listener + │─────▶│ ManagedAgentBackend
│ thread │ │ streaming bridge │ │ | LocalCliBackend │
│ │◀─────│ │◀─────│ | … │
│ user @mention │ │ channel-agnostic │ │ │
│ │ │ adapter │ │ │
└─────────────────┘ └──────────────────────┘ └────────────────────┘
WebSocket long-lived Node your agent of choice
(no public URL) process (your host) (managed, CLI, etc.)- A teammate @mentions the bot in Slack — Slack pushes the event over the Socket Mode WebSocket.
ach servecalls yourresolveBackend(ctx)hook; aBackendRegistryreturns the cached Backend for that thread (or creates one).- The bridge iterates
backend.sendMessage(text)— eachtext_delta/tool_use/tool_resultevent becomes a Slack stream update. donecloses the response;errorsurfaces with the formatter from your config.
