Skip to content
BotServBotServ
SignalBotAI BotOllamasignal-cliE2EEPrivacySelf-Hosting

Signal Bot with Local AI

Build a Signal bot with signal-cli and Ollama: E2E-encrypted AI chats, JSON-RPC, groups, Docker setup.

S

schutzgeist

6 min read
Signal Bot with Local AI

Signal Bot with Local AI

What this article covers

  • Building a Signal bot with signal-cli and Ollama.
  • Registering a bot number and pairing devices.
  • Receiving and sending messages via JSON-RPC and daemon mode.
  • Extensions: groups, RAG, attachments, reactions.
  • Why Signal is the most privacy-respecting bot platform.

Introduction

Signal is the only mainstream platform with true end-to-end encryption for everything, including bot messages. If you need an AI bot for sensitive communication (journalists, lawyers, internal business matters), Signal is where you’ll end up.

The catch: there’s no official bot API. The standard approach is signal-cli, a Java CLI that implements the Signal protocol and offers a daemon mode with JSON-RPC. Your bot is technically a “Signal client” that functions like any other device.

Common use cases

  • Confidential AI assistant: Chats remain E2E-encrypted all the way to your server, ideal for sensitive queries.
  • Family or team bot: Internal communication without Meta, Google, or cloud providers.
  • Alerting: Monitoring alerts sent E2E-encrypted to your phone (better than email).
  • Document assistant: Share confidential documents via Signal and process them locally.
  • Editorial contact: Sources can speak anonymously with the AI.

Prerequisites

  • A phone number for the bot (landlines work: verification by call; or a second SIM/eSIM)
  • Java 17+ (signal-cli is written in Java) or Docker
  • Ollama with a model (llama3.1:8b or similar)
  • Python for the bot logic

Step 1: Install and register signal-cli

# Binary release (Linux)
wget https://github.com/AsamK/signal-cli/releases/download/v0.13.x/signal-cli-0.13.x-Linux.tar.gz
sudo tar xf signal-cli-*.tar.gz -C /opt
sudo ln -s /opt/signal-cli-*/bin/signal-cli /usr/local/bin/signal-cli

