Signal Channel Plugin Setup and Number Model

Learn how to install and configure the Signal channel plugin for OpenClaw. This page covers signal-cli integration, the number model, and setup steps for bot operators.

Read this when

  • Setting up Signal support
  • Debugging Signal send/receive

PAGE: Signal (docs path: channels/signal) SECTION 1 of 2

Signal is a downloadable channel plugin (@openclaw/signal). The gateway communicates with 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.

The number model (read this first)

  • The gateway connects to a Signal device: the signal-cli account.
  • Running the bot on your personal Signal account causes it to ignore your own messages (loop protection).
  • For "I text the bot and it replies," use a separate bot number.

Install

openclaw plugins install @openclaw/signal

Bare plugin specs check ClawHub first, then fall back to npm. 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 required. See Plugins for general install rules.

Quick setup

Pick a number

Use a separate Signal number for the bot (recommended).

Install the plugin

openclaw plugins install @openclaw/signal

Run the guided setup

openclaw channels add

The wizard detects whether signal-cli is on PATH and, when missing, offers to install it: downloads the official native GraalVM build on Linux x86-64, or installs via Homebrew on macOS and other architectures. It then prompts for the bot number and signal-cli path.

For non-interactive setup, openclaw channels add --channel signal also accepts --signal-number <e164> for the bot phone number, plus --http-host <host> and --http-port <port> for the Signal daemon endpoint (default 127.0.0.1:8080).

  • QR link (fastest): signal-cli link -n "OpenClaw", then scan with Signal. See Path A.
  • SMS registration: dedicated number with captcha + SMS verification. See Path B.

Verify and pair

openclaw gateway call channels.status --params '{"probe":true}'

Send a first DM and approve pairing: openclaw pairing approve signal <CODE>.

Minimal config:

{
  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)
transportAccount-owned Signal connection and process mode
dmPolicyDM access policy (pairing recommended)
allowFromPhone numbers or uuid:<id> values allowed to DM

Multi-account support: use channels.signal.accounts with per-account config and optional name. Each named account owns its transport; it does not inherit the top-level transport. The top-level transport belongs only to the implicit default account. See Multi-account channels for the shared pattern.

What it is

  • Deterministic routing: replies always go back to Signal.
  • DMs share the agent's main session; groups are isolated (agent:<agentId>:signal:group:<groupId>).
  • By default, Signal may write config updates triggered by /config set|unset (requires commands.config: true). Disable with channels.signal.configWrites: false.
  1. Install signal-cli (JVM or native build), or let openclaw channels add install it for you.
  2. Link a bot account: signal-cli link -n "OpenClaw", then scan the QR in Signal.
  3. Configure Signal and start the gateway.

Setup path B: register dedicated bot number (SMS, Linux)

Use this for a dedicated bot number instead of linking an existing Signal app account. The flow below is tested on Ubuntu 24.

  1. Get a number that can receive SMS (or voice verification for landlines). A dedicated bot number avoids account/session conflicts.
  2. Install 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.

  1. Register and verify the number:
signal-cli -a +<BOT_PHONE_NUMBER> register

If captcha is required (browser access is needed to complete this step):

  1. Open https://signalcaptchas.org/registration/generate.html.
  2. Complete the captcha, copy the signalcaptcha://... link target from "Open Signal".
  3. Run from the same external IP as the browser session when possible (captcha tokens expire quickly).
  4. Register and verify immediately:
signal-cli -a +<BOT_PHONE_NUMBER> register --captcha '<SIGNALCAPTCHA_URL>'
signal-cli -a +<BOT_PHONE_NUMBER> verify <VERIFICATION_CODE>
  1. Configure OpenClaw, restart the gateway, verify the channel:
# 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 on the server: openclaw pairing approve signal <PAIRING_CODE>.
    • Save the bot number as a contact on your phone to avoid "Unknown contact".

Warning

