Signal Messenger Gateway — Setup Prompt for a Fresh Hermes Agent

Messaging

Hand this prompt to any Hermes agent and it installs, links, and verifies the Signal gateway end-to-end — including the ARM64/aarch64 native-library fix.

What you’ll learn
Privacy / placeholder note: this guide is public. The phone number shown (+614…) and the machine paths are placeholders. Every install uses its own phone number and home directory — replace +<NUMBER> and /opt/signal-cli-<VERSION> with the real values on your system before running any command. No bot tokens or API keys are required to set up Signal.
Contents
  1. Purpose & how it works
  2. Step 0 — detect architecture (critical)
  3. Step 1 — install Java 25
  4. Step 2 — install signal-cli
  5. Step 3 — ARM64 native library (aarch64 only)
  6. Step 4 — link the Signal account (phone required)
  7. Step 5 — start the HTTP daemon
  8. Step 6 — configure Hermes
  9. Step 7 — end-to-end verification
  10. Acceptance criteria
  11. Handoff summary

Purpose & how it works

Signal is the most privacy-focused mainstream messenger — end-to-end encrypted by default, open-source, minimal metadata. Hermes connects to Signal through the signal-cli daemon running in HTTP mode. The Hermes Signal adapter streams inbound messages in real-time via SSE and sends outbound messages via JSON-RPC. Crucially, the Hermes adapter needs no new Python packages — it only needs signal-cli running externally.

This guide is a portable system prompt: give it to any Hermes agent on any Linux server and it will install, link, configure, and verify everything itself. The only value that requires a human is the phone number, captured when the operator scans a QR to link the account.

Step 0 — Detect the architecture (critical)

This single step drives the whole install. signal-cli bundles native cryptography libraries (libsignal-client) only for x86_64 Linux, Windows, and macOS. On any other CPU (ARM64/aarch64, armv7, riscv64, …) you must supply a matching native library yourself or signal-cli fails with:

WARN Manager - Failed to call libsignal-client: no signal_jni in java.library.path
Missing required native library dependency: libsignal-client
uname -m
# x86_64  -> straightforward install (skip Step 3)
# aarch64 / arm64 / armv7 / riscv64 -> you need Step 3

Step 1 — Install Java 25

signal-cli is a Java application. Recent releases (v0.14.8+) are compiled for Java class file version 69.0, which means Java 25. Java 17 or 21 is not enough — you will hit UnsupportedClassVersionError. On Debian/Ubuntu:

sudo apt-get install -y openjdk-25-jre-headless
java -version   # expect "openjdk version 25.x"

If apt only offers an older JDK, install a 25 build from an official OpenJDK PPA or via SDKMAN, or choose a signal-cli release that matches an older class-file version.

Step 2 — Install signal-cli

