Signal Channel Plugin Setup and Configuration
Learn how to install and configure the Signal channel plugin for OpenClaw, including number model, setup paths, and using signal-cli native or container.
Read this when
- Setting up Signal support
- Debugging Signal send/receive
Signal is a channel plugin that you install separately (@openclaw/signal). The gateway communicates with signal-cli through HTTP, using either the native daemon (JSON-RPC plus SSE) or the bbernhard/signal-cli-rest-api container (REST with WebSocket). OpenClaw does not bundle libsignal.
The number model (read this first)
- The gateway attaches to a Signal device, specifically the
signal-cliaccount. - When you run the bot on your personal Signal account, it skips your own messages to prevent loops.
- To get "I message the bot and it responds," set up a dedicated bot number.
Install
openclaw plugins install @openclaw/signal
When a plugin spec is bare, ClawHub is tried first, then npm as a fallback. You can force a source using openclaw plugins install clawhub:@openclaw/signal or npm:@openclaw/signal. Running plugins install registers and activates the plugin, so no separate enable command is required. General installation guidance lives in Plugins.
Quick setup
Pick a number
Give the bot its own Signal number (recommended).
Install the plugin
openclaw plugins install @openclaw/signal
Run the guided setup
openclaw channels add
The wizard checks whether signal-cli is present on PATH; if it is not, installation is offered: the official native GraalVM build is downloaded on Linux x86-64, while macOS and other architectures get it through Homebrew. After that, it asks for the bot number and the signal-cli location.
For setups that are not interactive, openclaw channels add --channel signal can take --signal-number <e164> to specify the bot phone number, along with --http-host <host> and --http-port <port> for the Signal daemon endpoint, which defaults to 127.0.0.1:8080.
Link or register the account
- QR link (fastest): run
signal-cli link -n "OpenClaw", then scan it with Signal. Details are in Path A. - SMS registration: a dedicated number that goes through captcha plus SMS verification. See Path B.
Verify and pair
openclaw gateway call channels.status --params '{"probe":true}'
Send an initial DM and confirm the pairing with openclaw pairing approve signal <CODE>.
A minimal configuration looks like this:
{
channels: {
signal: {
enabled: true,
account: "+15551234567",
transport: {
kind: "managed-native",
cliPath: "signal-cli",
},
dmPolicy: "pairing",
allowFrom: ["+15557654321"],
},
},
}
| Field | Description |
|---|---|
account | Bot phone number in E.164 format (+15551234567) |
transport | Signal connection and process mode owned by the account |
dmPolicy | Who is allowed to DM (pairing is suggested) |
allowFrom | Phone numbers or uuid:<id> entries permitted to DM |
For multiple accounts, use channels.signal.accounts with per-account settings and optionally name. Every named account has its own transport; it does not pick up the top-level transport. The implicit default account is the only one that uses the top-level transport. The shared approach is covered in Multi-account channels.
When account-level dmPolicy and groupPolicy are not set, they fall back to the channel root; explicit account settings take precedence. If neither level defines them, DMs get pairing and groups get allowlist.
What it is
- Routing is deterministic: replies always go back through Signal.
- DMs share the agent's main session; with the default
session.groupScope: "per-group", groups stay separate (agent:<agentId>:signal:group:<groupId>). - Signal may, by default, write config updates triggered by
/config set|unset(which needscommands.config: true). Turn this off withchannels.signal.configWrites: false.
Setup path A: link existing Signal account (QR)
- Get
signal-cli(JVM or native build), or haveopenclaw channels addinstall it. - Link a bot account: run
signal-cli link -n "OpenClaw", then scan the QR code in Signal. - Set up Signal and launch the gateway.
Setup path B: register dedicated bot number (SMS, Linux)
For a dedicated bot number rather than connecting an existing Signal app account, use this approach. The steps below were validated on Ubuntu 24.
- Obtain a number capable of receiving SMS, or voice verification for landline numbers. Using a dedicated bot number prevents conflicts with existing account or session state.
- On the gateway host, install
signal-cli:
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
When using the JVM build (signal-cli-${VERSION}.tar.gz), a JRE must be installed beforehand. Keep signal-cli current; the upstream project notes that older releases may fail as Signal server APIs evolve.
- Register the number and complete verification:
signal-cli -a +<BOT_PHONE_NUMBER> register
If a captcha is required, browser access is necessary to finish this step:
- Open
https://signalcaptchas.org/registration/generate.html. - Solve the captcha, then copy the
signalcaptcha://...link target from the "Open Signal" option. - Run from the same external IP as the browser session when feasible, since captcha tokens expire quickly.
- Register and verify without delay:
signal-cli -a +<BOT_PHONE_NUMBER> register --captcha '<SIGNALCAPTCHA_URL>'
signal-cli -a +<BOT_PHONE_NUMBER> verify <VERIFICATION_CODE>
- Set up OpenClaw, restart the gateway, and confirm the channel works:
# If you run the gateway as a user systemd service:
systemctl --user restart openclaw-gateway.service
# Then verify:
openclaw doctor
openclaw channels status --probe
- Pair your DM sender:
- Send any message to the bot number.
- Approve it on the server with
openclaw pairing approve signal <PAIRING_CODE>. - Add the bot number to your phone contacts so it doesn't appear as "Unknown contact".
Warning
Using
signal-clito register a phone number account can invalidate the main Signal app session tied to that number. A dedicated bot number is preferred, or use QR link mode to preserve your current phone app configuration.
Upstream references:
signal-cliREADME:https://github.com/AsamK/signal-cli- Captcha flow:
https://github.com/AsamK/signal-cli/wiki/Registration-with-captcha - Linking flow:
https://github.com/AsamK/signal-cli/wiki/Linking-other-devices-(Provisioning)
External native daemon mode
To operate signal-cli yourself, which is useful when cold starts are slow, container initialization takes time, or CPUs are shared, run the daemon separately and direct OpenClaw to it:
For non-interactive setups, explicitly choose the endpoint kind when required:
openclaw channels add --channel signal --signal-number +15551234567 \
--http-url http://127.0.0.1:8080 --signal-transport external-native
{
channels: {
signal: {
transport: {
kind: "external-native",
url: "http://127.0.0.1:8080",
},
},
},
}
This bypasses auto-spawn and OpenClaw's startup wait. For a managed daemon that starts slowly, configure channels.signal.transport.startupTimeoutMs.
Container mode (bbernhard/signal-cli-rest-api)
Rather than running signal-cli natively, deploy the bbernhard/signal-cli-rest-api Docker container, which exposes signal-cli through a REST and WebSocket interface.
openclaw channels add --channel signal --signal-number +15551234567 \
--http-url http://signal-cli:8080 --signal-transport container
Requirements:
- The container must be run with
MODE=json-rpcto receive messages in real time. - Register or link your Signal account inside the container before OpenClaw connects to it.
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",
transport: {
kind: "container",
url: "http://signal-cli:8080",
},
},
},
}
The protocol and process lifecycle OpenClaw uses is determined by transport.kind:
| Value | Behavior |
|---|---|
"managed-native" | Launch native signal-cli and use JSON-RPC at /api/v1/rpc with SSE at /api/v1/events; url can pick a connection endpoint different from the daemon bind |
"external-native" | Connect to a native signal-cli daemon that is already running |
"container" | Connect to bbernhard REST at /v2/send and WebSocket at /v1/receive/{account} |
During setup and openclaw doctor --fix, an existing endpoint may be probed once to determine its concrete kind. Runtime operations do not auto-detect or switch protocols.
Container mode supports the same Signal operations as native mode when 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:
- Use
MODE=json-rpcfor receiving.MODE=normalcan make/v1/aboutappear healthy, but/v1/receive/{account}will not WebSocket-upgrade, so container receive streaming will fail its probe. - Set
kind: "container"for the bbernhard REST API andkind: "external-native"for nativesignal-cliJSON-RPC/SSE. - Container attachment downloads respect the same media byte limits as native mode. Oversized responses are rejected before being fully buffered when the server sends
Content-Length, and while streaming otherwise.
Access control (DMs + groups)
DMs:
- Default:
channels.signal.dmPolicy = "pairing". - Unknown senders receive a pairing code; messages are ignored until approval, and codes expire after 1 hour.
- Approve with
openclaw pairing list signalandopenclaw pairing approve signal <CODE>. - Pairing is the default token exchange for Signal DMs. Details: Pairing
- UUID-only senders (from
sourceUuid) are stored asuuid:<id>inchannels.signal.allowFrom.
Groups:
channels.signal.groupPolicy = open | allowlist | disabled.channels.signal.groupAllowFromdecides which groups or senders are allowed to trigger group replies whenallowlistis active; entries can be Signal group IDs (raw,group:<id>, orsignal:group:<id>), sender phone numbers,uuid:<id>values, or*.- Group behavior can be overridden using
channels.signal.groups["<group-id>" | "*"]withrequireMention,tools, andtoolsBySender. - For multi-account deployments, apply per-account overrides via
channels.signal.accounts.<id>.groups. - Adding a Signal group to the allowlist through
groupAllowFromalone does not turn off mention gating. A dedicatedchannels.signal.groups["<group-id>"]entry handles every group message unlessrequireMention=trueis specified. - When
requireMention=trueis enabled, native @mentions from Signal are resolved using structured mention metadata against the bot account phone oraccountUuid. ConfiguredmentionPatternsstill serve as a plain-text fallback. - Runtime behavior: if
channels.signalis absent entirely, group checks fall back togroupPolicy="allowlist"at runtime (even whenchannels.defaults.groupPolicyis configured).
Mention-gated group with bounded context:
{
channels: {
signal: {
account: "+15551234567",
accountUuid: "bot-signal-uuid",
groupPolicy: "allowlist",
groupAllowFrom: ["group:<signal-group-id>"],
historyLimit: 8,
groups: {
"<signal-group-id>": { requireMention: true },
},
},
},
messages: {
groupChat: {
mentionPatterns: ["\\bopenclaw\\b"],
},
},
}
Group messages that pass the allowlist but omit a bot mention produce no output and are retained only within the bounded pending history window. Once a later native @mention or fallback text mention activates the bot, OpenClaw pulls in that recent context and responds to the same group. Attachment bodies from skipped messages are never downloaded; they can show up only as compact media placeholders inside the pending context.
How it works (behavior)
- Native mode:
signal-clioperates as a daemon; the gateway consumes events through SSE. - Container mode: the gateway sends via REST API and listens over WebSocket.
- Inbound messages are converted into the shared channel envelope.
- Replies are always directed back to the originating number or group.
- When the backend accepts the inbound timestamp and author, replies to inbound messages include native Signal quote metadata; if that metadata is absent or rejected, OpenClaw delivers the reply as a standard message.
- Native quote usage is configured with
channels.signal.replyToMode = off | first | all | batched, orchannels.signal.replyToModeByChatType.direct/groupfor per-chat-type overrides. Account-level settings underchannels.signal.accounts.<id>take priority.
Media + limits
- Outbound text gets chunked to
channels.signal.textChunkLimit(default 4000). - Optional newline chunking: set
channels.signal.streaming.chunkMode="newline"to break on blank lines (paragraph boundaries) before applying length-based chunking. - Attachments are supported (base64 retrieved from
signal-cli). - Voice-note attachments rely on the
signal-clifilename as a MIME fallback whencontentTypeis absent, allowing audio transcription to still classify AAC voice memos. - Default media cap:
channels.signal.mediaMaxMb(default 8). - Use
channels.signal.ignoreAttachmentsto prevent media downloads for any transport. - Group history context draws on
channels.signal.historyLimit(orchannels.signal.accounts.*.historyLimit), with a fallback tomessages.groupChat.historyLimit. Set0to turn it off (default 50).
Typing + read receipts
- Typing indicators: OpenClaw emits typing signals via
signal-cli sendTypingand keeps refreshing them while a reply is in progress. - Read receipts: when
channels.signal.sendReadReceiptsis true, OpenClaw forwards read receipts for allowed DMs. signal-clidoes not provide read receipts for groups.
Lifecycle status reactions
Set messages.statusReactions.enabled: true so Signal displays the shared queued/thinking/tool/compaction/done/error reaction lifecycle on inbound turns. Signal targets the inbound message timestamp for reactions; group reactions are sent with the Signal group ID plus the original sender as the target author.
Status reactions also demand an ack reaction and a matching messages.ackReactionScope (direct, group-all, group-mentions, or all). Configure channels.signal.reactionLevel: "off" to disable Signal status reactions.
Signal restores the initial ack reaction after the final done/error state.
Reactions (message tool)
Combine message action=react with channel=signal.
- Targets: sender E.164 or UUID (use
uuid:<id>from pairing output; a bare UUID works as well). messageIdholds the Signal timestamp for the message you're reacting to.- Group reactions require
targetAuthorortargetAuthorUuid.
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: toggles reaction actions on or off (default true).channels.signal.reactionLevel:off | ack | minimal | extensive(defaultminimal).- Setting
off/ackturns off agent reactions, causing message toolreacterrors. - Using
minimal/extensiveactivates agent reactions and specifies the guidance level.
- Setting
- Overrides at the account level:
channels.signal.accounts.<id>.actions.reactions,channels.signal.accounts.<id>.reactionLevel.
Approval reactions
For Signal exec and plugin approval prompts, the routing blocks at the top level, approvals.exec and approvals.plugin, are applied. A dedicated channels.signal.execApprovals block does not exist for Signal.
๐grants a one-time approval.๐rejects the request.- When a request supports persistent approval,
/approve <id> allow-alwaysis the option to choose.
To resolve approval reactions, explicit Signal approvers must come from channels.signal.allowFrom, channels.signal.defaultTo, or the corresponding account-level fields. In direct same-chat exec approval prompts, the duplicate local /approve fallback can be suppressed without explicit approvers; however, for group approvals lacking approvers, the local fallback remains visible.
Question reactions
When an ask_user prompt contains a single non-secret, single-select question with one to four options, Signal displays 1๏ธโฃ through 4๏ธโฃ next to each option label. Answer by reacting to the delivered prompt with the number that matches your choice. OpenClaw checks that the reaction is on the bot-authored message, then translates the number to the canonical option via the Gateway. Reactions that are stale or repeated are discarded. Prompts with multiple questions, multi-select fields, or free-text responses still require a text reply; standard Signal DM/group admission rules determine sender authorization.
Delivery targets (CLI/cron)
- For DMs:
signal:+15551234567(or a plain E.164 number). - For UUID DMs:
uuid:<id>(or a bare UUID). - For groups:
signal:group:<groupId>. - For usernames:
username:<name>(if your Signal account supports it).
Aliases
To keep stable names on recurring Signal targets, set up aliases. These aliases exist only in OpenClaw's configuration; they do not create or modify Signal contacts.
{
channels: {
signal: {
aliases: {
me: "+15557654321",
jane: "uuid:123e4567-e89b-12d3-a456-426614174000",
ops: "group:<groupId>",
},
defaultTo: "signal:me",
},
},
}
Aliases can be used anywhere Signal delivery targets are accepted:
openclaw message send --channel signal --target signal:ops --message "Deployment is complete"
Account-level aliases inherit the top-level ones and can also add or override names:
{
channels: {
signal: {
aliases: {
me: "+15557654321",
},
accounts: {
work: {
aliases: {
ops: "group:<workGroupId>",
},
},
},
},
},
}
To list configured aliases, use openclaw directory peers list --channel signal and openclaw directory groups list --channel signal. The Signal directory is configuration-backed; it does not perform live queries of Signal contacts or make changes to the Signal account.
Troubleshooting
Start with this ladder:
openclaw status
openclaw gateway status
openclaw logs --follow
openclaw doctor
openclaw channels status --probe
Then, if needed, verify the DM pairing state:
openclaw pairing list signal
Typical issues:
- Daemon is reachable but no replies come back: check
account,transport.kind, the transport URL, and receive mode. - DMs are ignored: the sender has not yet passed pairing approval.
- Group messages are ignored: gating on group sender or mention blocks delivery.
- Config validation errors after edits: run
openclaw doctor --fix. - Signal absent from diagnostics: confirm
channels.signal.enabled: true.
Additional checks:
openclaw pairing list signal
pgrep -af signal-cli
openclaw logs --plain --limit 500 | grep -i "signal" | tail -20
For a triage walkthrough, see Channels Troubleshooting.
Security notes
- Account keys are stored locally by
signal-cli(usually in~/.local/share/signal-cli/data/). - Before migrating servers or rebuilding, back up the Signal account state.
- Keep
channels.signal.dmPolicy: "pairing"unless you intentionally want broader DM access. - SMS verification is required only for registration or recovery, but losing control of the number or account can make re-registration harder.
Configuration reference (Signal)
Complete configuration: Configuration
Provider options:
channels.signal.enabled: controls whether the channel starts up.channels.signal.account: the bot account's E.164 identifier.channels.signal.accountUuid: optional bot account UUID, used for native @mention detection and loop protection.channels.signal.transport: transport owned by the account. Leave it out to use managed native defaults.channels.signal.transport.kind:managed-native | external-native | container.channels.signal.transport.url: mandatory forexternal-nativeandcontainer; only needed formanaged-nativewhen its connection endpoint is different from the daemon bind.channels.signal.transport.cliPath: managed-native route tosignal-cli.channels.signal.transport.configPath: optional managed-nativesignal-cli --configfolder.channels.signal.transport.httpHost,channels.signal.transport.httpPort: managed-native daemon bind address (defaults to127.0.0.1:8080).channels.signal.transport.startupTimeoutMs: managed-native startup wait in milliseconds (minimum 1000, maximum 120000; default 30000).channels.signal.transport.receiveMode: managed-nativeon-start | manual.channels.signal.ignoreAttachments: skip downloading inbound attachments for this account.channels.signal.transport.ignoreStories: managed-native story setting.channels.signal.sendReadReceipts: forward read receipts.channels.signal.dmPolicy:pairing | allowlist | open | disabled(default: pairing).channels.signal.allowFrom: DM allowlist (E.164 oruuid:<id>).openneeds"*". Signal does not support usernames, so 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, accepting Signal group IDs (raw,group:<id>, orsignal:group:<id>), sender E.164 numbers, oruuid:<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 variant ofchannels.signal.groupsfor multi-account configurations.channels.signal.accounts.<id>.aliases: per-account aliases, combined 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: maximum group messages included as context (0 turns it off).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.streaming.chunkMode:length(default) ornewlineto break on blank lines (paragraph boundaries) before length-based chunking.channels.signal.mediaMaxMb: inbound/outbound media cap in MB (default 8).channels.signal.reactionLevel:off | ack | minimal | extensive(defaultminimal). For details, refer to Reactions.channels.signal.reactionNotifications:off | own | all | allowlist(defaultown), which controls when incoming reactions from others trigger notifications to the agent.channels.signal.reactionAllowlist: defines which senders' reactions notify the agent whenreactionNotifications: "allowlist".channels.signal.streaming.block.enabled,channels.signal.streaming.block.coalesce: channel-wide block-mode streaming settings. Check Streaming for more.
Global options that apply here:
agents.entries.*.groupChat.mentionPatterns(used as a plain-text fallback; when the bot account identity is set, Signal's native @mentions are picked up from structured metadata).messages.groupChat.mentionPatterns(global fallback).channels.signal.responsePrefixor an account-specificresponsePrefix.
Related
- Channels Overview - every channel supported
- Pairing - DM authentication and the pairing procedure
- Groups - group chat behavior and mention restrictions
- Channel Routing - how messages are routed across sessions
- Security - access model and hardening measures