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
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.
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# 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.
mkdir signal-ai
cd signal-ai
npm init -y
npm install @anthropic-ai/sdk dotenvCreate .env:
SIGNAL_NUMBER=+1234567890
ANTHROPIC_API_KEY=your-anthropic-keyrequire('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.
Coming soonFor 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);
}
});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,
});
}
}
});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.
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
});# 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 supports Signal as a first-class channel:
If you want Signal AI without managing signal-cli, OpenClaw handles everything.
OpenClaw on Signal. End-to-end encrypted AI assistance.
24-hour free trial · no credit card · cancel anytime
Build an AI bot for Matrix protocol. End-to-end encryption, federation, and bridging support for decentralized messaging.
Run ChatGPT or Claude on WhatsApp with Baileys. Complete self-hosted setup guide with code examples and deployment options.
Run your personal AI assistant 24/7 on a Mac Mini. Complete setup guide for OpenClaw, formerly known as Clawdbot and MoltBot.
STILL AWAKE?