{"id":"add-signal","name":"add-signal","summary":"signal-cli device-linkを通じてSignalチャネル統合を追加します。ネイティブアダプター — Chat SDKブリッジなし。","body":"# Add Signal Channel\n\nAdds Signal support via a native adapter that speaks JSON-RPC to a\n[signal-cli](https://github.com/AsamK/signal-cli) daemon — no Chat SDK bridge,\nonly Node.js builtins. NanoClaw links to Signal as a *secondary device* on your\nexisting phone: no new number, no bot API. Your assistant sends and receives as\nthe number on the phone that scans the link.\n\n## Apply\n\n### 1. Install signal-cli\n\nNanoClaw talks to Signal through signal-cli, which has no bot API of its own.\nInstall it if it isn't on PATH yet — Homebrew on macOS, the native release binary\non Linux (neither needs Java). If it's already installed this is a no-op:\n\n```nc:run effect:external\ncommand -v signal-cli >/dev/null 2>&1 || bash setup/install-signal-cli.sh\n```\n\n### 2. Copy the adapter and its registration test\n\nFetch the `channels` branch and copy the Signal adapter and its registration test\ninto `src/channels/` (overwrite — the branch is canonical):\n\n```nc:copy from-branch:channels\nsrc/channels/signal.ts\nsrc/channels/signal-registration.test.ts\n```\n\n### 3. Register the adapter\n\nAppend the self-registration import to the channel barrel (skipped if the line is\nalready present). This one line is the skill's only reach-in into core:\n\n```nc:append to:src/channels/index.ts\nimport './signal.js';\n```\n\n### 4. Install the QR-rendering dependency\n\nThe device-link step renders the linking URL as a terminal QR via `qrcode`.\nPinned to exact versions — the supply-chain policy rejects ranges and `latest`:\n\n```nc:dep\nqrcode@1.5.4\n@types/qrcode@1.5.6\n```\n\nThe adapter itself consumes only Node.js builtins, so there is no adapter package\nto install — `qrcode` is purely for rendering the link during setup.\n\n### 5. Build and validate\n\nBuild first: it guards the adapter's typed core-API consumption. Then run the one\nintegration test.\n\n```nc:run effect:build\npnpm run build\n```\n```nc:run effect:test\npnpm exec vitest run src/channels/signal-registration.test.ts\n```\n\n`signal-registration.test.ts` imports the real channel barrel and asserts the\nregistry contains `signal`. It goes red if the `import './signal.js';` line is\ndeleted or drifts, or if the barrel fails to evaluate — so the channel genuinely\nwould not register. The adapter has no npm dependency to guard; its typed\ncore-API consumption is covered by the build. End-to-end delivery against a real\nSignal account is verified manually once the service runs.\n\n## Link your Signal account\n\nThis is the whole credential step. signal-cli opens a device-link handshake,\nprints a `sgnl://linkdevice…` URL, and renders it as a scannable QR. You scan it\nonce from the phone that already runs Signal; that phone's number becomes the\naccount NanoClaw sends and receives as — no number is registered.\n\nThe device-link runs signal-cli, so it must be reachable first — on `PATH`, or at\n`$SIGNAL_CLI_PATH`. If step 1's install didn't land, the link step has nothing to\ndrive; confirm it's present before linking (re-run step 1 if this fails):\n\n```nc:run effect:check\ncommand -v signal-cli >/dev/null 2>&1 || [ -x \"$SIGNAL_CLI_PATH\" ]\n```\n\nTell the user:\n\n```nc:operator\nLink NanoClaw to your Signal account:\n1. On the phone that runs Signal, open Signal → Settings → Linked Devices → Link New Device.\n2. Scan the QR code shown below — or open the `sgnl://linkdevice…` link printed under it on that phone.\n3. Wait for confirmation. The linking URL expires after ~3 minutes; re-run this step for a fresh one.\n```\n\nRun the device-link. It blocks until you scan, then reports the linked phone\nnumber back as the account — that number is both your owner handle and the\nconversation address the wiring step needs:\n\n```nc:run effect:step capture:platform_id=ACCOUNT,owner_handle=ACCOUNT\npnpm exec tsx setup/index.ts --step signal-auth\n```\n\n`owner_handle` and `platform_id` both come back as the bare phone number (e.g.\n`+15551234567`). Your assistant reaches you through Signal's Note to Self, so the\nowner conversation is addressed by your own number — not a per-contact UUID.\n\n## Persist the account\n\nStore the linked number so the adapter binds the right account on start, then sync\nit into the container env:\n\n```nc:env-set\nSIGNAL_ACCOUNT={{platform_id}}\n```\n## Restart\n\nRestart the service so it loads the Signal adapter and binds the account you just\nlinked, and wait for its CLI socket before wiring:\n\n```nc:run effect:restart\nbash setup/lib/restart.sh\n```\n\n## Wiring\n\n### DMs\n\nAfter the service starts, send any message to the Signal number from your\npersonal Signal app. The router auto-creates a `messaging_groups` row. Then:\n\n```bash\npnpm exec tsx scripts/q.ts data/v2.db \\\n  \"SELECT id, platform_id FROM messaging_groups WHERE channel_type='signal' ORDER BY created_at DESC LIMIT 5\"\n```\n\nPass the `id` to `/init-first-agent` or `/manage-channels` to wire it to an agent group.\n\n### Groups\n\nAdd 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):\n\n```bash\n# Engage mode/pattern default to the Signal adapter's declared channel defaults\nncl wirings create --messaging-group-id mg-GROUPID --agent-group-id ag-AGENTID\n```\n\n### Grant user access\n\nNew 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):\n\n```bash\nncl users create --id \"signal:UUID\" --kind signal --display-name \"<name>\"\nncl roles grant --user \"signal:UUID\" --role owner\nncl members add --user \"signal:UUID\" --group ag-AGENTID\n```\n\nFind the UUID from `messaging_groups.platform_id` or the `users` table.\n\n## Next Steps\n\nIf you're in the middle of `/setup`, return to the setup flow now. Otherwise wire\nthis channel with `/init-first-agent` (or `/manage-channels`).\n\n## Channel Info\n\n- **type**: `signal`\n- **terminology**: Signal has \"chats\" (1:1 DMs) and \"groups.\" The owner reaches their own assistant through Note to Self.\n- **platform-id-format**:\n  - Owner DM (Note to Self): the bare phone number `+<number>` (e.g. `+15551234567`) — your own messages route back as inbound with `isFromMe`, addressed by your number.\n  - Third-party DM: `signal:{UUID}` — the sender's Signal ACI, **not** their phone number.\n  - Group: `signal:{base64GroupId}` — base64-encoded GroupV2 ID.\n- **how-to-find-id**: The owner number comes back from the device-link step above. For third parties or groups, send a message to the bot, then query `messaging_groups`.\n- **supports-threads**: no\n- **typical-use**: Personal assistant via Signal DMs or small group chats\n- **default-isolation**: One agent per Signal account. Multiple chats with the same operator can share an agent group; groups with other people should typically get their own agent group (the default `shared` session mode already gives each messaging group its own session).\n\n### Features\n\n- Markdown formatting — `**bold**`, `*italic*` / `_italic_`, `` `code` ``, ` ```code fence``` `, `~~strike~~`, `||spoiler||` (converted to Signal's offset-based text styles).\n- Quoted replies — `replyTo*` fields populated from Signal quotes.\n- Typing indicators — DMs only (Signal doesn't support group typing).\n- Note to Self — messages you send to your own account from another device route to the agent as inbound with `isFromMe: true`.\n- Voice attachments — detected but not transcribed by default; the agent receives a `[Voice Message]` placeholder. Run `/add-voice-transcription` for local transcription.\n\nNot supported yet: outbound file attachments (logged and dropped), edit/delete messages, reactions.\n\n## Alternatives\n\n### Register a dedicated number instead of linking\n\nThe device-link above joins Signal as a *secondary device* on an existing number.\nIf you'd rather give the assistant its own number, register a dedicated SIM or\nVoIP number that NanoClaw owns entirely. This path takes a captcha, an SMS (or\nvoice) verification, and an optional profile name.\n\n> **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.\n\n**Step 1: Solve the CAPTCHA**\n\nSignal requires a CAPTCHA on first registration:\n\n1. Open `https://signalcaptchas.org/registration/generate.html` in a browser\n2. Solve the captcha\n3. Right-click the **\"Open Signal\"** button → **Copy Link**\n4. The link starts with `signalcaptcha://` — the token is everything after that prefix\n\n**Step 2: Request SMS verification**\n\n```bash\nsignal-cli -a +1YOURNUMBER register --captcha \"PASTE_TOKEN_HERE\"\n```\n\n**Step 3: Voice call fallback (if your number can't receive SMS)**\n\nWait ~60 seconds after the SMS request, then:\n\n```bash\nsignal-cli -a +1YOURNUMBER register --voice --captcha \"SAME_TOKEN\"\n```\n\nSignal calls your number and reads a 6-digit code. The same captcha token is reusable — no need to solve a new one.\n\n> You must request SMS first. Requesting voice immediately fails with `Invalid verification method: Before requesting voice verification…`\n\n**Step 4: Verify**\n\n```bash\nsignal-cli -a +1YOURNUMBER verify CODE\n```\n\nNo output = success.\n\n**Step 5: Set profile name (optional)**\n\n> ⚠ Stop NanoClaw before running signal-cli commands — the daemon holds an exclusive lock on its data directory while running.\n\nRun from your NanoClaw project root:\n\n```bash\nsource setup/lib/install-slug.sh\n\n# macOS\nlaunchctl unload ~/Library/LaunchAgents/$(launchd_label).plist\nsignal-cli -a +1YOURNUMBER updateProfile --name \"YourBotName\"\n# optionally: --avatar /path/to/avatar.jpg\nlaunchctl load ~/Library/LaunchAgents/$(launchd_label).plist\n\n# Linux\nsystemctl --user stop $(systemd_unit)\nsignal-cli -a +1YOURNUMBER updateProfile --name \"YourBotName\"\nsystemctl --user start $(systemd_unit)\n```\n\nOnce registered, set `SIGNAL_ACCOUNT` to this number (as under **Persist the account** above) and restart the service.\n\n## Optional configuration\n\nThese `.env` keys tune how NanoClaw talks to the signal-cli daemon. All are\noptional — the defaults work for the device-link flow above.\n\n```bash\n# TCP daemon host and port (default: 127.0.0.1:7583)\nSIGNAL_TCP_HOST=127.0.0.1\nSIGNAL_TCP_PORT=7583\n\n# Path to the signal-cli binary (default: resolved on PATH)\nSIGNAL_CLI_PATH=/usr/local/bin/signal-cli\n\n# Whether NanoClaw manages the daemon lifecycle (default: true).\n# Set to false if you run signal-cli daemon externally.\nSIGNAL_MANAGE_DAEMON=true\n\n# signal-cli data directory (default: ~/.local/share/signal-cli)\nSIGNAL_DATA_DIR=~/.local/share/signal-cli\n```\n\n**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.\n\n## Troubleshooting\n\n### Daemon not reachable\n\n```bash\ngrep \"Signal\" logs/nanoclaw.log | tail\n```\n\nIf you see `Signal daemon failed to start. Is signal-cli installed and your account linked?`:\n- Confirm `signal-cli` is on PATH (or set `SIGNAL_CLI_PATH`)\n- Confirm the account is linked: `signal-cli -a +YOURNUMBER listIdentities` should succeed without prompting\n\nIf 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`.\n\n### Bot not responding\n\n1. Channel initialized: `grep \"Signal channel connected\" logs/nanoclaw.log | tail -1`\n2. Channel wired: `pnpm 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'\"`\n3. Service running: `launchctl print gui/$(id -u)/\"$(. setup/lib/install-slug.sh && launchd_label)\"` (macOS) / `systemctl --user status \"$(. setup/lib/install-slug.sh && systemd_unit)\"` (Linux)\n4. **Check for duplicate service instances** — if `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.\n\n### Messages delivered but never arrive (null platformMsgId)\n\nSignal 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.\n\n### Lost connection mid-session\n\nIf 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.\n\n### Messages dropped with `not_member`\n\nThe 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.\n\n### Captcha required\n\nSignal 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://`.\n\n### `Invalid verification method: Before requesting voice verification…`\n\nYou must request SMS first, wait ~60 seconds, then request voice. Both steps can use the same captcha token.\n\n### Config file in use / daemon lock\n\nsignal-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.\n\n### Group replies going to DM instead of group\n\nModern 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`.\n\n### QR / linking URL expired\n\nThe `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.","author":"@nanocoai","ownerProfile":null,"authorContacts":null,"sourceUrl":"https://github.com/nanocoai/nanoclaw/tree/main/.claude/skills/add-signal","license":"MIT","category":"writing","lang":"en","tokens":3515,"stars":0,"calls30d":1,"claimed":false,"visibility":"public","origin":"crawler","version":"0.1.0","createdAt":"2026-08-22","updatedAt":"2026-08-22","files":[{"path":"apply-fixtures.json","size":370,"sha256":"f86ef3a132f085e9b6572183097ccf59403637f6cacccc232d75a705c87281cb"},{"path":"REMOVE.md","size":1030,"sha256":"2d37ba8ead06f90c71678862c057bab1b5cc52d968e8beac60267b7a842477a3"}],"requires":{"mcp":[],"tools":[]},"safety":{"flags":[],"scannedAt":"2026-08-22","hasScripts":false,"networkEndpoints":["signalcaptchas.org"]}}