
Discord
- 6 installs
- 134 repo stars
- Updated August 4, 2026
- openhands/extensions
This is a copy of discord by openhands - installs and ranking accrue to the original listing.
Helps with ai & agent building tasks.
About
discord is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- discord
- AI & Agent Building
- AI-coding skill
Discord by the numbers
- 6 all-time installs (skills.sh)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/openhands/extensions --skill discordAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 6 |
|---|---|
| repo stars | ★ 134 |
| Last updated | August 4, 2026 |
| Repository | openhands/extensions ↗ |
What it does
Helps with ai & agent building tasks.
Files
Discord
Use this skill when implementing or automating Discord integrations.
Pick the right approach
1. Incoming webhooks (best for one-way posting)
- Good for CI notifications, alerts, build status, etc.
- No bot user needed.
- See: https://discord.com/developers/docs/resources/webhook#execute-webhook
2. Bot token + REST API (two-way / richer automation)
- Use when you need to post as a bot, manage channels, read history, moderate, etc.
- REST API base:
https://discord.com/api/v10 - Most REST calls use
Authorization: Bot <token>.
3. Interactions / slash commands (user-invoked commands)
- Use application commands and interaction webhooks.
- Typically requires running a web server to receive interactions and respond quickly.
Secrets & safety
- Never hard-code tokens. Use environment variables:
DISCORD_WEBHOOK_URLfor incoming webhooksDISCORD_BOT_TOKENfor bot REST API calls- Treat webhook URLs as secrets (they include a token).
- Do not automate normal user accounts (“self-bots”). Use official bot/OAuth flows.
Footguns / safety notes (read this)
- Webhook URLs are secrets (the token is embedded in the URL). Don’t paste them into issues, logs, CI output, or chat.
- Mentions are dangerous by default: always set
allowed_mentionsto something strict (these examples use{"parse": []}) to avoid accidentally pinging@everyone/ roles. - Watch for accidental secret logging:
- If you build your own scripts, avoid including full webhook URLs in exception messages.
- The bundled scripts sanitize webhook URLs in error output, but you should still avoid printing the URL yourself.
- Rate limits: handle HTTP 429 with
retry_after/Retry-After, and don’t retry forever.
Quick recipes
Post a message via an incoming webhook (recommended)
Discord requires at least one of content, embeds, components, file, or poll.
curl -sS -X POST \
-H 'Content-Type: application/json' \
-d '{"content":"Hello from OpenHands","allowed_mentions":{"parse":[]}}' \
"$DISCORD_WEBHOOK_URL"Post a message to a channel with a bot token
Endpoint: POST /channels/{channel_id}/messages (Create Message)
CHANNEL_ID="..."
curl -sS -X POST "https://discord.com/api/v10/channels/${CHANNEL_ID}/messages" \
-H "Authorization: Bot $DISCORD_BOT_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"content":"Hello from my bot","allowed_mentions":{"parse":[]}}'Docs: https://discord.com/developers/docs/resources/channel#create-message
Automation scripts (bundled)
These scripts are self-contained and only use the Python standard library.
- Post to a webhook:
python3 -m skills.discord.scripts.post_webhook --content "Build finished" --wait- Post to a channel using a bot token:
python3 -m skills.discord.scripts.send_message --channel-id "$CHANNEL_ID" --content "Hello"Rate limits
- Don’t hard-code limits. Use Discord’s
Retry-After/retry_afterand rate-limit headers when present. - On HTTP 429, wait for the provided delay (clamp to a sane maximum, add small jitter), then retry.
Docs: https://discord.com/developers/docs/topics/rate-limits
Slash commands / application commands
- Use guild commands for fast iteration (instant updates).
- Use global commands when ready; propagation can take longer.
Docs: https://discord.com/developers/docs/interactions/application-commands
Reference
For more details (OAuth2 flows, command registration endpoints, troubleshooting), see:
- references/REFERENCE.md
{
"name": "discord",
"version": "1.0.0",
"description": "Build and automate Discord integrations (bots, webhooks, slash commands, and REST API workflows). Use when the user mentions Discord, a Discord server/guild, channels, webhooks, bot tokens, slash c...",
"author": {
"name": "OpenHands",
"email": "contact@all-hands.dev"
},
"homepage": "https://github.com/OpenHands/extensions",
"repository": "https://github.com/OpenHands/extensions",
"license": "MIT",
"keywords": [
"discord",
"bot",
"webhook",
"automation"
]
}
Discord
Build and automate Discord integrations (bots, webhooks, slash commands, and REST API workflows). Use when the user mentions Discord, a Discord server/guild, channels, webhooks, bot tokens, slash commands/application commands, discord.js, or discord.py.
Triggers
This skill is activated by the following keywords:
discorddiscord apidiscord botdiscord webhookdiscord.jsdiscord.py
Details
This skill focuses on practical Discord automation patterns:
- Prefer incoming webhooks for one-way notifications.
- Use bot tokens + REST API for richer automation.
- Handle rate limits (HTTP 429) by waiting
retry_afterbefore retrying.
See also: references/REFERENCE.md.
Footguns / gotchas
- Webhook URLs contain a secret token; don’t log or share them.
- Set
allowed_mentionsstrictly (e.g.{ "parse": [] }) to avoid accidental pings. - Handle HTTP 429 using
retry_after/Retry-Afterand avoid infinite retries.
Discord reference
Official docs
- Discord Developer Docs (home): https://discord.com/developers/docs/intro
- REST API versioning:
https://discord.com/api/v10(use v10 endpoints) - Create Message (REST): https://discord.com/developers/docs/resources/channel#create-message
- Webhooks: https://discord.com/developers/docs/resources/webhook
- OAuth2: https://discord.com/developers/docs/topics/oauth2
- Application Commands (slash commands): https://discord.com/developers/docs/interactions/application-commands
- Rate limits: https://discord.com/developers/docs/topics/rate-limits
Footguns / gotchas
- Incoming webhook URLs contain a secret token; treat the entire URL like a password.
- If you include request URLs in error logs, sanitize
/webhooks/{id}/{token}(the token is secret). - Use
allowed_mentionsto prevent accidental mass pings. - Respect rate limits. Don’t spin in tight retry loops on 429.
Common workflows
1) Simple notifications (incoming webhook)
1. Create an incoming webhook in the Discord client (channel settings → Integrations → Webhooks). 2. Store it in DISCORD_WEBHOOK_URL. 3. POST JSON like { "content": "..." }.
Key points from Discord docs:
- Execute webhook:
POST /webhooks/{webhook.id}/{webhook.token}. - Must include at least one of
content,embeds,components,file, orpoll. - Content limit is 2000 characters.
- Use
allowed_mentionsto prevent accidental pings.
2) Bot token + REST API
1. Create an app + bot user in the Discord Developer Portal. 2. Invite the bot to a guild using an OAuth2 URL with the bot scope. 3. Use Authorization: Bot $DISCORD_BOT_TOKEN for REST calls.
For posting messages:
POST /channels/{channel_id}/messages
Helpful debugging hints:
- 401: invalid token / wrong auth header format
- 403: missing permissions in channel (e.g., Send Messages)
- 404: wrong ID or bot doesn’t have access to channel
3) Slash commands / interactions
- Command registration is done via HTTP endpoints.
- Use guild commands during development for instant updates.
- If you implement the HTTP interactions endpoint yourself, you must verify request signatures.
4) OAuth2 notes
Discord strongly recommends using the state parameter to prevent CSRF.
OAuth2 endpoints require application/x-www-form-urlencoded content type; JSON is not permitted.
5) Rate limits
Don’t hard-code limits; parse headers like:
X-RateLimit-LimitX-RateLimit-RemainingX-RateLimit-Reset-AfterX-RateLimit-Bucket
On HTTP 429:
- Read
Retry-After/ JSONretry_after, wait, then retry.
Suggested environment variables
DISCORD_WEBHOOK_URLDISCORD_BOT_TOKENDISCORD_CHANNEL_ID(optional convenience)DISCORD_GUILD_ID(optional, for command registration)DISCORD_APPLICATION_ID(optional, for command registration)
from __future__ import annotations
import random
import time
from dataclasses import dataclass
from typing import Any, Mapping
import requests
@dataclass(frozen=True)
class DiscordRateLimitInfo:
retry_after: float
is_global: bool
bucket: str | None
remaining: str | None
reset_after: str | None
class DiscordHTTPError(RuntimeError):
pass
def _parse_rate_limit_info(*, status_code: int, headers: Mapping[str, str], json_body: Any) -> DiscordRateLimitInfo | None:
if status_code != 429:
return None
retry_after: float | None = None
is_global = False
if isinstance(json_body, dict):
retry_after_val = json_body.get("retry_after")
if isinstance(retry_after_val, (int, float, str)):
try:
retry_after = float(retry_after_val)
except ValueError:
retry_after = None
is_global = bool(json_body.get("global", False))
if retry_after is None:
hdr = headers.get("Retry-After")
if hdr is not None:
try:
retry_after = float(hdr)
except ValueError:
retry_after = None
if retry_after is None:
reset_after = headers.get("X-RateLimit-Reset-After")
if reset_after is not None:
try:
retry_after = float(reset_after)
except ValueError:
retry_after = None
if retry_after is None:
return None
return DiscordRateLimitInfo(
retry_after=retry_after,
is_global=is_global,
bucket=headers.get("X-RateLimit-Bucket"),
remaining=headers.get("X-RateLimit-Remaining"),
reset_after=headers.get("X-RateLimit-Reset-After"),
)
def post_json(
*,
url: str,
headers: Mapping[str, str],
payload: Mapping[str, object],
timeout_s: float = 30,
max_retries: int = 3,
max_retry_after_s: float = 60.0,
jitter_s: float = 0.25,
redact_url_in_errors: bool = False,
) -> dict[str, object] | None:
attempt = 0
while True:
attempt += 1
try:
resp = requests.post(url, headers=dict(headers), json=dict(payload), timeout=timeout_s)
except requests.RequestException as e:
raise DiscordHTTPError(f"HTTP request failed ({e}).") from e
body_text = resp.text or ""
json_body: Any = None
if body_text:
try:
json_body = resp.json()
except ValueError:
json_body = None
rl = _parse_rate_limit_info(status_code=resp.status_code, headers=resp.headers, json_body=json_body)
if rl is not None and attempt <= max_retries:
sleep_s = min(max(0.0, rl.retry_after), max_retry_after_s)
if jitter_s > 0:
sleep_s += random.uniform(0.0, jitter_s)
time.sleep(sleep_s)
continue
if resp.status_code >= 400:
context_bits: list[str] = []
if not redact_url_in_errors:
context_bits.append(f"url={url}")
if rl is not None:
context_bits.append(f"rate_limit_global={rl.is_global}")
if rl.bucket is not None:
context_bits.append(f"rate_limit_bucket={rl.bucket}")
if rl.remaining is not None:
context_bits.append(f"rate_limit_remaining={rl.remaining}")
if rl.reset_after is not None:
context_bits.append(f"rate_limit_reset_after={rl.reset_after}")
context = (" " + " ".join(context_bits)) if context_bits else ""
msg = f"HTTP request failed (HTTP {resp.status_code}).{context}"
if body_text:
msg += f" Response: {body_text[:500]}"
raise DiscordHTTPError(msg)
if not body_text:
return None
if isinstance(json_body, dict):
return json_body
return {"raw": body_text}
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import json
import os
import sys
import urllib.parse
import urllib.request
from ._http import DiscordHTTPError, post_json
def _with_wait_param(url: str, *, wait: bool) -> str:
if not wait:
return url
parts = urllib.parse.urlsplit(url)
query = dict(urllib.parse.parse_qsl(parts.query, keep_blank_values=True))
query["wait"] = "true"
return urllib.parse.urlunsplit(
(parts.scheme, parts.netloc, parts.path, urllib.parse.urlencode(query), parts.fragment)
)
def _request_json(url: str, payload: dict[str, object], *, wait: bool, max_retries: int) -> dict[str, object] | None:
request_url = _with_wait_param(url, wait=wait)
headers = {
"Content-Type": "application/json",
"User-Agent": "OpenHands-DiscordSkill/1.0 (+https://github.com/OpenHands/skills)",
}
return post_json(
url=request_url,
headers=headers,
payload=payload,
max_retries=max_retries,
redact_url_in_errors=True,
)
def main() -> int:
parser = argparse.ArgumentParser(
description=(
"Post a message to a Discord incoming webhook. "
"The webhook URL is secret; avoid printing/logging it."
)
)
parser.add_argument(
"--webhook-url",
default=os.getenv("DISCORD_WEBHOOK_URL"),
help="Incoming webhook URL (default: $DISCORD_WEBHOOK_URL)",
)
parser.add_argument(
"--content",
help="Message content (max 2000 characters). If omitted, read from stdin.",
)
parser.add_argument(
"--wait",
action="store_true",
help="Add ?wait=true to get the created message object.",
)
parser.add_argument(
"--max-retries",
type=int,
default=3,
help="Retries on HTTP 429 (default: 3).",
)
args = parser.parse_args()
if not args.webhook_url:
print("Missing --webhook-url (or set DISCORD_WEBHOOK_URL).", file=sys.stderr)
return 2
content = args.content
if content is None:
content = sys.stdin.read().strip()
if not content:
print("No content provided (use --content or stdin).", file=sys.stderr)
return 2
payload = {
"content": content,
"allowed_mentions": {"parse": []},
}
result = _request_json(
args.webhook_url,
payload,
wait=args.wait,
max_retries=max(0, args.max_retries),
)
if result is not None:
print(json.dumps(result, indent=2, sort_keys=True))
return 0
if __name__ == "__main__":
raise SystemExit(main())
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import json
import os
import sys
import urllib.request
from ._http import DiscordHTTPError, post_json
API_BASE = "https://discord.com/api/v10"
def _post_message(
*,
token: str,
channel_id: str,
payload: dict[str, object],
max_retries: int,
) -> dict[str, object] | None:
url = f"{API_BASE}/channels/{channel_id}/messages"
headers = {
"Authorization": f"Bot {token}",
"Content-Type": "application/json",
"User-Agent": "OpenHands-DiscordSkill/1.0 (+https://github.com/OpenHands/skills)",
}
try:
return post_json(url=url, headers=headers, payload=payload, max_retries=max_retries)
except DiscordHTTPError as e:
raise DiscordHTTPError(f"Discord API call failed. channel_id={channel_id}. {e}") from e
def main() -> int:
parser = argparse.ArgumentParser(description="Send a message to a Discord channel using a bot token.")
parser.add_argument(
"--token",
default=os.getenv("DISCORD_BOT_TOKEN"),
help="Bot token (default: $DISCORD_BOT_TOKEN)",
)
parser.add_argument(
"--channel-id",
default=os.getenv("DISCORD_CHANNEL_ID"),
help="Channel ID (default: $DISCORD_CHANNEL_ID)",
)
parser.add_argument(
"--content",
help="Message content (max 2000 characters). If omitted, read from stdin.",
)
parser.add_argument(
"--max-retries",
type=int,
default=3,
help="Retries on HTTP 429 (default: 3).",
)
parser.add_argument(
"--allow-mentions",
action="store_true",
help="Allow Discord to parse mentions. Default is safe (no mentions).",
)
args = parser.parse_args()
if not args.token:
print("Missing --token (or set DISCORD_BOT_TOKEN).", file=sys.stderr)
return 2
if not args.channel_id:
print("Missing --channel-id (or set DISCORD_CHANNEL_ID).", file=sys.stderr)
return 2
content = args.content
if content is None:
content = sys.stdin.read().strip()
if not content:
print("No content provided (use --content or stdin).", file=sys.stderr)
return 2
payload: dict[str, object] = {"content": content}
if not args.allow_mentions:
payload["allowed_mentions"] = {"parse": []}
result = _post_message(
token=args.token,
channel_id=args.channel_id,
payload=payload,
max_retries=max(0, args.max_retries),
)
if result is not None:
print(json.dumps(result, indent=2, sort_keys=True))
return 0
if __name__ == "__main__":
raise SystemExit(main())