VERSION=$(curl -Ls -o /dev/null -w %{url_effective} \
  https://github.com/AsamK/signal-cli/releases/latest | sed 's/.*\/v//')
curl -L -O "https://github.com/AsamK/signal-cli/releases/download/v${VERSION}/signal-cli-${VERSION}.tar.gz"
sudo tar xf "signal-cli-${VERSION}.tar.gz" -C /opt
sudo ln -sf "/opt/signal-cli-${VERSION}/bin/signal-cli" /usr/local/bin/signal-cli
signal-cli --version   # confirm it prints a version

A UnsupportedClassVersionError ... class file version 69.0 means Java is too old — return to Step 1.

Step 3 — ARM64 native library (aarch64 only)

Skip this step entirely on x86_64. On aarch64, signal-cli does not ship the native lib. First, find the libsignal-client version signal-cli needs:

ls /opt/signal-cli-${VERSION}/lib/ | grep libsignal-client
# e.g. libsignal-client-0.102.1.jar  ->  serialized token "so01021"

Download a prebuilt aarch64 lib matching that version from a community mirror. The serialized so<NNNN> token is the version digits concatenated (e.g. 0.102.1 → so01021):

curl -sL -o /tmp/libsignal_arm64.gz \
  "https://media.projektzentrisch.de/temp/signal-cli/libsignal_jni_<soNNNN>_ubuntu2004_arm64.gz"
gunzip -f /tmp/libsignal_arm64.gz
sudo mkdir -p /opt/signal-cli-${VERSION}/native
sudo cp /tmp/libsignal_arm64 /opt/signal-cli-${VERSION}/native/libsignal_jni.so
file /opt/signal-cli-${VERSION}/native/libsignal_jni.so   # MUST read "ELF ... ARM aarch64"

Make the JVM use it. The signal-cli launcher forwards a SIGNAL_CLI_OPTS env var to every run — export it in your shell profile and prefix every signal-cli command:

export SIGNAL_CLI_OPTS="-Djava.library.path=/opt/signal-cli-${VERSION}/native"
# persist: append that export line to ~/.bashrc
SIGNAL_CLI_OPTS=... signal-cli --version
SIGNAL_CLI_OPTS=... timeout 15 signal-cli link -n "Test"
# both must now work; link prints a "sgnl://linkdevice?uuid=..." URI

This is the only interactive step. signal-cli runs as a linked secondary device of the operator's phone (like WhatsApp Web). The operator scans a QR code with Signal:

sudo apt-get install -y qrencode
# start the link in a background process so it stays alive while they scan,
signal-cli link -n "HermesAgent"
# read the resulting "sgnl://linkdevice?uuid=...&pub_key=..." URI and render it:
qrencode -t ANSIUTF8 -o - "<the-sgnl-link-uri>"

Present the QR and tell the operator: Open Signal on phone → Settings → Linked Devices → Link New Device → scan. The process prints Associated with: +614... when the scan succeeds. The link expires in ~1–2 minutes; re-run it if it times out. Capture the E.164 phone number — it becomes SIGNAL_ACCOUNT.

signal-cli listAccounts   # confirm the Number; may block if the daemon holds the lock
Pitfall — masking: signal-cli 0.14.8 masks the E.164 as +614****6080 in all output — link/listAccounts logs, accounts.json, even the SQLite account.db — as a privacy feature. You cannot recover the full number from disk. Ask the operator for their E.164 (+614XXXXXXXX) to set SIGNAL_ACCOUNT / SIGNAL_ALLOWED_USERS / SIGNAL_HOME_CHANNEL and to target the send test.
Delivering the QR reliably: the link lives only ~1–2 min, and an in-terminal ANSI/UTF8 QR often does not render or scan for a remote operator. The robust pattern is to automate link→render→send: start signal-cli link in a background subprocess, immediately read the printed sgnl://linkdevice?... URI, qrencode it to a PNG, and push the image to the operator's Telegram via sendPhoto (using TELEGRAM_BOT_TOKEN + TELEGRAM_ALLOWED_USERS). A single script keeps the QR inside the expiry window — the link cannot be extended.

Step 5 — Start the HTTP daemon

Pick a free port first: the default 8080 — and very often 8081 too — are usually already taken on a real server (on the diyyourdata box both were held by apache2, which makes signal-cli exit 3 / Failed to initialize HTTP Server). Confirm a port is free before starting:
ss -tln | grep -E ':(8080|8081) '   # any match = taken; pick another, e.g. 8082
Also check for a stale daemon still running — it holds the account config lock, and the next launch spins forever on Config file is in use by another instance, waiting…:
ps aux | grep '[o]rg.asamk.signal.Main daemon'
pkill -f 'signal-cli-<VERSION>.jar'   # kill the stale one, then start clean
SIGNAL_CLI_OPTS=... signal-cli --account +<NUMBER> daemon --http 127.0.0.1:<PORT>
# verify it is up:
curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:<PORT>/api/v1/check   # 200

Install it as a systemd service for persistence across reboots — prefer this over a raw background process, and bind strictly to localhost (the daemon may otherwise also bind LAN interfaces). Example unit:

# /etc/systemd/system/signal-cli.service
[Unit]
Description=signal-cli HTTP daemon for Hermes gateway
After=network.target

[Service]
Type=simple
User=<account-owner>                 # same user that owns ~/.local/share/signal-cli
Environment=SIGNAL_CLI_OPTS=-Djava.library.path=/opt/signal-cli-<VERSION>/native
ExecStart=/opt/signal-cli-<VERSION>/bin/signal-cli daemon --http 127.0.0.1:<PORT>
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
sudo cp /path/to/signal-cli.service /etc/systemd/system/ && sudo systemctl daemon-reload
sudo systemctl enable --now signal-cli
systemctl status signal-cli   # Active: active (running)

Step 6 — Configure Hermes

hermes config set gateway.platforms.signal.enabled true

Add to ~/.hermes/.env (create/append; then chmod 600):

SIGNAL_HTTP_URL=http://127.0.0.1:<PORT>
SIGNAL_ACCOUNT=+<NUMBER>
SIGNAL_ALLOWED_USERS=+<NUMBER>     # allowlist the operator; comma-separate for more
SIGNAL_HOME_CHANNEL=+<NUMBER>      # default delivery target for cron jobs
# Optional groups: SIGNAL_GROUP_ALLOWED_USERS=<groupId1,groupId2> or *  (omit to disable)

If python-dotenv warns “could not parse statement starting at line N”, inspect that line — a stray lone = breaks parsing; remove it, but never remove real key lines. Then restart the gateway (or hermes gateway install / sudo hermes gateway install --system for persistence) and confirm in ~/.hermes/logs/gateway.log:

grep -iE "signal|platform" ~/.hermes/logs/gateway.log | tail
# look for "Signal: connected to http://127.0.0.1:<PORT>" AND "✓ signal connected"
# and "Gateway running with N platform(s)" that counts signal.

Step 7 — End-to-end verification

Send a live message. Because the daemon holds the config lock, the CLI signal-cli ... send may block on “Config file is in use by another instance” — send via the HTTP daemon JSON-RPC instead:

curl -s -m 40 -X POST http://127.0.0.1:<PORT>/api/v1/rpc -H "Content-Type: application/json" -d '{
  "jsonrpc":"2.0",
  "method":"send",
  "params":{"account":"+<NUMBER>","message":"Test from Hermes","recipient":["+<NUMBER>"]},
  "id":"test_send_1"
}'
# expect result.results[].type == "SUCCESS"

