xmppcb
A lean XMPP AI chatbot in C. It joins MUC rooms, logs messages,
answers @bot commands via an OpenAI-compatible LLM HTTP API, and runs
@scriptname commands backed by local scripts.
Inspired by the suckless spirit: one source file, one library dependency (OpenSSL), fixed buffers, no runtime config parser.
There is no end-to-end OMEMO/OTR encryption. Transport is still encrypted: STARTTLS for the XMPP connection, HTTPS for the LLM API.
Build
Needs a C compiler and OpenSSL development headers
(apt install libssl-dev).
make
The makefile copies config.def.h to config.h on first build.
Edit config.h (not the .def) to set your server, rooms and model.
Configure
Non-secret settings live in config.h. Recompile after changing it.
Secrets are read from the environment, never stored on disk by the bot. Export them by hand:
export XMPP_PASSWORD='the-bot-account-password'
export LLM_API_KEY='sk-...'
Or use the bundled start script, which sets both variables and then
execs the bot. Copy the template, fill in your secrets, and lock it
down so only you can read it:
cp start.sample start
chmod 700 start
instruction.md holds the system prompt; it is read once at startup.
On a fresh checkout, copy instruction.md.sample to instruction.md,
then edit it and restart to change the bot's behaviour.
Choosing the LLM model and provider
The bot speaks the OpenAI-compatible /v1/chat/completions protocol, so
it works with any provider that does — OpenAI, OpenRouter, Together, a
local Ollama/llama.cpp server, etc. Four macros in config.h select it:
| Macro | Purpose |
|---|---|
LLM_HOST |
API hostname |
LLM_PORT |
API port (usually 443) |
LLM_PATH |
chat/completions path |
LLM_MODEL |
model identifier |
OpenAI (default):
#define LLM_HOST "api.openai.com"
#define LLM_PATH "/v1/chat/completions"
#define LLM_MODEL "gpt-5.4-mini"
OpenRouter:
#define LLM_HOST "openrouter.ai"
#define LLM_PATH "/api/v1/chat/completions"
#define LLM_MODEL "anthropic/claude-sonnet-4-6"
Change the macros, run make again, and set LLM_API_KEY to a key for
that provider. The bot sends max_completion_tokens; a few non-OpenAI
servers still expect the older max_tokens field and would need a
source change.
Run
Run from the directory containing instruction.md and config.h:
./xmppcb
If you set up the start script above, run ./start instead — it
exports the secrets, cds into this directory and execs the bot.
It reconnects automatically after a dropped connection. On an
authentication failure it exits with status 1 (no retry loop).
Logs go to stderr. Per-room chat history is appended to
history/<room>.log.
Usage
In a joined room:
| Command | Effect |
|---|---|
@bot help |
Show commands |
@bot summarize [period] |
Summarise the conversation |
@bot tasks [period] |
Extract action items |
@bot <question> |
Ask anything, using recent chat as context |
@<name> [args] |
Run the script command cmd/<name> (see below) |
Period tokens: 1h 1d 2d 1w 2w 1m. Without one, the last 50 messages
are used.
Script commands
Besides the @bot builtins, any executable you drop into the cmd/
directory becomes an @<name> command — no recompile, no restart.
Example, cmd/weather:
#!/bin/sh
# @weather [location] - current weather via wttr.in
loc=$(printf '%s' "${1:-Krakow}" | tr ' ' '+')
curl -fsS --max-time 8 "https://wttr.in/${loc}?format=3"
chmod +x cmd/weather
Then in any joined room @weather Wroclaw makes the bot run the script,
capture its output (stdout and stderr) and post it back. @bot help
lists the script commands it finds.
How it works and the safety boundaries:
- The text after the command name is passed as one argv element —
@weather New Yorkrunscmd/weather "New York". The bot never hands it to a shell, so there is no command injection; quote"$1"inside your script and you are safe. - Command names allow only letters, digits,
-and_, so they cannot escape thecmd/directory. - A script is SIGKILLed after
CMD_TIMEOUT_SECseconds (default 10); the bot is single-threaded, so it is paused meanwhile. - Output is capped at ~4 KB; longer output is truncated with
...[cut]. - Unknown
@namesare ignored silently, so the bot does not react to@mentionsof people.
Security: anyone in a joined room can run anything in cmd/. The
command set is whatever lives in that directory — keep it curated, and
do not let scripts print secrets. CMD_DIR and CMD_TIMEOUT_SEC are in
config.h. Two example scripts ship in cmd/: weather and uptime.
Run under a supervisor
The bot does not daemonise and does not restart itself after it exits.
Use a process supervisor so it restarts on crash and comes up at boot.
Each example runs the start script, so no secrets land in the
supervisor config. Replace /home/xmppcb/xmppcb and the xmppcb
user/group with yours.
The scripts below are example templates, not tested on every release — check them against your OS version before relying on them.
FreeBSD (rc.d)
/usr/local/etc/rc.d/xmppcb:
#!/bin/sh
# PROVIDE: xmppcb
# REQUIRE: NETWORKING
# KEYWORD: shutdown
. /etc/rc.subr
name=xmppcb
rcvar=xmppcb_enable
load_rc_config $name
: ${xmppcb_enable:="NO"}
: ${xmppcb_user:="xmppcb"}
: ${xmppcb_dir:="/home/xmppcb/xmppcb"}
pidfile="/var/run/${name}.pid"
command="/usr/sbin/daemon"
command_args="-r -P ${pidfile} -u ${xmppcb_user} ${xmppcb_dir}/start"
run_rc_command "$1"
chmod +x it, then sysrc xmppcb_enable=YES and
service xmppcb start. daemon -r restarts the bot if it dies.
OpenBSD (rc.d)
/etc/rc.d/xmppcb:
#!/bin/ksh
daemon="/home/xmppcb/xmppcb/start"
daemon_user="_xmppcb"
. /etc/rc.d/rc.subr
rc_bg=YES
rc_cmd $1
chmod +x it, then rcctl enable xmppcb and rcctl start xmppcb.
OpenBSD's rc.d starts the bot at boot but does not respawn it on
crash; pair it with a cron check or a supervisor if you need that.
OpenRC
/etc/init.d/xmppcb:
#!/sbin/openrc-run
description="xmppcb XMPP AI bot"
command="/home/xmppcb/xmppcb/start"
command_user="xmppcb:xmppcb"
supervisor="supervise-daemon"
respawn_delay=10
depend() {
need net
}
chmod +x it, then rc-update add xmppcb default and
rc-service xmppcb start. supervise-daemon handles the restart.
sysvinit
sysvinit respawns processes listed in /etc/inittab. Add one line:
xb:2345:respawn:/bin/su -s /bin/sh -c /home/xmppcb/xmppcb/start xmppcb
Then run telinit q to reload. The respawn action gives you both
boot start and restart-on-crash without a separate init script.
runit
/etc/sv/xmppcb/run:
#!/bin/sh
exec chpst -u xmppcb:xmppcb /home/xmppcb/xmppcb/start
chmod +x it, then link the service into the scan directory:
ln -s /etc/sv/xmppcb /var/service/
(some distributions use /etc/service or /run/runit/service).
runit supervises and restarts the bot automatically.
Notes and limitations
- The bot only sees messages sent while it is in the room.
- LLM calls and script commands are blocking: the bot pauses while a
reply or a
cmd/script runs. Adequate for a low-traffic bot. - The XML parser is intentionally narrow — it understands only the XMPP stanzas this bot needs, not arbitrary XML.
- Resource use is small: ~37 KB binary, a few MB RSS.
Layout
xmppcb/
├── xmppcb.c all logic: TCP, TLS, XML, SASL, MUC, HTTP, LLM
├── config.def.h config template (committed)
├── config.h your config (copied from .def, gitignored)
├── instruction.md.sample system prompt template (committed)
├── start.sample run-script template (committed)
├── cmd/ @scriptname command scripts (weather, uptime, fx)
├── makefile
└── history/ per-room chat logs (generated, gitignored)
