docs/channels/signal.md
Signal is a downloadable channel plugin (@openclaw/signal). The gateway talks to signal-cli over HTTP: either the native daemon (JSON-RPC + SSE) or the bbernhard/signal-cli-rest-api container (REST + WebSocket). OpenClaw does not embed libsignal.
signal-cli account.openclaw plugins install @openclaw/signal
Bare plugin specs try ClawHub first, then npm fallback. Force a source with openclaw plugins install clawhub:@openclaw/signal or npm:@openclaw/signal. plugins install registers and enables the plugin; no separate enable step is needed. See Plugins for general install rules.
Minimal config:
{
channels: {
signal: {
enabled: true,
account: "+15551234567",
cliPath: "signal-cli",
dmPolicy: "pairing",
allowFrom: ["+15557654321"],
},
},
}
| Field | Description |
|---|---|
account | Bot phone number in E.164 format (+15551234567) |
cliPath | Path to signal-cli (signal-cli if on PATH) |
configPath | signal-cli config dir passed as --config |
dmPolicy | DM access policy (pairing recommended) |
allowFrom | Phone numbers or uuid:<id> values allowed to DM |
Multi-account support: use channels.signal.accounts with per-account config and optional name. See Multi-account channels for the shared pattern.
agent:<agentId>:signal:group:<groupId>)./config set|unset (requires commands.config: true). Disable with channels.signal.configWrites: false.signal-cli (JVM or native build), or let openclaw channels add install it for you.signal-cli link -n "OpenClaw", then scan the QR in Signal.Use this for a dedicated bot number instead of linking an existing Signal app account. The flow below is tested on Ubuntu 24.
signal-cli on the gateway host:VERSION=$(curl -Ls -o /dev/null -w %{url_effective} https://github.com/AsamK/signal-cli/releases/latest | sed -e 's/^.*\/v//')
curl -L -O "https://github.com/AsamK/signal-cli/releases/download/v${VERSION}/signal-cli-${VERSION}-Linux-native.tar.gz"
sudo tar xf "signal-cli-${VERSION}-Linux-native.tar.gz" -C /opt
sudo ln -sf /opt/signal-cli /usr/local/bin/
signal-cli --version
If you use the JVM build (signal-cli-${VERSION}.tar.gz), install a JRE first. Keep signal-cli updated; upstream notes old releases can break as Signal server APIs change.
signal-cli -a +<BOT_PHONE_NUMBER> register
If captcha is required (browser access is needed to complete this step):
https://signalcaptchas.org/registration/generate.html.signalcaptcha://... link target from "Open Signal".signal-cli -a +<BOT_PHONE_NUMBER> register --captcha '<SIGNALCAPTCHA_URL>'
signal-cli -a +<BOT_PHONE_NUMBER> verify <VERIFICATION_CODE>
# If you run the gateway as a user systemd service:
systemctl --user restart openclaw-gateway.service
# Then verify:
openclaw doctor
openclaw channels status --probe
openclaw pairing approve signal <PAIRING_CODE>.Upstream references:
signal-cli README: https://github.com/AsamK/signal-clihttps://github.com/AsamK/signal-cli/wiki/Registration-with-captchahttps://github.com/AsamK/signal-cli/wiki/Linking-other-devices-(Provisioning)To manage signal-cli yourself (slow JVM cold starts, container init, shared CPUs), run the daemon separately and point OpenClaw at it:
{
channels: {
signal: {
httpUrl: "http://127.0.0.1:8080",
autoStart: false,
},
},
}
This skips auto-spawn and OpenClaw's startup wait. For slow auto-spawned starts, set channels.signal.startupTimeoutMs.
Instead of running signal-cli natively, use the bbernhard/signal-cli-rest-api Docker container, which wraps signal-cli behind a REST + WebSocket interface.
Requirements:
MODE=json-rpc for real-time message receiving.Example docker-compose.yml service:
signal-cli:
image: bbernhard/signal-cli-rest-api:latest
environment:
MODE: json-rpc
ports:
- "8080:8080"
volumes:
- signal-cli-data:/home/.local/share/signal-cli
OpenClaw config:
{
channels: {
signal: {
enabled: true,
account: "+15551234567",
httpUrl: "http://signal-cli:8080",
autoStart: false,
apiMode: "container", // or "auto" to detect automatically
},
},
}
apiMode controls which protocol OpenClaw uses:
| Value | Behavior |
|---|---|
"auto" | (Default) Probes both transports; streaming validates container WebSocket receive |
"native" | Force native signal-cli (JSON-RPC at /api/v1/rpc, SSE at /api/v1/events) |
"container" | Force bbernhard container (REST at /v2/send, WebSocket at /v1/receive/{account}) |
When apiMode is "auto", OpenClaw caches the detected mode for 30 seconds per daemon URL to avoid repeated probes (native wins when both transports are healthy). Container receive is only selected for streaming after /v1/receive/{account} upgrades to WebSocket, which requires MODE=json-rpc.
Container mode supports the same Signal operations as native mode where the container exposes matching APIs: sends, receives, attachments, typing indicators, read/viewed receipts, reactions, groups, and styled text. OpenClaw translates native Signal RPC calls into the container's REST payloads, including group.{base64(internal_id)} group IDs and text_mode: "styled" for formatted text.
Operational notes:
autoStart: false with container mode; OpenClaw should not spawn a native daemon when apiMode: "container" is selected.MODE=json-rpc for receiving. MODE=normal can make /v1/about look healthy, but /v1/receive/{account} will not WebSocket-upgrade, so OpenClaw will not select container receive streaming in auto mode.apiMode: "container" when httpUrl points at the bbernhard REST API, "native" when it points at native signal-cli JSON-RPC/SSE, and "auto" when the deployment may vary.Content-Length, and while streaming otherwise.DMs:
channels.signal.dmPolicy = "pairing".openclaw pairing list signal and openclaw pairing approve signal <CODE>.sourceUuid) are stored as uuid:<id> in channels.signal.allowFrom.Groups:
channels.signal.groupPolicy = open | allowlist | disabled.channels.signal.groupAllowFrom controls which groups or senders can trigger group replies when allowlist is set; entries can be Signal group IDs (raw, group:<id>, or signal:group:<id>), sender phone numbers, uuid:<id> values, or *.channels.signal.groups["<group-id>" | "*"] can override group behavior with requireMention, tools, and toolsBySender.channels.signal.accounts.<id>.groups for per-account overrides in multi-account setups.groupAllowFrom does not disable mention gating by itself. A specifically configured channels.signal.groups["<group-id>"] entry processes every group message unless requireMention: true is explicitly set.channels.signal is completely missing, runtime falls back to groupPolicy="allowlist" for group checks (even if channels.defaults.groupPolicy is set).signal-cli runs as a daemon; the gateway reads events via SSE.channels.signal.replyToMode = off | first | all | batched, or channels.signal.replyToModeByChatType.direct/group for per-chat-type overrides. Account-level values under channels.signal.accounts.<id> take precedence.channels.signal.textChunkLimit (default 4000).channels.signal.chunkMode="newline" to split on blank lines (paragraph boundaries) before length chunking.signal-cli).signal-cli filename as a MIME fallback when contentType is missing, so audio transcription can still classify AAC voice memos.channels.signal.mediaMaxMb (default 8).channels.signal.ignoreAttachments to skip downloading media.channels.signal.historyLimit (or channels.signal.accounts.*.historyLimit), falling back to messages.groupChat.historyLimit. Set 0 to disable (default 50).signal-cli sendTyping and refreshes them while a reply is running.channels.signal.sendReadReceipts is true, OpenClaw forwards read receipts for allowed DMs.signal-cli does not expose read receipts for groups.Set messages.statusReactions.enabled: true to let Signal show the shared queued/thinking/tool/compaction/done/error reaction lifecycle on inbound turns. Signal uses the inbound message timestamp as the reaction target; group reactions are sent with the Signal group ID plus the original sender as the target author.
Status reactions also require an ack reaction and a matching messages.ackReactionScope (direct, group-all, group-mentions, or all). Set channels.signal.reactionLevel: "off" to disable Signal status reactions.
messages.removeAckAfterReply: true clears the final status reaction after the configured hold time. Otherwise Signal restores the initial ack reaction after the final done/error state.
Use message action=react with channel=signal.
uuid:<id> from pairing output; a bare UUID also works).messageId is the Signal timestamp for the message you're reacting to.targetAuthor or targetAuthorUuid.message action=react channel=signal target=uuid:123e4567-e89b-12d3-a456-426614174000 messageId=1737630212345 emoji=🔥
message action=react channel=signal target=+15551234567 messageId=1737630212345 emoji=🔥 remove=true
message action=react channel=signal target=signal:group:<groupId> targetAuthor=uuid:<sender-uuid> messageId=1737630212345 emoji=✅
Config:
channels.signal.actions.reactions: enable/disable reaction actions (default true).channels.signal.reactionLevel: off | ack | minimal | extensive (default minimal).
off/ack disables agent reactions (message tool react errors).minimal/extensive enables agent reactions and sets the guidance level.channels.signal.accounts.<id>.actions.reactions, channels.signal.accounts.<id>.reactionLevel.Signal exec and plugin approval prompts use the top-level approvals.exec and approvals.plugin routing blocks. Signal has no channels.signal.execApprovals block.
👍 approves once.👎 denies./approve <id> allow-always when a request offers persistent approval.Approval reaction resolution requires explicit Signal approvers from channels.signal.allowFrom, channels.signal.defaultTo, or the matching account-level fields. Direct same-chat exec approval prompts can still suppress the duplicate local /approve fallback without explicit approvers; no-approver group approvals keep the local fallback visible.
signal:+15551234567 (or plain E.164).uuid:<id> (or bare UUID).signal:group:<groupId>.username:<name> (if supported by your Signal account).Configure aliases for stable names on recurring Signal targets. Aliases are OpenClaw-side config only; they do not create or edit Signal contacts.
{
channels: {
signal: {
aliases: {
me: "+15557654321",
jane: "uuid:123e4567-e89b-12d3-a456-426614174000",
ops: "group:<groupId>",
},
defaultTo: "signal:me",
},
},
}
Use aliases anywhere Signal delivery targets are accepted:
openclaw message send --channel signal --target signal:ops --message "Deployment is complete"
Per-account aliases inherit the top-level aliases and can add or override names:
{
channels: {
signal: {
aliases: {
me: "+15557654321",
},
accounts: {
work: {
aliases: {
ops: "group:<workGroupId>",
},
},
},
},
},
}
openclaw directory peers list --channel signal and openclaw directory groups list --channel signal list configured aliases. The Signal directory is config-backed; it does not live-query Signal contacts or mutate the Signal account.
Run this ladder first:
openclaw status
openclaw gateway status
openclaw logs --follow
openclaw doctor
openclaw channels status --probe
Then confirm DM pairing state if needed:
openclaw pairing list signal
Common failures:
httpUrl, account) and receive mode.openclaw doctor --fix.channels.signal.enabled: true.Extra checks:
openclaw pairing list signal
pgrep -af signal-cli
grep -i "signal" "/tmp/openclaw/openclaw-$(date +%Y-%m-%d).log" | tail -20
For triage flow: Channels Troubleshooting.
signal-cli stores account keys locally (typically ~/.local/share/signal-cli/data/).channels.signal.dmPolicy: "pairing" unless you explicitly want broader DM access.Full configuration: Configuration
Provider options:
channels.signal.enabled: enable/disable channel startup.channels.signal.apiMode: auto | native | container (default: auto). See Container mode.channels.signal.account: E.164 for the bot account.channels.signal.cliPath: path to signal-cli.channels.signal.configPath: optional signal-cli --config directory.channels.signal.httpUrl: full daemon URL (overrides host/port).channels.signal.httpHost, channels.signal.httpPort: daemon bind (default 127.0.0.1:8080).channels.signal.autoStart: auto-spawn daemon (default true if httpUrl unset).channels.signal.startupTimeoutMs: startup wait timeout in ms (min 1000, cap 120000; default 30000).channels.signal.receiveMode: on-start | manual.channels.signal.ignoreAttachments: skip attachment downloads.channels.signal.ignoreStories: ignore stories from the daemon.channels.signal.sendReadReceipts: forward read receipts.channels.signal.dmPolicy: pairing | allowlist | open | disabled (default: pairing).channels.signal.allowFrom: DM allowlist (E.164 or uuid:<id>). open requires "*". Signal has no usernames; use phone/UUID IDs.channels.signal.aliases: OpenClaw-side aliases for DM or group delivery targets.channels.signal.groupPolicy: open | allowlist | disabled (default: allowlist).channels.signal.groupAllowFrom: group allowlist; accepts Signal group IDs (raw, group:<id>, or signal:group:<id>), sender E.164 numbers, or uuid:<id> values.channels.signal.groups: per-group overrides keyed by Signal group ID (or "*"). Supported fields: requireMention, tools, toolsBySender.channels.signal.accounts.<id>.groups: per-account version of channels.signal.groups for multi-account setups.channels.signal.accounts.<id>.aliases: per-account aliases, merged with top-level aliases.channels.signal.replyToMode: native reply quote mode, off | first | all | batched (default: all).channels.signal.replyToModeByChatType.direct, channels.signal.replyToModeByChatType.group: per-chat-type native reply quote overrides.channels.signal.accounts.<id>.replyToMode, channels.signal.accounts.<id>.replyToModeByChatType.direct, channels.signal.accounts.<id>.replyToModeByChatType.group: per-account reply quote overrides.channels.signal.historyLimit: max group messages to include as context (0 disables).channels.signal.dmHistoryLimit: DM history limit in user turns. Per-user overrides: channels.signal.dms["<phone_or_uuid>"].historyLimit.channels.signal.textChunkLimit: outbound chunk size in characters (default 4000).channels.signal.chunkMode: length (default) or newline to split on blank lines (paragraph boundaries) before length chunking.channels.signal.mediaMaxMb: inbound/outbound media cap in MB (default 8).channels.signal.reactionLevel: off | ack | minimal | extensive (default minimal). See Reactions.channels.signal.reactionNotifications: off | own | all | allowlist (default own) - when the agent is notified of incoming reactions from others.channels.signal.reactionAllowlist: senders whose reactions notify the agent when reactionNotifications: "allowlist".channels.signal.blockStreaming, channels.signal.blockStreamingCoalesce: block-mode streaming controls shared across channels. See Streaming.Related global options:
agents.list[].groupChat.mentionPatterns (Signal does not support native mentions).messages.groupChat.mentionPatterns (global fallback).messages.responsePrefix.