The recipient is the same account, so the message arrives in the operator's Signal app / Note to Self.

Acceptance criteria

#Criterion
1signal-cli --version prints 0.14.8+.
2On aarch64: libsignal_jni.so is ELF ARM aarch64, and link/send run without the no signal_jni error.
3signal-cli link yields a working URI and prints Associated with.
4Daemon HTTP check returns 200 on a free port (confirm 8080/8081 aren't already taken).
5gateway.log shows ✓ signal connected and Gateway running with N platform(s).
6A JSON-RPC send returns SUCCESS and the operator receives it.

Handoff summary

Report back without secrets: architecture detected, signal-cli version, whether the aarch64 native lib was needed, daemon URL + health, gateway.log connection status, end-to-end test result, and the names of the SIGNAL_* env vars that were set (not their values), plus how the daemon and gateway are kept alive (persistent service or manual).

Published to the public blog · verified live on an aarch64 (ARM64) Ubuntu 24.04 server with signal-cli 0.14.8, OpenJDK 25, and the Hermes gateway (updated 2026-10-07 to cover the signal-cli number-masking, QR-via-Telegram delivery, and free-port / systemd daemon pitfalls).

Bottom line
signal-cli in HTTP mode plus the ARM64 native library gives Hermes a 24/7 privacy-forward messaging adapter with no new Python packages.
📍 Location Tracking
We use your IP address (country/city only) for analytics. No personal data collected.