Registering a phone number account with signal-cli can de-authenticate the main Signal app session for that number. Prefer a dedicated bot number, or use QR link mode to keep your existing phone app setup.

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 manage signal-cli yourself (slow JVM cold starts, container init, shared CPUs), run the daemon separately and point OpenClaw at it:

For non-interactive setup, select the endpoint kind explicitly when needed:

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 skips auto-spawn and OpenClaw's startup wait. For a managed daemon with a slow start, set channels.signal.transport.startupTimeoutMs.

Container mode (bbernhard/signal-cli-rest-api)

Instead of running signal-cli natively, use the bbernhard/signal-cli-rest-api Docker container, which wraps signal-cli behind a REST + WebSocket interface.

openclaw channels add --channel signal --signal-number +15551234567 \
  --http-url http://signal-cli:8080 --signal-transport container

Requirements:

  • The container must run with MODE=json-rpc for real-time message receiving.
  • Register or link your Signal account inside the container before connecting OpenClaw.

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",
      },
    },
  },
}

transport.kind controls which protocol and process lifecycle OpenClaw uses:

ValueBehavior
"managed-native"Start native signal-cli and use JSON-RPC at /api/v1/rpc plus SSE at /api/v1/events; url may select a connection endpoint distinct from the daemon bind
"external-native"Connect to an already-running native signal-cli daemon
"container"Connect to bbernhard REST at /v2/send and WebSocket at /v1/receive/{account}

Setup and openclaw doctor --fix may probe an existing endpoint once to identify its concrete kind. Runtime operations do not auto-detect or switch protocols.

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:

  • Use MODE=json-rpc for receiving. MODE=normal can make /v1/about look 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 honor 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 get a pairing code; messages are ignored until approved (codes expire after 1 hour).
  • Approve via 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 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.
  • Use channels.signal.accounts.<id>.groups for per-account overrides in multi-account setups.
  • Allowlisting a Signal group through 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 set.
  • With requireMention=true, Signal native @mentions are matched from structured mention metadata against the bot account phone or accountUuid. Configured mentionPatterns remain a plain-text fallback.
  • Runtime note: if channels.signal is completely missing, runtime falls back to groupPolicy="allowlist" for group checks (even if channels.defaults.groupPolicy is set).

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"],
    },
  },
}

Allowed group messages that do not mention the bot stay silent and are kept only in the bounded pending history window. When a later native @mention or fallback text mention triggers the bot, OpenClaw includes that recent context and replies to the same group. Skipped attachment bodies are not downloaded; they may appear only as compact media placeholders in the pending context.

How it works (behavior)

  • Native mode: signal-cli runs as a daemon; the gateway reads events via SSE.
  • Container mode: the gateway sends via REST API and receives via WebSocket.
  • Inbound messages are normalized into the shared channel envelope.
  • Replies always route back to the same number or group.
  • Replies to inbound messages include native Signal quote metadata when the backend accepts the inbound timestamp and author; if quote metadata is missing or rejected, OpenClaw sends the reply as a normal message.
  • Configure native quote use with 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.

Media + limits

  • Outbound text is split into chunks of channels.signal.textChunkLimit (default 4000).
  • Optional blank line chunking: set channels.signal.streaming.chunkMode="newline" to break on paragraph boundaries before splitting by length.
  • Attachments are supported (base64 fetched from signal-cli).
  • Voice note attachments use the signal-cli filename as a MIME fallback when contentType is missing, so audio transcription can still classify AAC voice memos.
  • Default media limit: channels.signal.mediaMaxMb (default 8).
  • Use channels.signal.ignoreAttachments to skip downloading media for any transport.
  • Group history context uses channels.signal.historyLimit (or channels.signal.accounts.*.historyLimit), falling back to messages.groupChat.historyLimit. Set 0 to disable (default 50).

Typing + read receipts

  • Typing indicators: OpenClaw sends typing signals via signal-cli sendTyping and refreshes them while a reply is running.
  • Read receipts: when channels.signal.sendReadReceipts is true, OpenClaw forwards read receipts for allowed DMs.
  • signal-cli does not expose read receipts for groups.