Register the number (requires a captcha link from https://signalcaptchas.org/):

signal-cli -a +4917612345678 register
signal-cli -a +4917612345678 verify 123456
signal-cli -a +4917612345678 updateProfile --name "AI Assistant"

Alternative: Link it as a secondary device to your existing account; the bot will send messages as you (fine for personal assistants, but not for public bots):

signal-cli link -n "Bot-Server"
# returns sgnl:// link → display as QR code on your phone (e.g., with qrencode) and scan

Step 2: Daemon mode with JSON-RPC

signal-cli can run as a JSON-RPC daemon; your Python code communicates over TCP or HTTP:

# TCP daemon
signal-cli -a +4917612345678 daemon --tcp 127.0.0.1:7583

# or HTTP (REST-like)
signal-cli -a +4917612345678 daemon --http 127.0.0.1:8080

JSON-RPC methods (selected): send, receive, listGroups, sendTyping, addReaction, updateProfile.

Step 3: Bot logic with Ollama

The simplest approach is a receive loop via CLI calls (good for getting started):

#!/usr/bin/env python3
import json
import subprocess
import ollama

BOT_NUMBER = "+4917612345678"
MODEL = "llama3.1:8b"
ALLOWED = {"+491701234567"}          # Who can use the bot
conversations = {}

client = ollama.Client(host="http://localhost:11434")

def send(recipient, text):
    subprocess.run([
        "signal-cli", "-a", BOT_NUMBER, "send",
        "-m", text, recipient
    ], check=False)

def ask_ollama(user, text):
    history = conversations.setdefault(user, [])
    history.append({"role": "user", "content": text})
    history = history[-10:]
    resp = client.chat(
        model=MODEL,
        messages=[{"role": "system", "content":
                   "You are a helpful assistant. Keep your answers brief."}] + history)
    answer = resp["message"]["content"]
    history.append({"role": "assistant", "content": answer})
    conversations[user] = history
    return answer

def main():
    proc = subprocess.Popen(
        ["signal-cli", "-a", BOT_NUMBER, "receive", "-t", "-1",
         "--output", "json"],
        stdout=subprocess.PIPE, text=True)
    for line in proc.stdout:
        line = line.strip()
        if not line:
            continue
        try:
            envelope = json.loads(line)
        except json.JSONDecodeError:
            continue
        msg = envelope.get("envelope", {})
        source = msg.get("sourceNumber") or msg.get("source")
        data = msg.get("dataMessage")
        if not data or source not in ALLOWED:
            continue
        text = data.get("message")
        if not text:
            continue
        print(f"[{source}] {text}")
        answer = ask_ollama(source, text)
        # Signal limit ~64k, but short answers are better
        send(source, answer[:6000])

if __name__ == "__main__":
    main()

This works immediately: polling mode, no webhook needed.

Extensions

Group operation

signal-cli can manage groups. The bot responds in groups only when mentioned or with a prefix:

data = msg.get("dataMessage")
group = data.get("groupInfo")          # {"groupId": "...", "type": "..."}
text = data.get("message") or ""

if group:
    if not text.startswith("!ai"):
        continue
    text = text[3:].strip()
    # Send the reply to the group:
    subprocess.run(["signal-cli", "-a", BOT_NUMBER, "send",
                    "-g", group["groupId"], "-m", answer])

Create and manage groups via CLI:

signal-cli -a +49... updateGroup -n "Team-AI" -m +491701234567 +491702345678
signal-cli -a +49... listGroups

Reactions and typing indicator

# Show typing
signal-cli -a +49... sendTyping +491701234567

# Send reaction
signal-cli -a +49... sendReaction -e "👍" -a +491701234567 -t 1695123456789 +491701234567

Attachments: send images to vision models

dataMessage.attachments contains file paths: feed images to minicpm-v or llava, audio to Whisper:

for att in data.get("attachments", []):
    if att.get("contentType", "").startswith("image/"):
        resp = client.chat(model="minicpm-v", messages=[{
            "role": "user",
            "content": "What is in this image?",
            "images": [att["file"]],
        }])
        send(source, resp["message"]["content"])

RAG like Telegram

Same pattern: embed the message, query Qdrant, inject context into the system prompt. See Telegram Bot RAG and Local RAG.

Deployment

Docker: signal-cli as a daemon

services:
  signal-cli:
    image: registry.gitlab.com/morph027/signal-cli-daemon:latest
    volumes:
      - signal_data:/home/.local/share/signal-cli
    environment:
      - MODE=json-rpc
      - SIGNAL_NUMBER=+4917612345678
    ports:
      - "127.0.0.1:7583:7583"
    restart: always

  signal-bot:
    build: ./bot
    restart: always
    depends_on: [signal-cli, ollama]

  ollama:
    image: ollama/ollama:latest
    volumes: [ollama_data:/root/.ollama]

volumes:
  signal_data:
  ollama_data:

systemd without Docker

[Unit]
Description=Signal AI Bot
After=network-online.target

[Service]
Type=simple
User=signalbot
WorkingDirectory=/opt/signal-bot
ExecStart=/usr/bin/python3 bot.py
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target

Privacy, the Real Reason for Signal

  • Full E2EE: Messages are encrypted between sender and bot server. Signal servers see only metadata, and they minimize that too.
  • No ads, no tracking: Signal is a nonprofit organization.
  • Open Source: Client, server, and signal-cli are all open source.
  • Boundary: The bot itself decrypts on your server, so your rules apply there. Signal still sees metadata (who writes when).

Common Pitfalls

  • Registration Captcha: Signal requires captchas during signup. Workaround: register the account on a phone first, then pair it as a linked device.
  • Number blocked: If Signal blocks the number (rare): run signal-cli ... register again.
  • Daemon crashes: signal-cli is Java; on OOM, set JAVA_OPTS="-Xmx512m".
  • Receiving stalls: Slow responses in the receive loop block the entire loop. Move Ollama calls into threads or async functions.
  • Sealed Sender: Messages from unknown contacts require trust. Use signal-cli trust -a <number> for contacts.

Further Reading

Key Takeaways:

  • Signal bots run through signal-cli (daemon + JSON-RPC), not via an official bot API.
  • Only platform with full E2EE, ideal for confidential bots.
  • Setup: register a number or pair as a linked device, then run the receive loop.
  • Extensions work the same as everywhere: groups, RAG, vision, Whisper.
  • For public bots it’s cumbersome: Telegram is simpler, Signal is more private.

FAQ

Is there an official Signal bot API?

No. signal-cli (by AsamK) is the unofficial standard, a complete Signal client as CLI/daemon. It’s open source and actively maintained, but without official support from Signal.

Do I need my own phone number?

Not necessarily. You can pair the bot as a linked device to your account (it sends as you). For a standalone bot: you’ll need a separate number. A landline or second SIM works fine.

Are bot messages really encrypted?

Yes. Signal encrypts everything end-to-end, including bot chats. The message decrypts only on your server. That makes Signal the best choice for confidential bot communication.

Registration fails at the captcha. What do I do?

Solve the captcha at signalcaptchas.org and pass the token to register. Alternatively: register the number on a real phone first, then pair the bot as a linked device (skips that step).

Does the bot work in groups?

Yes. Add the bot to a group and group messages come through with groupInfo in the envelope. The bot can reply to the group, manage members (updateGroup), and react.

Is signal-cli fast enough?

For bots, yes. Messages arrive in under a second. The bottleneck is always the local AI inference, not the transport. Only the Java overhead uses about 200-400 MB RAM.

Signal or Matrix for private bots?

Signal: better E2EE (even minimizes metadata), but unofficial API. Matrix: official bot API (matrix-nio), self-hostable server, E2EE optional. For pure privacy, Signal. For integration and control, Matrix.

Sources and Further Reading

Back to Blog
Share:

Related Posts