Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Add Signal channel integration via signal-cli device-link. Native adapter — no Chat SDK bridge.
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-02 | ✗→✓ | ▲ Improved | 163% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 122% | 0% |
| case-04 | ✗→✓ | ▲ Improved | 68% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 129% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 211% | 0% |
Adds Signal support via a native adapter that speaks JSON-RPC to a signal-cli daemon — no Chat SDK bridge, only Node.js builtins. NanoClaw links to Signal as a secondary device on your existing phone: no new number, no bot API. Your assistant sends and receives as the number on the phone that scans the link.
NanoClaw talks to Signal through signal-cli, which has no bot API of its own. Install it if it isn't on PATH yet — Homebrew on macOS, the native release binary on Linux (neither needs Java). If it's already installed this is a no-op:
nc:run effect:externalcommand -v signal-cli >/dev/null 2>&1 || bash setup/install-signal-cli.sh
Fetch the channels branch and copy the Signal adapter and its registration test into src/channels/ (overwrite — the branch is canonical):
nc:copy from-branch:channelssrc/channels/signal.ts src/channels/signal-registration.test.ts
Append the self-registration import to the channel barrel (skipped if the line is already present). This one line is the skill's only reach-in into core:
nc:append to:src/channels/index.tsimport './signal.js';
The device-link step renders the linking URL as a terminal QR via qrcode. Pinned to exact versions — the supply-chain policy rejects ranges and latest:
nc:depqrcode@1.5.4 @types/qrcode@1.5.6
The adapter itself consumes only Node.js builtins, so there is no adapter package to install — qrcode is purely for rendering the link during setup.
Build first: it guards the adapter's typed core-API consumption. Then run the one integration test.
nc:run effect:buildpnpm run build
nc:run effect:testpnpm exec vitest run src/channels/signal-registration.test.ts
signal-registration.test.ts imports the real channel barrel and asserts the registry contains signal. It goes red if the import './signal.js'; line is deleted or drifts, or if the barrel fails to evaluate — so the channel genuinely would not register. The adapter has no npm dependency to guard; its typed core-API consumption is covered by the build. End-to-end delivery against a real Signal account is verified manually once the service runs.
This is the whole credential step. signal-cli opens a device-link handshake, prints a sgnl://linkdevice… URL, and renders it as a scannable QR. You scan it once from the phone that already runs Signal; that phone's number becomes the account NanoClaw sends and receives as — no number is registered.
The device-link runs signal-cli, so it must be reachable first — on PATH, or at $SIGNAL_CLI_PATH. If step 1's install didn't land, the link step has nothing to drive; confirm it's present before linking (re-run step 1 if this fails):
nc:run effect:checkcommand -v signal-cli >/dev/null 2>&1 || [ -x "$SIGNAL_CLI_PATH" ]
Tell the user:
nc:operatorLink NanoClaw to your Signal account: 1. On the phone that runs Signal, open Signal → Settings → Linked Devices → Link New Device. 2. Scan the QR code shown below — or open the `sgnl://linkdevice…` link printed under it on that phone. 3. Wait for confirmation. The linking URL expires after ~3 minutes; re-run this step for a fresh one.
Run the device-link. It blocks until you scan, then reports the linked phone number back as the account — that number is both your owner handle and the conversation address the wiring step needs:
nc:run effect:step capture:platform_id=ACCOUNT,owner_handle=ACCOUNTpnpm exec tsx setup/index.ts --step signal-auth
owner_handle and platform_id both come back as the bare phone number (e.g. +15551234567). Your assistant reaches you through Signal's Note to Self, so the owner conversation is addressed by your own number — not a per-contact UUID.
Store the linked number so the adapter binds the right account on start, then sync it into the container env:
nc:env-setSIGNAL_ACCOUNT={{platform_id}}
Restart the service so it loads the Signal adapter and binds the account you just linked, and wait for its CLI socket before wiring:
nc:run effect:restartbash setup/lib/restart.sh
After the service starts, send any message to the Signal number from your personal Signal app. The router auto-creates a messaging_groups row. Then:
bashpnpm exec tsx scripts/q.ts data/v2.db \ "SELECT id, platform_id FROM messaging_groups WHERE channel_type='signal' ORDER BY created_at DESC LIMIT 5"
Pass the id to /init-first-agent or /manage-channels to wire it to an agent group.
Add the Signal number to a group from your phone, send any message, then wire the resulting row the same way. Each group gets its own session with the default shared mode (one session per agent + messaging group). Create the wiring with ncl — the host service must be running (ncl connects to it over a Unix socket):
bash# Engage mode/pattern default to the Signal adapter's declared channel defaults ncl wirings create --messaging-group-id mg-GROUPID --agent-group-id ag-AGENTID
New Signal users (including the owner's Signal identity) are silently dropped with not_member until granted access. After the user's first message appears in messaging_groups (host service running):
bashncl users create --id "signal:UUID" --kind signal --display-name "<name>" ncl roles grant --user "signal:UUID" --role owner ncl members add --user "signal:UUID" --group ag-AGENTID
Find the UUID from messaging_groups.platform_id or the users table.
If you're in the middle of /setup, return to the setup flow now. Otherwise wire this channel with /init-first-agent (or /manage-channels).
signal+<number> (e.g. +15551234567) — your own messages route back as inbound with isFromMe, addressed by your number.signal:{UUID} — the sender's Signal ACI, not their phone number.signal:{base64GroupId} — base64-encoded GroupV2 ID.messaging_groups.shared session mode already gives each messaging group its own session).**bold**, *italic* / _italic_, code , code fence , ~~strike~~, ||spoiler|| (converted to Signal's offset-based text styles).replyTo* fields populated from Signal quotes.isFromMe: true.[Voice Message] placeholder. Run /add-voice-transcription for local transcription.Not supported yet: outbound file attachments (logged and dropped), edit/delete messages, reactions.
The device-link above joins Signal as a secondary device on an existing number. If you'd rather give the assistant its own number, register a dedicated SIM or VoIP number that NanoClaw owns entirely. This path takes a captcha, an SMS (or voice) verification, and an optional profile name.
> VoIP numbers: Signal requires SMS verification before voice. Some VoIP providers are blocked even for voice calls. If registration fails with an auth error, try a different provider or a physical SIM.
Step 1: Solve the CAPTCHA
Signal requires a CAPTCHA on first registration:
https://signalcaptchas.org/registration/generate.html in a browsersignalcaptcha:// — the token is everything after that prefixStep 2: Request SMS verification
bashsignal-cli -a +1YOURNUMBER register --captcha "PASTE_TOKEN_HERE"
Step 3: Voice call fallback (if your number can't receive SMS)
Wait ~60 seconds after the SMS request, then:
bashsignal-cli -a +1YOURNUMBER register --voice --captcha "SAME_TOKEN"
Signal calls your number and reads a 6-digit code. The same captcha token is reusable — no need to solve a new one.
> You must request SMS first. Requesting voice immediately fails with Invalid verification method: Before requesting voice verification…
Step 4: Verify
bashsignal-cli -a +1YOURNUMBER verify CODE
No output = success.
Step 5: Set profile name (optional)
> ⚠ Stop NanoClaw before running signal-cli commands — the daemon holds an exclusive lock on its data directory while running.
Run from your NanoClaw project root:
bashsource setup/lib/install-slug.sh # macOS launchctl unload ~/Library/LaunchAgents/$(launchd_label).plist signal-cli -a +1YOURNUMBER updateProfile --name "YourBotName" # optionally: --avatar /path/to/avatar.jpg launchctl load ~/Library/LaunchAgents/$(launchd_label).plist # Linux systemctl --user stop $(systemd_unit) signal-cli -a +1YOURNUMBER updateProfile --name "YourBotName" systemctl --user start $(systemd_unit)
Once registered, set SIGNAL_ACCOUNT to this number (as under Persist the account above) and restart the service.
These .env keys tune how NanoClaw talks to the signal-cli daemon. All are optional — the defaults work for the device-link flow above.
bash# TCP daemon host and port (default: 127.0.0.1:7583) SIGNAL_TCP_HOST=127.0.0.1 SIGNAL_TCP_PORT=7583 # Path to the signal-cli binary (default: resolved on PATH) SIGNAL_CLI_PATH=/usr/local/bin/signal-cli # Whether NanoClaw manages the daemon lifecycle (default: true). # Set to false if you run signal-cli daemon externally. SIGNAL_MANAGE_DAEMON=true # signal-cli data directory (default: ~/.local/share/signal-cli) SIGNAL_DATA_DIR=~/.local/share/signal-cli
Security note: keep the TCP host on 127.0.0.1. The daemon has no auth — binding it to a public interface would expose your full Signal account to the network.
bashgrep "Signal" logs/nanoclaw.log | tail
If you see Signal daemon failed to start. Is signal-cli installed and your account linked?:
signal-cli is on PATH (or set SIGNAL_CLI_PATH)signal-cli -a +YOURNUMBER listIdentities should succeed without promptingIf you see Signal daemon not reachable at 127.0.0.1:7583 and SIGNAL_MANAGE_DAEMON=false, start the daemon yourself: signal-cli -a +YOURNUMBER daemon --tcp 127.0.0.1:7583.
grep "Signal channel connected" logs/nanoclaw.log | tail -1pnpm exec tsx scripts/q.ts data/v2.db "SELECT mg.platform_id, mg.name FROM messaging_groups mg JOIN messaging_group_agents mga ON mg.id = mga.messaging_group_id WHERE mg.channel_type='signal'"launchctl print gui/$(id -u)/"$(. setup/lib/install-slug.sh && launchd_label)" (macOS) / systemctl --user status "$(. setup/lib/install-slug.sh && systemd_unit)" (Linux)logs/nanoclaw.error.log shows No adapter for channel type channelType="signal" despite the adapter starting, two NanoClaw processes are racing. See the /debug skill section "No adapter for channel type / Messages silently lost" for the full fix.Signal responses show platformMsgId=undefined in the main log. This means the delivery poll ran but found no adapter — likely a duplicate service instance issue (see above). Affected messages cannot be retried; the user must resend.
If you see Signal channel lost TCP connection to signal-cli daemon in the logs, the daemon dropped the connection. Restart the service to re-establish.
not_memberThe Signal user hasn't been granted membership. New Signal senders — including the owner's Signal identity — are gated until granted access. /init-first-agent grants the owner automatically; for other users, grant access as shown under Grant user access in the Wiring section (or via /manage-channels) after their first message appears in messaging_groups. This affects every new Signal user, since their Signal identity is a separate user record from their identity on other channels even if it's the same person.
Signal requires a captcha for new registrations. Go to https://signalcaptchas.org/registration/generate.html, solve it, right-click "Open Signal", copy the link, extract the token after signalcaptcha://.
Invalid verification method: Before requesting voice verification…You must request SMS first, wait ~60 seconds, then request voice. Both steps can use the same captcha token.
signal-cli holds an exclusive lock on its data directory while the daemon is running. Stop NanoClaw before running any signal-cli commands directly, then restart afterward.
Modern Signal groups use GroupV2. The adapter must extract the group ID from envelope?.dataMessage?.groupV2?.id — not groupInfo?.groupId, which is GroupV1/legacy. If group messages are routing as DMs, check src/channels/signal.ts and confirm the groupId extraction falls through to groupV2.id.
The sgnl://linkdevice… URL (and the Path A registration captcha) expire after a few minutes. Re-run the device-link step to get a fresh QR.
Other measured skills in the registry, with their headline benchmark lift.