From 50a5b4ddcc5728bb83b482ca1d9a0bda08d09ecb Mon Sep 17 00:00:00 2001
From: Magi Metal <36491250+magimetal@users.noreply.github.com>
Date: Fri, 9 Jan 2026 13:58:25 -0500
Subject: [PATCH] Discord: fix DM recipient parsing for bare numeric user IDs
(#596)
Co-authored-by: Shadow
---
CHANGELOG.md | 1 +
README.md | 2 +-
docs/cli/message.md | 2 +-
docs/gateway/configuration.md | 2 +-
docs/providers/discord.md | 2 +-
docs/reference/AGENTS.default.md | 2 +-
src/discord/send.test.ts | 13 +++++++++++++
src/discord/send.ts | 5 +++++
8 files changed, 24 insertions(+), 5 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7376b5c9b..3258a4734 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -130,6 +130,7 @@
- Dev templates: ship C-3PO dev workspace defaults as docs templates and use them for dev bootstrap. — thanks @steipete
- Config: fix Minimax hosted onboarding to write `agents.defaults` and allow `msteams` as a heartbeat target. — thanks @steipete
- Discord: add channel/category management actions (create/edit/move/delete + category removal). (#487) - thanks @NicholasSpisak
+- Discord: reject ambiguous bare numeric IDs in DM sends with clear guidance on `user:` vs `channel:` prefixes (fixes "Unknown Channel" error when DMing by user ID). (#596) — thanks @magimetal
## 2026.1.8
diff --git a/README.md b/README.md
index fb56738af..eeb623f88 100644
--- a/README.md
+++ b/README.md
@@ -468,5 +468,5 @@ Thanks to all clawtributors:
-
+
diff --git a/docs/cli/message.md b/docs/cli/message.md
index 1e6b8b2e4..bff1aa94e 100644
--- a/docs/cli/message.md
+++ b/docs/cli/message.md
@@ -24,7 +24,7 @@ Provider selection:
Target formats (`--to`):
- WhatsApp: E.164 or group JID
- Telegram: chat id or `@username`
-- Discord/Slack: `channel:` or `user:` (raw id ok)
+- Discord/Slack: `channel:` or `user:` (raw id is ambiguous for Discord)
- Signal: E.164, `group:`, or `signal:+E.164`
- iMessage: handle or `chat_id:`
- MS Teams: conversation id (`19:...@thread.tacv2`) or `conversation:` or `user:`
diff --git a/docs/gateway/configuration.md b/docs/gateway/configuration.md
index da70fd387..0bbb8bb72 100644
--- a/docs/gateway/configuration.md
+++ b/docs/gateway/configuration.md
@@ -765,7 +765,7 @@ Multi-account support lives under `discord.accounts` (see the multi-account sect
}
```
-Clawdbot starts Discord only when a `discord` config section exists. The token is resolved from `DISCORD_BOT_TOKEN` or `discord.token` (unless `discord.enabled` is `false`). Use `user:` (DM) or `channel:` (guild channel) when specifying delivery targets for cron/CLI commands.
+Clawdbot starts Discord only when a `discord` config section exists. The token is resolved from `DISCORD_BOT_TOKEN` or `discord.token` (unless `discord.enabled` is `false`). Use `user:` (DM) or `channel:` (guild channel) when specifying delivery targets for cron/CLI commands; bare numeric IDs are ambiguous and rejected.
Guild slugs are lowercase with spaces replaced by `-`; channel keys use the slugged channel name (no leading `#`). Prefer guild ids as keys to avoid rename ambiguity.
Reaction notification modes:
- `off`: no reaction events.
diff --git a/docs/providers/discord.md b/docs/providers/discord.md
index 3f844fa7e..a70876cbf 100644
--- a/docs/providers/discord.md
+++ b/docs/providers/discord.md
@@ -20,7 +20,7 @@ Status: ready for DM and guild text channels via the official Discord bot gatewa
3. Configure Clawdbot with `DISCORD_BOT_TOKEN` (or `discord.token` in `~/.clawdbot/clawdbot.json`).
4. Run the gateway; it auto-starts the Discord provider only when a `discord` config section exists **and** the token is set (unless `discord.enabled = false`).
- If you prefer env vars, still add `discord: { enabled: true }` to `~/.clawdbot/clawdbot.json` and set `DISCORD_BOT_TOKEN`.
-5. Direct chats: use `user:` (or a `<@id>` mention) when delivering; all turns land in the shared `main` session.
+5. Direct chats: use `user:` (or a `<@id>` mention) when delivering; all turns land in the shared `main` session. Bare numeric IDs are ambiguous and rejected.
6. Guild channels: use `channel:` for delivery. Mentions are required by default and can be set per guild or per channel.
7. Direct chats: secure by default via `discord.dm.policy` (default: `"pairing"`). Unknown senders get a pairing code (expires after 1 hour); approve via `clawdbot pairing approve --provider discord `.
- To keep old “open to anyone” behavior: set `discord.dm.policy="open"` and `discord.dm.allowFrom=["*"]`.
diff --git a/docs/reference/AGENTS.default.md b/docs/reference/AGENTS.default.md
index cce2c4cda..957b516ab 100644
--- a/docs/reference/AGENTS.default.md
+++ b/docs/reference/AGENTS.default.md
@@ -92,7 +92,7 @@ git commit -m "Add Clawd workspace"
- **eightctl** — Control your sleep, from the terminal.
- **imsg** — Send, read, stream iMessage & SMS.
- **wacli** — WhatsApp CLI: sync, search, send.
-- **discord** — Discord actions: react, stickers, polls.
+- **discord** — Discord actions: react, stickers, polls. Use `user:` or `channel:` targets (bare numeric ids are ambiguous).
- **gog** — Google Suite CLI: Gmail, Calendar, Drive, Contacts.
- **spotify-player** — Terminal Spotify client to search/queue/control playback.
- **sag** — ElevenLabs speech with mac-style say UX; streams to speakers by default.
diff --git a/src/discord/send.test.ts b/src/discord/send.test.ts
index 6f714ca2b..9468d6d5d 100644
--- a/src/discord/send.test.ts
+++ b/src/discord/send.test.ts
@@ -109,6 +109,19 @@ describe("sendMessageDiscord", () => {
expect(res.channelId).toBe("chan1");
});
+ it("rejects bare numeric IDs as ambiguous", async () => {
+ const { rest } = makeRest();
+ await expect(
+ sendMessageDiscord("273512430271856640", "hello", { rest, token: "t" }),
+ ).rejects.toThrow(/Ambiguous Discord recipient/);
+ await expect(
+ sendMessageDiscord("273512430271856640", "hello", { rest, token: "t" }),
+ ).rejects.toThrow(/user:273512430271856640/);
+ await expect(
+ sendMessageDiscord("273512430271856640", "hello", { rest, token: "t" }),
+ ).rejects.toThrow(/channel:273512430271856640/);
+ });
+
it("adds missing permission hints on 50013", async () => {
const { rest, postMock, getMock } = makeRest();
const perms = PermissionFlagsBits.ViewChannel;
diff --git a/src/discord/send.ts b/src/discord/send.ts
index 8a4ff1395..d54811af7 100644
--- a/src/discord/send.ts
+++ b/src/discord/send.ts
@@ -305,6 +305,11 @@ function parseRecipient(raw: string): DiscordRecipient {
}
return { kind: "user", id: candidate };
}
+ if (/^\d+$/.test(trimmed)) {
+ throw new Error(
+ `Ambiguous Discord recipient "${trimmed}". Use "user:${trimmed}" for DMs or "channel:${trimmed}" for channel messages.`,
+ );
+ }
return { kind: "channel", id: trimmed };
}