openclaw - 💡(How to fix) Fix Discord: native slash commands not deployed for secondary channel accounts even with commands.native: true [1 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#78406Fetched 2026-05-07 03:37:16
View on GitHub
Comments
1
Participants
2
Timeline
1
Reactions
2
Timeline (top)
commented ×1

In a multi-account Discord setup (multiple bots, each bound to a different agent via bindings), runDiscordCommandDeployInBackground only effectively deploys slash commands for the default account. Secondary accounts (with their own tokens under channels.discord.accounts.<id>) end up with zero commands registered with Discord, even though they probe-resolve and handle messages correctly.

Root Cause

Multi-account Discord setups are explicitly supported by openclaw via channels.discord.accounts + bindings (per-persona bots, e.g. one for work, one for personal). Without slash command auto-deploy for secondary accounts, every persona except the default loses the entire / command surface, which significantly degrades UX (no /model, /new, /reset, /think, etc.).

Fix Action

Workaround

Mirror the default bot's command set to the secondary bots' applications via a direct PUT /applications/{app_id}/commands against Discord's REST API on gateway restart. Sample script:

#!/usr/bin/env bash
#
# Sync Discord slash commands from the default bot to secondary bots
# whose openclaw-side native deploy doesn't run.
#
# Usage:
#   ./sync-discord-commands.sh
#
# Idempotent: uses Discord's bulk PUT to upsert the entire command list,
# which doesn't burn the daily-create quota the same way one-by-one creates do.

set -euo pipefail

CONFIG="$HOME/.openclaw/openclaw.json"
SECONDARY_ACCOUNTS=("accusentry" "eidoforge")  # adjust to your account ids

extract_app_id() {
  local token="$1"
  echo "$token" | cut -d'.' -f1 | base64 -d 2>/dev/null
}

# 1. Fetch default bot's command set
default_token=$(node -e "console.log(JSON.parse(require('fs').readFileSync('$CONFIG','utf8')).channels.discord.token)")
default_app_id=$(extract_app_id "$default_token")
echo "Source: default bot (app_id=$default_app_id)"

source_commands=$(curl -s --max-time 30 -H "Authorization: Bot $default_token" \
  "https://discord.com/api/v10/applications/${default_app_id}/commands")

source_count=$(echo "$source_commands" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{console.log(JSON.parse(s).length)})')
echo "  Fetched $source_count commands"

if [[ "$source_count" -eq 0 ]]; then
  echo "Source has 0 commands — refusing to mirror an empty set." >&2
  exit 1
fi

# 2. Strip server-only metadata so commands re-apply cleanly to other apps
to_push=$(echo "$source_commands" | node -e '
let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{
  const cmds = JSON.parse(s);
  const cleaned = cmds.map(c => {
    const { id, application_id, version, ...rest } = c;
    return rest;
  });
  process.stdout.write(JSON.stringify(cleaned));
});
')

# 3. Push to each secondary account
for account in "${SECONDARY_ACCOUNTS[@]}"; do
  token=$(node -e "console.log(JSON.parse(require('fs').readFileSync('$CONFIG','utf8')).channels.discord.accounts['$account']?.token || '')")
  if [[ -z "$token" ]]; then
    echo "  $account: skipped (no token in config)"
    continue
  fi
  app_id=$(extract_app_id "$token")
  echo ""
  echo "Target: $account (app_id=$app_id)"

  result=$(curl -s --max-time 30 \
    -X PUT \
    -H "Authorization: Bot $token" \
    -H "Content-Type: application/json" \
    -d "$to_push" \
    "https://discord.com/api/v10/applications/${app_id}/commands")

  status=$(echo "$result" | node -e '
let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{
  try {
    const j = JSON.parse(s);
    if (Array.isArray(j)) console.log("ok:" + j.length);
    else console.log("err:" + JSON.stringify(j).slice(0,200));
  } catch(e) { console.log("parse_err:" + s.slice(0,200)); }
});
')

  if [[ "$status" =~ ^ok: ]]; then
    echo "  ✅ deployed ${status#ok:} commands"
  else
    echo "  ❌ ${status}"
  fi
done

echo ""
echo "Done. Discord clients may need a Ctrl+R reload to refresh the command cache."

Code Example

#!/usr/bin/env bash
#
# Sync Discord slash commands from the default bot to secondary bots
# whose openclaw-side native deploy doesn't run.
#
# Usage:
#   ./sync-discord-commands.sh
#
# Idempotent: uses Discord's bulk PUT to upsert the entire command list,
# which doesn't burn the daily-create quota the same way one-by-one creates do.

set -euo pipefail

CONFIG="$HOME/.openclaw/openclaw.json"
SECONDARY_ACCOUNTS=("accusentry" "eidoforge")  # adjust to your account ids

extract_app_id() {
  local token="$1"
  echo "$token" | cut -d'.' -f1 | base64 -d 2>/dev/null
}

# 1. Fetch default bot's command set
default_token=$(node -e "console.log(JSON.parse(require('fs').readFileSync('$CONFIG','utf8')).channels.discord.token)")
default_app_id=$(extract_app_id "$default_token")
echo "Source: default bot (app_id=$default_app_id)"

source_commands=$(curl -s --max-time 30 -H "Authorization: Bot $default_token" \
  "https://discord.com/api/v10/applications/${default_app_id}/commands")

source_count=$(echo "$source_commands" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{console.log(JSON.parse(s).length)})')
echo "  Fetched $source_count commands"

if [[ "$source_count" -eq 0 ]]; then
  echo "Source has 0 commands — refusing to mirror an empty set." >&2
  exit 1
fi

# 2. Strip server-only metadata so commands re-apply cleanly to other apps
to_push=$(echo "$source_commands" | node -e '
let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{
  const cmds = JSON.parse(s);
  const cleaned = cmds.map(c => {
    const { id, application_id, version, ...rest } = c;
    return rest;
  });
  process.stdout.write(JSON.stringify(cleaned));
});
')

# 3. Push to each secondary account
for account in "${SECONDARY_ACCOUNTS[@]}"; do
  token=$(node -e "console.log(JSON.parse(require('fs').readFileSync('$CONFIG','utf8')).channels.discord.accounts['$account']?.token || '')")
  if [[ -z "$token" ]]; then
    echo "  $account: skipped (no token in config)"
    continue
  fi
  app_id=$(extract_app_id "$token")
  echo ""
  echo "Target: $account (app_id=$app_id)"

  result=$(curl -s --max-time 30 \
    -X PUT \
    -H "Authorization: Bot $token" \
    -H "Content-Type: application/json" \
    -d "$to_push" \
    "https://discord.com/api/v10/applications/${app_id}/commands")

  status=$(echo "$result" | node -e '
let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{
  try {
    const j = JSON.parse(s);
    if (Array.isArray(j)) console.log("ok:" + j.length);
    else console.log("err:" + JSON.stringify(j).slice(0,200));
  } catch(e) { console.log("parse_err:" + s.slice(0,200)); }
});
')

  if [[ "$status" =~ ^ok: ]]; then
    echo "  ✅ deployed ${status#ok:} commands"
  else
    echo "  ❌ ${status}"
  fi
done

echo ""
echo "Done. Discord clients may need a Ctrl+R reload to refresh the command cache."
RAW_BUFFERClick to expand / collapse

Summary

In a multi-account Discord setup (multiple bots, each bound to a different agent via bindings), runDiscordCommandDeployInBackground only effectively deploys slash commands for the default account. Secondary accounts (with their own tokens under channels.discord.accounts.<id>) end up with zero commands registered with Discord, even though they probe-resolve and handle messages correctly.

Environment

  • openclaw v2026.5.5
  • @openclaw/discord v2026.5.5
  • Linux (Ubuntu, systemd-managed gateway)

Repro

  1. Configure two or more Discord accounts under channels.discord.accounts with separate bot tokens (each from a different Discord application).
  2. Add bindings to route each account to its own agent.
  3. Set commands.native: true both at top-level (commands.native) and at the discord-channel level (channels.discord.commands.native), and also per-account (channels.discord.accounts.<id>.commands.native).
  4. Restart the gateway.
  5. Query GET /applications/{app_id}/commands for each bot's application via the Discord REST API.

Expected

Each bot's application has its full command set deployed (matching the default bot's set, modulo skill / agent differences).

