Signal AI Chatbot: Privacy-First Setup Guide
Run an AI assistant on Signal for encrypted conversations. Complete signal-cli setup with Claude integration.
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 --versionRegister 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 123456Replace +1234567890 with your bot's phone number.
Project setup
mkdir signal-ai
cd signal-ai
npm init -y
npm install @anthropic-ai/sdk dotenvCreate .env:
SIGNAL_NUMBER=+1234567890
ANTHROPIC_API_KEY=your-anthropic-keyBasic 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 trialUse 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.
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-aiOpenClaw 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
- ChatGPT on WhatsApp - More popular but less private
- Telegram AI Bot - Easier setup, good privacy
- Self-Hosted AI Guide - Compare all channel options
Private AI conversations
OpenClaw on Signal. End-to-end encrypted AI assistance.
24-hour free trial · No credit card required · Cancel anytime