muxclaw
v2.0.0
Published
Multi-client AI coding agent router via Telegram/WhatsApp
Readme
MuxClaw
A lightweight Node.js system that maps Telegram/WhatsApp group chats to client projects and triggers an AI coding agent (Pi or OpenCode) to make code changes on demand.
Architecture
Telegram / WhatsApp
|
+-------------------------------+
| Layer 1: Router (stateless) | bot.ts + router.ts
| Maps chatId -> project agent |
+---------------+---------------+
|
+-------------------------------+ one per project, persistent
| Layer 2: Chat Agent | chat-agent.ts (Anthropic SDK)
| Owns conversation + tools. |
| chatModel (default: Haiku) |
+---------------+---------------+
| (when coding needed)
+-------------------------------+ ephemeral Docker container per job
| Layer 3: Worker | docker.ts + pi.dockerfile
| Pi or OpenCode runs inside. |
| workerModel (default: Sonnet)|
| Only project dir mounted. |
+-------------------------------+
|
Dev server (hot reload) + Cloudflare tunnel
|
client-a.yourdomain.comQuick Start
1. Prerequisites
Install the following on your machine (local Mac/Linux or remote VM):
| Tool | Install | Purpose |
|------|---------|---------|
| Node.js >= 24 | nvm or nodejs.org | Runtime |
| pnpm | npm install -g pnpm | Package manager |
| Docker | docs.docker.com | Worker containers |
| git | Pre-installed on most systems / sudo apt install git | Version control |
| curl | Pre-installed on most systems / sudo apt install curl | Used by preview scripts |
| GitHub CLI (gh) | brew install gh / sudo apt install gh | Auto-create project repos |
| cloudflared | brew install cloudflared / cloudflare docs | Preview tunnels |
Optional:
| Tool | Install | Purpose |
|------|---------|---------|
| pm2 | npm install -g pm2 | Run router as always-on daemon (pnpm daemon:install) |
| Claude CLI | claude.ai/download | Generate setup tokens (claude setup-token) for subscription auth |
Telegram bot setup
- Open Telegram and message @BotFather
- Send
/newbot, follow prompts to name your bot - Copy the bot token — set it as
TELEGRAM_BOT_TOKENin.env - Disable privacy mode so the bot sees all group messages: send
/setprivacyto BotFather, select your bot, choose Disable
Cloudflare account
Sign up at dash.cloudflare.com (free tier is enough). You need a Cloudflare account to:
- Deploy projects to Workers (
pnpm staging/pnpm deploy) - Use named tunnels for preview (optional — quick tunnels work without an account)
Wrangler auth (for deployment)
npx wrangler login # one-time, opens browser to authorize CloudflareOn a headless VM, use an API token instead:
export CLOUDFLARE_API_TOKEN=<your-token> # create at dash.cloudflare.com/profile/api-tokensThis is only needed when you deploy projects to Cloudflare Workers. You can skip this until step 8.
GitHub CLI auth
Local machine (GUI):
gh auth login # opens browser
gh auth refresh -h github.com -s workflow # add workflow scope for CI/CDHeadless VM (no GUI):
gh auth login -h github.com -w # prints a code, open URL on any device
gh auth refresh -h github.com -s workflowSet git protocol to HTTPS (avoids SSH key setup):
gh auth setup-git # configures git to use gh tokenCloudflare tunnel auth (optional)
cloudflared tunnel login # one-time, opens browser (or use device code on VM)This stores ~/.cloudflared/cert.pem. Only needed if using named tunnels — quick tunnels (default) work without it.
2. Setup
./setup.shThis installs dependencies, approves build scripts, adds your user to the docker group (if needed), builds Docker images, and creates .env from the template.
Note: If your user was just added to the
dockergroup, you must log out and back in (or reboot) for the group to take effect. Then re-run./setup.sh— it's idempotent.
3. Configure environment
Edit .env (created by setup.sh):
TELEGRAM_BOT_TOKEN=your_token_here
# Pick ONE auth method:
ANTHROPIC_API_KEY= # pay-per-token
ANTHROPIC_OAUTH_TOKEN= # Claude subscription (claude setup-token)Option A — API key (pay-per-token):
Get your key from console.anthropic.com/account/keys and set ANTHROPIC_API_KEY.
Option B — Setup token (Claude subscription): Requires the Claude CLI installed (see prerequisites). Run:
claude setup-tokenCopy the token (starts with sk-ant-oat-) and set ANTHROPIC_OAUTH_TOKEN. This routes through your Claude subscription — no per-token billing.
4. Create a project
Option A: Scaffold a new Astro site (recommended for web projects)
pnpm create-project my-project --name "My Project"This scaffolds an Astro + TailwindCSS + Cloudflare Workers project under projects/my-project/, installs dependencies, inits git, and registers it with the router. Each scaffolded project includes:
| Script | What | Use case |
|--------|------|----------|
| pnpm dev | Astro dev server (localhost) | Developer working locally |
| pnpm preview | Dev server + Cloudflare tunnel | AI agent + client hot reload |
| pnpm staging | Build + deploy to Workers staging | Client approval before go-live |
| pnpm deploy | Build + deploy to Workers production | Production release |
Options: --dir <path>, --tunnel <name>, --worker, --chat-model, --worker-model
To configure preview, set TUNNEL_NAME in the project's .env (or pass --tunnel at creation time). Without a tunnel name, pnpm preview uses a quick tunnel with a random URL.
Option B: Register an existing project
pnpm add-project my-project /path/to/existing --name "My Project"The project directory must be a git repo. If the project has a scripts/preview.sh, the router will use it for live preview sessions. Options: --repo, --worker, --chat-model, --worker-model
5. Start the bot
pnpm start # production
pnpm run dev # development (auto-restart)Startup validates: Docker running, Docker images built, auth configured, Telegram token set (if needed).
6. Add a channel
Add the bot to a Telegram group (or WhatsApp), then @mention it once. The chat ID is logged to the console via [discovery]:
[discovery] Unknown chat: platform=telegram id=-1001234567890 title="My Group" type=supergroupCopy the ID and attach it:
pnpm add-channel my-project telegram -1001234567890Restart the bot to pick up the new channel. A project can have multiple channels:
pnpm add-channel my-project whatsapp [email protected]7. Deploy (when ready)
Deploy from the router using deploy-project:
pnpm deploy-project my-project staging # deploy to my-project-staging.workers.dev
pnpm deploy-project my-project production # deploy to productionOr from the project directory directly:
cd projects/my-project
pnpm staging # deploy to staging
pnpm deploy # deploy to productionRequires wrangler auth — see Wrangler auth in prerequisites.
8. Enable WhatsApp (optional)
WhatsApp support uses Baileys (no official API / business account needed).
- Uncomment
startWhatsApp()insrc/bot.ts - Start the router — a QR code appears in the terminal
- Scan the QR code with WhatsApp on your phone (Settings > Linked Devices > Link a Device)
- Auth state is saved in
whatsapp-auth/— you only need to scan once
Add WhatsApp channels the same way as Telegram:
pnpm add-channel my-project whatsapp [email protected]The chat ID is logged to the console via [discovery] when someone messages a group the bot is in.
9. Run as daemon (optional)
To keep the router running after you close the terminal:
pnpm daemon:install # start as background service (uses pm2)
pnpm daemon:logs # view live logs
pnpm daemon:status # check if running
pnpm daemon:uninstall # stop and removeRequires pm2 — install with npm install -g pm2.
Config
Defaults
All projects inherit from defaults. Projects only override what's different.
| Field | Default | Used by |
|-------|---------|---------|
| chatModel | claude-haiku-4-5-20251001 | Chat agent (Anthropic SDK) |
| workerModel | anthropic/claude-sonnet-4-6 | Worker (Pi/OpenCode in Docker) |
| worker | pi | Docker image + CLI |
Per-project overrides
| Field | Required | Description |
|-------|----------|-------------|
| project | Yes | Path to project directory (relative to repo root, e.g. projects/my-project) |
| repo | No | Git repo URL — project is auto-cloned on first start if directory is missing |
| channels | Yes | Array of { platform, chatId } |
| name | No | Display name (defaults to slug) |
| chatModel | No | Override chat model |
| workerModel | No | Override worker model |
| worker | No | pi or opencode |
Preview, tunnel, and deploy config live in the project itself (.env and wrangler.jsonc), not in the router.
Auth
| Method | Env var | Worker support | Use case |
|--------|---------|---------------|----------|
| Setup token | ANTHROPIC_OAUTH_TOKEN | Pi only | Claude subscription (personal/pilot) |
| API key | ANTHROPIC_API_KEY | Pi + OpenCode | Pay-per-token (production) |
The chat agent (Layer 2) supports both methods via the Anthropic SDK.
Pi auto-detects setup tokens by prefix (sk-ant-oat) and routes them through OAuth bearer auth.
Workers
| Worker | Docker image | Auth | Model format |
|--------|-------------|------|-------------|
| Pi | client-agent-pi | API key + setup token | anthropic/claude-sonnet-4-6 |
| OpenCode | client-agent-opencode | API key only | anthropic/claude-sonnet-4-6 |
Pi is the default — it supports Claude subscription auth, has LSP diagnostics, hash-anchored edits, and skills.
Client Usage
make the header dark blue
add a contact form to the homepage
what did you just change?
start the preview server
cancel
/clearThe bot receives all messages in registered channels and uses AI to decide which ones are directed at it. You can also @mention it or reply to its messages to guarantee a response. Send /clear or reset to start a fresh conversation.
Security
- Each job runs in a fresh Docker container (
--rm) - Only the project's folder is mounted — other projects are invisible
- Memory: 2GB, CPU: 2 cores per container
- Runs as non-root user (uid 1000)
- Client messages are wrapped in
[CLIENT MESSAGE]tags and treated as untrusted input - Coding prompts are rewritten by the chat agent — never passed raw
File Structure
muxclaw/
|
+-- src/
| +-- bot.ts # entry point + startup validation
| +-- telegram.ts # Telegraf bot
| +-- whatsapp.ts # Baileys WhatsApp
| +-- handler.ts # thin bridge to router
| +-- router.ts # Layer 1: chatId -> project dispatch
| +-- agents.ts # agent registry (one ChatAgent per project)
| +-- chat-agent.ts # Layer 2: Anthropic SDK agentic loop
| +-- docker.ts # Layer 3: spawn/monitor/cancel containers
| +-- devserver.ts # preview session lifecycle (calls project's pnpm preview)
| +-- ports.ts # dynamic port allocator
| +-- history.ts # per-project conversation persistence
| +-- git.ts # git status helper
| +-- types.ts # shared TypeScript interfaces
|
+-- scripts/
| +-- create-project.ts # pnpm create-project CLI (scaffold + register)
| +-- remove-project.ts # pnpm remove-project CLI (unregister + optional delete)
| +-- add-project.ts # pnpm add-project CLI (register existing)
| +-- add-channel.ts # pnpm add-channel CLI
|
+-- pi.dockerfile # Pi coding agent image
+-- opencode.dockerfile # OpenCode agent image
+-- config.json # defaults + projects
+-- .env.example # environment variables template
+-- package.json
+-- tsconfig.json