Actual

  • Default bot's application: full command set (e.g., 62 commands in my setup).
  • Secondary bots' applications: 0 commands.
  • The deploy-commands:scheduled and deploy-commands:done startup-phase log lines never appear for secondary accounts (verbose mode would help confirm, but no warnings appear at default verbosity either).
  • Manually PUT /applications/{secondary_app_id}/commands succeeds, confirming the bot tokens, OAuth scopes (bot + applications.commands), and Discord API access are all healthy. So the issue is in runDiscordCommandDeployInBackground not being invoked / running for non-default accounts.

Code path

provider-CnLt-Y4Z.js (per-account setup) calls runDiscordCommandDeployInBackground({ enabled: nativeEnabled, accountId, ... }) once per account. With commands.native: true set at every visible layer, resolveNativeCommandSetting should return true (early return on setting === true).

But the phase: "deploy-commands:schedule" log line never appears for secondary accounts, suggesting the call site itself is being skipped or short-circuited before reaching logDiscordStartupPhase.

Workaround

Mirror the default bot's command set to the secondary bots' applications via a direct PUT /applications/{app_id}/commands against Discord's REST API on gateway restart. Sample script:

#!/usr/bin/env bash
#
# Sync Discord slash commands from the default bot to secondary bots
# whose openclaw-side native deploy doesn't run.
#
# Usage:
#   ./sync-discord-commands.sh
#
# Idempotent: uses Discord's bulk PUT to upsert the entire command list,
# which doesn't burn the daily-create quota the same way one-by-one creates do.

