--- summary: "Signal support via signal-cli (native daemon or bbernhard container), setup paths, and number model" read_when: - Setting up Signal support - Debugging Signal send/receive title: "Signal" --- 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](https://github.com/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** makes it ignore your own messages (loop protection). - For "I text the bot and it replies," use a **separate bot number**. ## Install ```bash 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](/tools/plugin) for general install rules. ## Quick setup Use a **separate Signal number** for the bot (recommended). ```bash openclaw plugins install @openclaw/signal ``` ```bash 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 ` for the bot phone number, plus `--http-host ` and `--http-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](#setup-path-a-link-existing-signal-account-qr). - **SMS registration:** dedicated number with captcha + SMS verification. See [Path B](#setup-path-b-register-dedicated-bot-number-sms-linux). ```bash openclaw gateway call channels.status --params '{"probe":true}' ``` Send a first DM and approve pairing: `openclaw pairing approve signal `. Minimal config: ```json5 { 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` | Account-owned Signal connection and process mode | | `dmPolicy` | DM access policy (`pairing` recommended) | | `allowFrom` | Phone numbers or `uuid:` 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](/gateway/config-channels#multi-account-all-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::signal:group:`). - By default, Signal may write config updates triggered by `/config set|unset` (requires `commands.config: true`). Disable with `channels.signal.configWrites: false`. ## Setup path A: link existing Signal account (QR) 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: ```bash 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. 3. Register and verify the number: ```bash signal-cli -a + 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: ```bash signal-cli -a + register --captcha '' signal-cli -a + verify ``` 4. Configure OpenClaw, restart the gateway, verify the channel: ```bash # If you run the gateway as a user systemd service: systemctl --user restart openclaw-gateway.service # Then verify: openclaw doctor openclaw channels status --probe ``` 5. Pair your DM sender: - Send any message to the bot number. - Approve on the server: `openclaw pairing approve signal `. - Save the bot number as a contact on your phone to avoid "Unknown contact". 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: ```bash openclaw channels add --channel signal --signal-number +15551234567 \ --http-url http://127.0.0.1:8080 --signal-transport external-native ``` ```json5 { 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](https://github.com/bbernhard/signal-cli-rest-api) Docker container, which wraps `signal-cli` behind a REST + WebSocket interface. ```bash 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: ```yaml 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: ```json5 { channels: { signal: { enabled: true, account: "+15551234567", transport: { kind: "container", url: "http://signal-cli:8080", }, }, }, } ``` `transport.kind` controls which protocol and process lifecycle OpenClaw uses: | Value | Behavior | | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `"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 `. - Pairing is the default token exchange for Signal DMs. Details: [Pairing](/channels/pairing) - UUID-only senders (from `sourceUuid`) are stored as `uuid:` 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:`, or `signal:group:`), sender phone numbers, `uuid:` values, or `*`. - `channels.signal.groups["" | "*"]` can override group behavior with `requireMention`, `tools`, and `toolsBySender`. - Use `channels.signal.accounts..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[""]` 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: ```json5 { channels: { signal: { account: "+15551234567", accountUuid: "bot-signal-uuid", groupPolicy: "allowlist", groupAllowFrom: ["group:"], historyLimit: 8, groups: { "": { 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.` take precedence. ## Media + limits - Outbound text is chunked to `channels.signal.textChunkLimit` (default 4000). - Optional newline chunking: set `channels.signal.streaming.chunkMode="newline"` to split on blank lines (paragraph boundaries) before length chunking. - 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 cap: `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:` 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`. ```text 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: targetAuthor=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..actions.reactions`, `channels.signal.accounts..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 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:` (or bare UUID). - Groups: `signal:group:`. - Usernames: `username:` (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. ```json5 { channels: { signal: { aliases: { me: "+15557654321", jane: "uuid:123e4567-e89b-12d3-a456-426614174000", ops: "group:", }, defaultTo: "signal:me", }, }, } ``` Use aliases anywhere Signal delivery targets are accepted: ```bash 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: ```json5 { channels: { signal: { aliases: { me: "+15557654321", }, accounts: { work: { aliases: { ops: "group:", }, }, }, }, }, } ``` `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: ```bash openclaw status openclaw gateway status openclaw logs --follow openclaw doctor openclaw channels status --probe ``` Then confirm DM pairing state if needed: ```bash 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: ```bash openclaw pairing list signal pgrep -af signal-cli openclaw logs --plain --limit 500 | grep -i "signal" | tail -20 ``` For triage flow: [Channels Troubleshooting](/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](/gateway/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:`). `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:`, or `signal:group:`), sender E.164 numbers, or `uuid:` values. - `channels.signal.groups`: per-group overrides keyed by Signal group ID (or `"*"`). Supported fields: `requireMention`, `tools`, `toolsBySender`. - `channels.signal.accounts..groups`: per-account version of `channels.signal.groups` for multi-account setups. - `channels.signal.accounts..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..replyToMode`, `channels.signal.accounts..replyToModeByChatType.direct`, `channels.signal.accounts..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[""].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](#reactions-message-tool). - `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](/concepts/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`. ## Related - [Channels Overview](/channels) - all supported channels - [Pairing](/channels/pairing) - DM authentication and pairing flow - [Groups](/channels/groups) - group chat behavior and mention gating - [Channel Routing](/channels/channel-routing) - session routing for messages - [Security](/gateway/security) - access model and hardening