A versatile AI assistant with a simple, elegant architecture — multiplexes
messaging channels to coding agents via
nq, a zero-setup Unix job queue.
Three independent processes cooperate via the filesystem:
muxclaw ingress muxclaw egress
┌──────────────┐ ┌───────────────┐
│ channel → │ │ → channel │
│ save message │ nq queue │ watch for │
│ + attachments│──────────→────│ completed │
│ enqueue job │ │ jobs, reply │
└──────────────┘ └───────────────┘
↕
nq → muxclaw dispatch
(reads prompt.txt,
runs coding agent)
- ingress — Long-running channel listener. Saves
prompt.txt,meta.json, and attachments into the message directory, then enqueues adispatchjob vianq. A temporary symlink maps the nq job name (,HEXTIME.PID) back to the message directory. - dispatch — Invoked by
nq. Readsprompt.txtand runs the configured coding agent (e.g.,claude -p).nqcaptures stdout and moves the job file tocompleted/orfailed/. - egress — Long-running watcher. Scans existing jobs on startup, then uses
Deno.watchFsfor live events. Readsmeta.json, sends the agent output back to the channel, and moves the job file into the message directory (marking it processed).
- Deno (v2+)
- nq — zero-setup Unix job queue
- Claude Code CLI (
claude) - A Telegram Bot Token
Create ~/.config/muxclaw/config.json with your bot token and allowed users:
{
"channels": {
"telegram": {
"token": "bot123:ABC..."
}
},
"allowedUsers": [
{ "userId": "12345" }
],
"workspace": "/path/to/your/project",
"agent": {
"name": "claude"
}
}You can find your Telegram user ID by messaging @userinfobot.
Run both commands in separate terminals. The agent runs in the current working
directory unless workspace is set in config.json:
# Terminal 1 — receive Telegram messages and queue jobs
deno task cli ingress
# Terminal 2 — watch for completed jobs and send replies
deno task cli egressIn private chats the bot handles all messages. In group chats it only responds to messages that @mention the bot.
Supported message types: text, photos, documents, audio, and voice messages. Replied-to messages are included as quoted context in the prompt.
Config and state follow the XDG Base Directory Specification:
| Directory | Purpose | XDG Variable |
|---|---|---|
~/.config/muxclaw/ |
Config (config.json) |
$XDG_CONFIG_HOME |
~/.local/share/muxclaw/ |
Persistent job data | $XDG_DATA_HOME |
~/.local/state/muxclaw/queue/ |
nq job queue | $XDG_STATE_HOME |
~/.local/share/muxclaw/
├── messages/
│ └── <channel>/ # e.g., telegram/
│ └── <id>/ # e.g., 123456789_456/
│ ├── prompt.txt # full prompt (quote + attachments + text)
│ ├── meta.json # routing info (channel, chatId, messageId, userId)
│ ├── attachments/ # downloaded photos, documents, audio, voice
│ └── ,HEXTIME.PID # job output (moved here after egress)
└── ,HEXTIME.PID.d # temporary symlink → message dir (removed after egress)
| Variable | Description | Default |
|---|---|---|
NQDIR |
nq queue directory | $XDG_STATE_HOME/muxclaw/queue |
Channels (e.g., Telegram) and coding agents (e.g., Claude Code) should be configurable and swappable. The architecture intentionally decouples ingress, dispatch, and egress so that adding a new channel or agent doesn't require changes to the other components.
Pull the image and run the container (mount claude CLI, its auth, your config,
and workspace):
docker run -it --rm \
-v ~/.claude:/home/deno/.claude \
-v ~/.claude.json:/home/deno/.claude.json \
-v ~/.config/muxclaw:/home/deno/.config/muxclaw \
-v $(pwd)/workspace:/workspace \
ghcr.io/jihchi/muxclawThis starts a Zellij session with three panes:
┌──────────────┬──────────────┐
│ Ingress │ Egress │
├──────────────┴──────────────┤
│ Workspace (/workspace) │
└─────────────────────────────┘
Licensed under either of
at your option.