set -euo pipefail

CONFIG="$HOME/.openclaw/openclaw.json"
SECONDARY_ACCOUNTS=("accusentry" "eidoforge")  # adjust to your account ids

extract_app_id() {
  local token="$1"
  echo "$token" | cut -d'.' -f1 | base64 -d 2>/dev/null
}

# 1. Fetch default bot's command set
default_token=$(node -e "console.log(JSON.parse(require('fs').readFileSync('$CONFIG','utf8')).channels.discord.token)")
default_app_id=$(extract_app_id "$default_token")
echo "Source: default bot (app_id=$default_app_id)"

source_commands=$(curl -s --max-time 30 -H "Authorization: Bot $default_token" \
  "https://discord.com/api/v10/applications/${default_app_id}/commands")

source_count=$(echo "$source_commands" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{console.log(JSON.parse(s).length)})')
echo "  Fetched $source_count commands"

if [[ "$source_count" -eq 0 ]]; then
  echo "Source has 0 commands — refusing to mirror an empty set." >&2
  exit 1
fi

# 2. Strip server-only metadata so commands re-apply cleanly to other apps
to_push=$(echo "$source_commands" | node -e '
let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{
  const cmds = JSON.parse(s);
  const cleaned = cmds.map(c => {
    const { id, application_id, version, ...rest } = c;
    return rest;
  });
  process.stdout.write(JSON.stringify(cleaned));
});
')

# 3. Push to each secondary account
for account in "${SECONDARY_ACCOUNTS[@]}"; do
  token=$(node -e "console.log(JSON.parse(require('fs').readFileSync('$CONFIG','utf8')).channels.discord.accounts['$account']?.token || '')")
  if [[ -z "$token" ]]; then
    echo "  $account: skipped (no token in config)"
    continue
  fi
  app_id=$(extract_app_id "$token")
  echo ""
  echo "Target: $account (app_id=$app_id)"

  result=$(curl -s --max-time 30 \
    -X PUT \
    -H "Authorization: Bot $token" \
    -H "Content-Type: application/json" \
    -d "$to_push" \
    "https://discord.com/api/v10/applications/${app_id}/commands")

  status=$(echo "$result" | node -e '
let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{
  try {
    const j = JSON.parse(s);
    if (Array.isArray(j)) console.log("ok:" + j.length);
    else console.log("err:" + JSON.stringify(j).slice(0,200));
  } catch(e) { console.log("parse_err:" + s.slice(0,200)); }
});
')

  if [[ "$status" =~ ^ok: ]]; then
    echo "  ✅ deployed ${status#ok:} commands"
  else
    echo "  ❌ ${status}"
  fi
done

echo ""
echo "Done. Discord clients may need a Ctrl+R reload to refresh the command cache."

Why this matters

Multi-account Discord setups are explicitly supported by openclaw via channels.discord.accounts + bindings (per-persona bots, e.g. one for work, one for personal). Without slash command auto-deploy for secondary accounts, every persona except the default loses the entire / command surface, which significantly degrades UX (no /model, /new, /reset, /think, etc.).

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

openclaw - 💡(How to fix) Fix Discord: native slash commands not deployed for secondary channel accounts even with commands.native: true [1 comments, 2 participants]