Back to Blog
TutorialFebruary 2, 202611 min

Signal AI Chatbot: Privacy-First Setup Guide

Run an AI assistant on Signal for encrypted conversations. Complete signal-cli setup with Claude integration.

signalprivacyencryptionai chatbotself-hosted

Molted Team

Molted.cloud

Signal is the gold standard for private messaging. End-to-end encryption, no metadata collection, open source. If privacy matters to you, running an AI assistant on Signal keeps your conversations off Big Tech servers. This guide shows how to build one with signal-cli.

Why Signal for AI?

  • End-to-end encryption - Even your AI conversations are encrypted
  • No phone number exposure - Use a separate number for the bot
  • Open protocol - No corporate API restrictions
  • Disappearing messages - Auto-delete sensitive AI conversations
  • No ads, no tracking - Signal Foundation is a nonprofit

Requirements

  • A phone number for the bot (can be a VoIP number)
  • Linux/macOS server (signal-cli works best on Linux)
  • Java 17+ runtime
  • Anthropic or OpenAI API key

Install signal-cli

signal-cli is the command-line interface for Signal. It handles registration, sending, and receiving messages.

# Download latest release
wget https://github.com/AsamK/signal-cli/releases/download/v0.13.0/signal-cli-0.13.0-Linux.tar.gz
tar xf signal-cli-0.13.0-Linux.tar.gz
sudo mv signal-cli-0.13.0 /opt/signal-cli
sudo ln -sf /opt/signal-cli/bin/signal-cli /usr/local/bin/signal-cli

# Verify installation
signal-cli --version

Register your bot number

# Request verification code via SMS
signal-cli -u +1234567890 register

# Or via voice call
signal-cli -u +1234567890 register --voice

# Verify with the code you receive
signal-cli -u +1234567890 verify 123456

Replace +1234567890 with your bot's phone number.

Project setup

mkdir signal-ai
cd signal-ai
npm init -y
npm install @anthropic-ai/sdk dotenv

Create .env:

SIGNAL_NUMBER=+1234567890
ANTHROPIC_API_KEY=your-anthropic-key

Basic Signal AI bot

require('dotenv').config();
const { spawn } = require('child_process');
const Anthropic = require('@anthropic-ai/sdk');

const SIGNAL_NUMBER = process.env.SIGNAL_NUMBER;
const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
const conversations = new Map();

// Send message via signal-cli
function sendMessage(recipient, text) {
  return new Promise((resolve, reject) => {
    const proc = spawn('signal-cli', [
      '-u', SIGNAL_NUMBER,
      'send',
      '-m', text,
      recipient,
    ]);

    proc.on('close', (code) => {
      if (code === 0) resolve();
      else reject(new Error(`signal-cli exited with code ${code}`));
    });
  });
}

// Process message with AI
async function handleMessage(sender, text) {
  if (!conversations.has(sender)) {
    conversations.set(sender, []);
  }
  const history = conversations.get(sender);

  history.push({ role: 'user', content: text });

  if (history.length > 20) {
    history.splice(0, history.length - 20);
  }

  try {
    const response = await anthropic.messages.create({
      model: 'claude-sonnet-4-20250514',
      max_tokens: 1024,
      system: 'You are a privacy-focused AI assistant on Signal. Keep responses concise.',
      messages: history,
    });

    const reply = response.content[0].text;
    history.push({ role: 'assistant', content: reply });

    await sendMessage(sender, reply);
  } catch (error) {
    console.error('Error:', error);
    await sendMessage(sender, 'Sorry, I encountered an error.');
  }
}

// Listen for incoming messages
function startListener() {
  console.log('Starting Signal listener...');

  const proc = spawn('signal-cli', [
    '-u', SIGNAL_NUMBER,
    'receive',
    '--json',
  ]);

  proc.stdout.on('data', async (data) => {
    const lines = data.toString().split('\n').filter(Boolean);

    for (const line of lines) {
      try {
        const msg = JSON.parse(line);

        if (msg.envelope?.dataMessage?.message) {
          const sender = msg.envelope.source;
          const text = msg.envelope.dataMessage.message;

          console.log(`Message from ${sender}: ${text}`);
          await handleMessage(sender, text);
        }
      } catch (e) {
        // Ignore parse errors
      }
    }
  });

  proc.stderr.on('data', (data) => {
    console.error('signal-cli error:', data.toString());
  });

  proc.on('close', (code) => {
    console.log(`Listener exited with code ${code}. Restarting...`);
    setTimeout(startListener, 5000);
  });
}

startListener();

Privacy-first AI

OpenClaw supports Signal with automatic reconnection and group chat.

Start free trial

Use JSON-RPC daemon (recommended)

For better performance, run signal-cli as a daemon:

# Start daemon
signal-cli -u +1234567890 daemon --socket /tmp/signal.sock &

# Or with JSON-RPC over TCP
signal-cli -u +1234567890 daemon --tcp 127.0.0.1:7583 &
const net = require('net');

class SignalClient {
  constructor(host = '127.0.0.1', port = 7583) {
    this.host = host;
    this.port = port;
    this.requestId = 0;
    this.pending = new Map();
    this.messageHandlers = [];
  }

  connect() {
    return new Promise((resolve, reject) => {
      this.socket = net.createConnection(this.port, this.host);

      this.socket.on('connect', () => {
        console.log('Connected to signal-cli daemon');
        resolve();
      });

      this.socket.on('data', (data) => {
        const lines = data.toString().split('\n').filter(Boolean);
        for (const line of lines) {
          this.handleResponse(JSON.parse(line));
        }
      });

      this.socket.on('error', reject);
    });
  }

  handleResponse(response) {
    if (response.id && this.pending.has(response.id)) {
      const { resolve, reject } = this.pending.get(response.id);
      this.pending.delete(response.id);

      if (response.error) {
        reject(new Error(response.error.message));
      } else {
        resolve(response.result);
      }
    } else if (response.method === 'receive') {
      // Incoming message
      for (const handler of this.messageHandlers) {
        handler(response.params);
      }
    }
  }

  call(method, params = {}) {
    return new Promise((resolve, reject) => {
      const id = ++this.requestId;
      this.pending.set(id, { resolve, reject });

      const request = JSON.stringify({ jsonrpc: '2.0', id, method, params });
      this.socket.write(request + '\n');
    });
  }

  async send(recipient, message) {
    return this.call('send', {
      recipient: [recipient],
      message,
    });
  }

  onMessage(handler) {
    this.messageHandlers.push(handler);
  }
}

// Usage
const signal = new SignalClient();
await signal.connect();

signal.onMessage(async (params) => {
  if (params.envelope?.dataMessage?.message) {
    const sender = params.envelope.source;
    const text = params.envelope.dataMessage.message;
    await handleMessage(sender, text);
  }
});

Group chat support

signal.onMessage(async (params) => {
  const envelope = params.envelope;

  // Check if it's a group message
  if (envelope?.dataMessage?.groupInfo) {
    const groupId = envelope.dataMessage.groupInfo.groupId;
    const sender = envelope.source;
    const text = envelope.dataMessage.message;

    // Only respond if mentioned (e.g., "@AI" in message)
    if (text?.includes('@AI')) {
      const cleanText = text.replace('@AI', '').trim();
      const reply = await getAIResponse(cleanText);

      await signal.call('send', {
        groupId,
        message: reply,
      });
    }
  }
});

Disappearing messages

Signal supports disappearing messages. The bot respects the chat's settings automatically, but you can also set them:

// Set disappearing messages timer (in seconds)
await signal.call('updateExpirationTimer', {
  recipient: [sender],
  expiresInSeconds: 3600, // 1 hour
});

Signal AI without the complexity

Molted deploys OpenClaw with Signal integration. Registration wizard included.

Try free for 24 hours

Security hardening

  • Run as dedicated user - Not root
  • Encrypt storage - signal-cli stores keys in ~/.local/share/signal-cli
  • Firewall - Only expose what's needed
  • Allowlist - Only respond to known contacts
const ALLOWED_NUMBERS = new Set([
  '+1111111111',
  '+2222222222',
]);

signal.onMessage(async (params) => {
  const sender = params.envelope?.source;

  if (!ALLOWED_NUMBERS.has(sender)) {
    console.log(`Ignoring message from unknown sender: ${sender}`);
    return;
  }

  // ... process message
});

Deployment

# Create systemd service
sudo tee /etc/systemd/system/signal-ai.service << EOF
[Unit]
Description=Signal AI Bot
After=network.target

[Service]
Type=simple
User=signalbot
WorkingDirectory=/home/signalbot/signal-ai
ExecStart=/usr/bin/node index.js
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable signal-ai
sudo systemctl start signal-ai

OpenClaw for Signal

OpenClaw supports Signal as a first-class channel:

  • Built-in registration wizard
  • Automatic reconnection
  • Group chat support
  • Multi-model switching
  • Conversation persistence

If you want Signal AI without managing signal-cli, OpenClaw handles everything.

Related guides

Free 24-hour trial

Private AI conversations

OpenClaw on Signal. End-to-end encrypted AI assistance.

Start free trial

24-hour free trial · No credit card required · Cancel anytime

Ready to try OpenClaw?

Deploy your AI personal assistant in 60 seconds. No coding required.

Start free trial

24-hour free trial · No credit card required