Lifecycle status reactions

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.

Signal restores the initial ack reaction after the final done/error state.

Reactions (message tool)

Use message action=react with channel=signal.

  • Targets: sender E.164 or UUID (use uuid:<id> from pairing output; a bare UUID also works).
  • messageId is 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: 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.
  • Per-account overrides: channels.signal.accounts.<id>.actions.reactions, channels.signal.accounts.<id>.reactionLevel.

Approval reactions

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.
  • Use /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.

Question reactions

For an ask_user prompt with one non-secret, single-select question and one to four options, Signal shows 1๏ธโƒฃ through 4๏ธโƒฃ beside the option labels. React to the delivered prompt with the matching number to answer it. OpenClaw verifies the reaction targets the bot-authored message, then maps the number to the canonical option through the Gateway. Stale or duplicate taps are ignored. Multi-question, multi-select, and free-text prompts remain text-reply-only; normal Signal DM/group admission rules authorize the sender.

Delivery targets (CLI/cron)

  • DMs: signal:+15551234567 (or plain E.164).
  • UUID DMs: uuid:<id> (or bare UUID).
  • Groups: signal:group:<groupId>.
  • Usernames: username:<name> (if supported by your Signal account).

Aliases

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.

Troubleshooting

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:

  • Daemon reachable but no replies: verify account, transport.kind, the transport URL, and receive mode.
  • DMs ignored: sender is pending pairing approval.
  • Group messages ignored: group sender/mention gating blocks delivery.
  • Config validation errors after edits: run openclaw doctor --fix.
  • Signal missing from diagnostics: confirm channels.signal.enabled: true.

Extra checks:

openclaw pairing list signal
pgrep -af signal-cli
openclaw logs --plain --limit 500 | grep -i "signal" | tail -20

For triage flow: Channels Troubleshooting.

Security notes

  • signal-cli stores account keys locally (typically ~/.local/share/signal-cli/data/).
  • Back up Signal account state before server migration or rebuild.
  • Keep channels.signal.dmPolicy: "pairing" unless you explicitly want broader DM access.
  • SMS verification is only needed for registration or recovery flows, but losing control of the number/account can complicate re-registration.

Configuration reference (Signal)

Full configuration: Configuration

Provider options:

  • channels.signal.enabled: enable/disable channel startup.
  • channels.signal.account: E.164 for the bot account.
  • channels.signal.accountUuid: optional bot account UUID for native @mention detection and loop protection.
  • channels.signal.transport: account-owned transport. Omit it for managed native defaults.
  • channels.signal.transport.kind: managed-native | external-native | container.
  • channels.signal.transport.url: required for external-native and container; optional for managed-native when its connection endpoint differs from the daemon bind.
  • channels.signal.transport.cliPath: managed-native path to signal-cli.
  • channels.signal.transport.configPath: optional managed-native signal-cli --config directory.
  • channels.signal.transport.httpHost, channels.signal.transport.httpPort: managed-native daemon bind (default 127.0.0.1:8080).
  • channels.signal.transport.startupTimeoutMs: managed-native startup wait in ms (min 1000, cap 120000; default 30000).
  • channels.signal.transport.receiveMode: managed-native on-start | manual.
  • channels.signal.ignoreAttachments: skip inbound attachment downloads for this account.
  • channels.signal.transport.ignoreStories: managed-native story toggle.
  • 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.streaming.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.streaming.block.enabled, channels.signal.streaming.block.coalesce: block-mode streaming controls shared across channels. See Streaming.

Related global options:

  • agents.entries.*.groupChat.mentionPatterns (plain-text fallback; Signal native @mentions are detected from structured metadata when the bot account identity is configured).
  • messages.groupChat.mentionPatterns (global fallback).
  • channels.signal.responsePrefix or an account-level responsePrefix.