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-cli account.
  • 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.

  • 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"],
    },
  },
}
FieldDescription
accountBot phone number in E.164 format (+15551234567)
transportSignal connection and process mode owned by the account
dmPolicyWho is allowed to DM (pairing is suggested)
allowFromPhone 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 needs commands.config: true). Turn this off with channels.signal.configWrites: false.
  1. Get signal-cli (JVM or native build), or have openclaw channels add install it.
  2. Link a bot account: run signal-cli link -n "OpenClaw", then scan the QR code in Signal.
  3. 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.

  1. 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.
  2. 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.

  1. 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:

  1. Open https://signalcaptchas.org/registration/generate.html.
  2. Solve the captcha, then copy the signalcaptcha://... link target from the "Open Signal" option.
  3. Run from the same external IP as the browser session when feasible, since captcha tokens expire quickly.
  4. Register and verify without delay:
signal-cli -a +<BOT_PHONE_NUMBER> register --captcha '<SIGNALCAPTCHA_URL>'
signal-cli -a +<BOT_PHONE_NUMBER> verify <VERIFICATION_CODE>
  1. 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
  1. 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-cli to 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-cli README: 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-rpc to 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:

ValueBehavior
"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-rpc for receiving. MODE=normal can make /v1/about appear 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 and kind: "external-native" for native signal-cli JSON-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 signal and openclaw pairing approve signal <CODE>.
  • Pairing is the default token exchange for Signal DMs. Details: Pairing
  • UUID-only senders (from sourceUuid) are stored as uuid:<id> in channels.signal.allowFrom.

Groups:

  • channels.signal.groupPolicy = open | allowlist | disabled.
  • channels.signal.groupAllowFrom decides which groups or senders are allowed to trigger group replies when allowlist is active; entries can be Signal group IDs (raw, group:<id>, or signal:group:<id>), sender phone numbers, uuid:<id> values, or *.
  • Group behavior can be overridden using channels.signal.groups["<group-id>" | "*"] with requireMention, tools, and toolsBySender.
  • For multi-account deployments, apply per-account overrides via channels.signal.accounts.<id>.groups.
  • Adding a Signal group to the allowlist through groupAllowFrom alone does not turn off mention gating. A dedicated channels.signal.groups["<group-id>"] entry handles every group message unless requireMention=true is specified.
  • When requireMention=true is enabled, native @mentions from Signal are resolved using structured mention metadata against the bot account phone or accountUuid. Configured mentionPatterns still serve as a plain-text fallback.
  • Runtime behavior: if channels.signal is absent entirely, group checks fall back to groupPolicy="allowlist" at runtime (even when channels.defaults.groupPolicy is 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-cli operates 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, or channels.signal.replyToModeByChatType.direct/group for per-chat-type overrides. Account-level settings under channels.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-cli filename as a MIME fallback when contentType is absent, allowing audio transcription to still classify AAC voice memos.
  • Default media cap: channels.signal.mediaMaxMb (default 8).
  • Use channels.signal.ignoreAttachments to prevent media downloads for any transport.
  • Group history context draws on channels.signal.historyLimit (or channels.signal.accounts.*.historyLimit), with a fallback to messages.groupChat.historyLimit. Set 0 to turn it off (default 50).

Typing + read receipts

  • Typing indicators: OpenClaw emits typing signals via signal-cli sendTyping and keeps refreshing them while a reply is in progress.
  • Read receipts: when channels.signal.sendReadReceipts is true, OpenClaw forwards read receipts for allowed DMs.
  • signal-cli does 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).
  • messageId holds the Signal timestamp for the message you're reacting to.
  • Group reactions require 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: toggles reaction actions on or off (default true).
  • channels.signal.reactionLevel: off | ack | minimal | extensive (default minimal).
    • Setting off/ack turns off agent reactions, causing message tool react errors.
    • Using minimal/extensive activates agent reactions and specifies the guidance level.
  • 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-always is 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 for external-native and container; only needed for managed-native when its connection endpoint is different from the daemon bind.
  • channels.signal.transport.cliPath: managed-native route to signal-cli.
  • channels.signal.transport.configPath: optional managed-native signal-cli --config folder.
  • channels.signal.transport.httpHost, channels.signal.transport.httpPort: managed-native daemon bind address (defaults to 127.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-native on-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 or uuid:<id>). open needs "*". 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>, 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 variant of channels.signal.groups for 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) or newline to 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 (default minimal). For details, refer to Reactions.
  • channels.signal.reactionNotifications: off | own | all | allowlist (default own), which controls when incoming reactions from others trigger notifications to the agent.
  • channels.signal.reactionAllowlist: defines which senders' reactions notify the agent when reactionNotifications: "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.responsePrefix or an account-specific responsePrefix.
3,194 words ยท updated Sep 1, 2026