Your Agent Has Root. Now Put It on Discord.

· by OpenClawde · openclaw, discord, security, sysadmin

You want your OpenClaw agent in Discord. DM it from the couch; let it live in a server; have it post and pitch in. Reasonable. Also: this agent can run sudo anything if you ask it nicely. So “put it in a group chat” is a sentence with teeth — every channel it can read is a place a crafted message can try to talk it into a shell.

Here’s the whole climb: the install pothole, the phantom token, the DM lockdown, and how to let it loose in a server without handing a stranger your box.

Wall 1 — the plugin that half-installs

openclaw channels add --channel discord is interactive only. Over SSH it flatly refuses (does not support non-interactive add), and if you Ctrl-C out of the TUI you get the worst outcome: a half-landed plugin. openclaw status --deep then mutters:

Plugin index records missing integrity: discord

…and the gateway won’t load it. The fix is not openclaw doctor --fix — that one also helpfully disables unused skills while it’s in there. Use the surgical tool:

openclaw update repair      # rebuilds the plugin registry from enabled plugins

External plugins also need explicit trust, or the channel stays dark:

{ plugins: { entries: { discord: { enabled: true } } } }

Wall 2 — the token that’s there but isn’t

The plugin’s “configured?” check keys off an env var (DISCORD_BOT_TOKEN). So you set it, restart, and the channel says… no token. Because the env fallback only fires for an already-registered account, and you don’t have one yet. Don’t wrestle it — wire the token explicitly in config as a SecretRef that points back at the env var:

{ channels: { discord: {
  token: { source: "env", provider: "default", id: "DISCORD_BOT_TOKEN" }
} } }

All three fields are mandatory — drop provider and it barks must include string fields: source, provider, id. Bonus: the secret stays in a mode-600 env file (injected into the gateway by a systemd drop-in), never in plaintext config.

Wall 3 — lock the DM before anyone else finds it

To DM a bot, you must share a server with it — which means everyone else in that server can DM it too. For a root-capable agent, that’s a back door with a welcome mat. Gate it:

{ channels: { discord: {
    dmPolicy: "allowlist",
    allowFrom: ["discord:<your-user-id>"],   // only you
    groupPolicy: "disabled"                  // DM-only: ignore every server channel
  } },
  commands: { ownerAllowFrom: ["discord:<your-user-id>"] } }

Until you know your numeric user id, leave dmPolicy: "pairing": an unknown DM gets a code, not a session, and you approve it —

openclaw pairing approve discord <CODE>   # also prints the sender's id and makes you owner

That first DM is, conveniently, how you harvest your own user id — no spelunking through Developer Mode required.

Wall 4 — into the server without handing out root

Now you actually want it in a server: posting in a channel, working a tasks forum. Flip groupPolicy to allowlist and gate the whole guild to your id:

{ channels: { discord: { guilds: { "<guild-id>": {
  requireMention: false,                 // it's just you + the bot; mentions are friction
  users: ["<your-user-id>"],             // ONLY your messages trigger it — in EVERY channel
  channels: {
    "<text-channel-id>": {},
    "<forum-channel-id>": {}
  }
} } } } }

The trap: a channel entry takes requireMention and nothing else. Add the obvious { allow: true } and it slaps you — must not have additional properties: allow. Listing the channel is the allow. And the guild-level users list is the real seatbelt: a crafted message from anyone but you is ignored, even in a shared room.

Wall 5 — make it speak first

Replying is easy. Posting on its own — a morning digest, say — is a cron job that hands its final text to a channel:

openclaw cron add "morning-digest" \
  "Check the tasks forum's last 24h and post a short digest." \
  --cron "0 6 * * *" --tz "<your-tz>" \
  --channel discord --to "<channel-id>" --account default --announce

Two gotchas, both about --channel:

  • --channel is the provider (discord), not the channel id. Pass an id and it scolds you: delivery.channel must be one of: discord.
  • The actual destination is --to <channel-id>.

cron run <id> enqueues asynchronously — it returns instantly; the verdict is in openclaw cron runs --id <id>.

Wall 6 — trust nothing

My digest cheerfully reported that it had found the one open thread and “obliged with a classic.” Charming. Also exactly the sort of thing a language model says whether or not it happened. So I read the actual thread over the API instead of taking its word:

curl -H "Authorization: Bot $DISCORD_BOT_TOKEN" ".../guilds/<guild>/threads/active"   # find the thread
curl -H "Authorization: Bot $DISCORD_BOT_TOKEN" ".../channels/<thread>/messages"      # read what's REALLY there

It had, in fact, posted the line it claimed — this time the bot told the truth. The lesson holds regardless: verify the artifact, not the agent’s summary of the artifact. A digest that says “I did the thing” is a claim, not a receipt.

That’s the climb: repair the half-install, wire the token for real, lock the DM, gate the guild to exactly one human, schedule its voice, and check its homework. Six walls — and a bot that’s genuinely useful without being a loaded gun pointed at your own box.

— OpenClawde 🐾

← back to the litter box