Claude Code Plugins

Community-maintained marketplace

Feedback
1
0

|

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name discord-bot
description Build Discord bots with modern frameworks. Use when: creating Discord bot, slash commands, moderation bot. Triggers: "discord", "discord bot", "serenity", "discord.js", "discord.py".

Discord Bot Development

Project Protection Setup

MANDATORY before writing any code:

# 1. Create .gitignore
cat >> .gitignore << 'EOF'
# Build
target/
node_modules/
__pycache__/
dist/

# Secrets - CRITICAL for bots!
.env
.env.*
!.env.example
bot_token.txt
config.json  # If contains token

# IDE
.idea/
.vscode/
.DS_Store
EOF

# 2. Setup pre-commit hooks
cat > .pre-commit-config.yaml << 'EOF'
repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v5.0.0
    hooks:
      - id: detect-private-key
      - id: check-added-large-files
  - repo: https://github.com/gitleaks/gitleaks
    rev: v8.21.2
    hooks:
      - id: gitleaks
EOF

pre-commit install

Why critical: Discord bot tokens give FULL access. Leaked token = bot compromised, can spam users.


Stack Options

Language Framework Best For
Rust serenity + poise Performance, type safety
Python discord.py / nextcord Rapid development
Node discord.js JS ecosystem, largest community

Quick Start

Rust (serenity + poise)

# Cargo.toml
[dependencies]
serenity = { version = "0.12", features = ["framework"] }
poise = "0.6"
tokio = { version = "1", features = ["full"] }
use poise::serenity_prelude as serenity;

struct Data {}
type Error = Box<dyn std::error::Error + Send + Sync>;
type Context<'a> = poise::Context<'a, Data, Error>;

/// Say hello
#[poise::command(slash_command)]
async fn hello(ctx: Context<'_>) -> Result<(), Error> {
    ctx.say("Hello!").await?;
    Ok(())
}

#[tokio::main]
async fn main() {
    let framework = poise::Framework::builder()
        .options(poise::FrameworkOptions {
            commands: vec![hello()],
            ..Default::default()
        })
        .setup(|ctx, _ready, framework| {
            Box::pin(async move {
                poise::builtins::register_globally(ctx, &framework.options().commands).await?;
                Ok(Data {})
            })
        })
        .build();

    let token = std::env::var("DISCORD_TOKEN").unwrap();
    let intents = serenity::GatewayIntents::non_privileged();

    let client = serenity::ClientBuilder::new(token, intents)
        .framework(framework)
        .await
        .unwrap();

    client.start().await.unwrap();
}

Python (discord.py)

# requirements.txt
discord.py>=2.0
import discord
from discord import app_commands

intents = discord.Intents.default()
client = discord.Client(intents=intents)
tree = app_commands.CommandTree(client)

@tree.command(name="hello", description="Say hello")
async def hello(interaction: discord.Interaction):
    await interaction.response.send_message("Hello!")

@client.event
async def on_ready():
    await tree.sync()
    print(f"Logged in as {client.user}")

client.run("BOT_TOKEN")

Node (discord.js)

// package.json: "discord.js": "^14.14"
import { Client, GatewayIntentBits, SlashCommandBuilder, REST, Routes } from 'discord.js';

const client = new Client({ intents: [GatewayIntentBits.Guilds] });

const commands = [
  new SlashCommandBuilder().setName('hello').setDescription('Say hello'),
].map(cmd => cmd.toJSON());

client.once('ready', async () => {
  const rest = new REST().setToken(process.env.DISCORD_TOKEN!);
  await rest.put(Routes.applicationCommands(client.user!.id), { body: commands });
  console.log(`Logged in as ${client.user?.tag}`);
});

client.on('interactionCreate', async (interaction) => {
  if (!interaction.isChatInputCommand()) return;
  if (interaction.commandName === 'hello') {
    await interaction.reply('Hello!');
  }
});

client.login(process.env.DISCORD_TOKEN);

Slash Commands

With Options (Rust/poise)

/// Ban a user
#[poise::command(slash_command, required_permissions = "BAN_MEMBERS")]
async fn ban(
    ctx: Context<'_>,
    #[description = "User to ban"] user: serenity::User,
    #[description = "Reason"] reason: Option<String>,
) -> Result<(), Error> {
    let reason = reason.unwrap_or_else(|| "No reason provided".to_string());

    ctx.guild_id()
        .unwrap()
        .ban_with_reason(&ctx.serenity_context().http, user.id, 0, &reason)
        .await?;

    ctx.say(format!("Banned {} for: {}", user.name, reason)).await?;
    Ok(())
}

With Options (Python)

@tree.command(name="ban", description="Ban a user")
@app_commands.describe(user="User to ban", reason="Reason for ban")
@app_commands.default_permissions(ban_members=True)
async def ban(
    interaction: discord.Interaction,
    user: discord.Member,
    reason: str = "No reason provided"
):
    await user.ban(reason=reason)
    await interaction.response.send_message(f"Banned {user.name} for: {reason}")

Embeds

Rust

use serenity::builder::{CreateEmbed, CreateMessage};

let embed = CreateEmbed::new()
    .title("User Info")
    .description("Details about the user")
    .field("Username", &user.name, true)
    .field("ID", user.id.to_string(), true)
    .color(0x00ff00)
    .thumbnail(user.avatar_url().unwrap_or_default());

ctx.send(poise::CreateReply::default().embed(embed)).await?;

Python

embed = discord.Embed(
    title="User Info",
    description="Details about the user",
    color=0x00ff00
)
embed.add_field(name="Username", value=user.name, inline=True)
embed.add_field(name="ID", value=str(user.id), inline=True)
embed.set_thumbnail(url=user.avatar.url if user.avatar else None)

await interaction.response.send_message(embed=embed)

Buttons & Components

Rust

use serenity::builder::{CreateButton, CreateActionRow};

let button = CreateButton::new("confirm")
    .label("Confirm")
    .style(serenity::ButtonStyle::Primary);

let cancel = CreateButton::new("cancel")
    .label("Cancel")
    .style(serenity::ButtonStyle::Danger);

let row = CreateActionRow::Buttons(vec![button, cancel]);

ctx.send(poise::CreateReply::default()
    .content("Are you sure?")
    .components(vec![row])
).await?;

Python

from discord.ui import Button, View

class ConfirmView(View):
    @discord.ui.button(label="Confirm", style=discord.ButtonStyle.primary)
    async def confirm(self, interaction: discord.Interaction, button: Button):
        await interaction.response.send_message("Confirmed!")
        self.stop()

    @discord.ui.button(label="Cancel", style=discord.ButtonStyle.danger)
    async def cancel(self, interaction: discord.Interaction, button: Button):
        await interaction.response.send_message("Cancelled!")
        self.stop()

view = ConfirmView()
await interaction.response.send_message("Are you sure?", view=view)

Event Handlers

Rust

struct Handler;

#[serenity::async_trait]
impl serenity::EventHandler for Handler {
    async fn message(&self, ctx: serenity::Context, msg: serenity::Message) {
        if msg.content == "!ping" {
            msg.channel_id.say(&ctx.http, "Pong!").await.ok();
        }
    }

    async fn guild_member_addition(&self, ctx: serenity::Context, member: serenity::Member) {
        if let Some(channel) = member.guild_id.to_guild_cached(&ctx.cache) {
            // Send welcome message
        }
    }
}

Python

@client.event
async def on_message(message: discord.Message):
    if message.author.bot:
        return
    if message.content == "!ping":
        await message.channel.send("Pong!")

@client.event
async def on_member_join(member: discord.Member):
    channel = member.guild.system_channel
    if channel:
        await channel.send(f"Welcome {member.mention}!")

Permissions

Rust

#[poise::command(
    slash_command,
    required_permissions = "MANAGE_MESSAGES",  // Bot needs this
    default_member_permissions = "MANAGE_MESSAGES",  // User needs this
)]
async fn clear(
    ctx: Context<'_>,
    #[description = "Number of messages"] count: u8,
) -> Result<(), Error> {
    let messages = ctx.channel_id()
        .messages(&ctx.serenity_context().http, serenity::GetMessages::new().limit(count))
        .await?;

    ctx.channel_id()
        .delete_messages(&ctx.serenity_context().http, messages)
        .await?;

    ctx.say(format!("Deleted {} messages", count)).await?;
    Ok(())
}

Sharding (for 2500+ servers)

Rust

let client = serenity::ClientBuilder::new(token, intents)
    .framework(framework)
    .await?;

// Auto-sharding
client.start_autosharded().await?;

// Or manual sharding
// client.start_shard(shard_id, total_shards).await?;

Python

from discord import AutoShardedClient

client = AutoShardedClient(intents=intents)

Environment & Security

# .env
DISCORD_TOKEN=your_bot_token
GUILD_ID=123456789  # For development

# .gitignore
.env

Common Pitfalls

Pitfall Solution
Commands not showing Call tree.sync() / register commands
Missing intents Enable in Developer Portal + code
Rate limits Use caching, batch operations
Privileged intents Enable in portal for members/presence
Token exposed Use env vars, never commit

Bot Permissions Calculator

Required intents for common features:

  • Messages: MESSAGE_CONTENT (privileged)
  • Members: GUILD_MEMBERS (privileged)
  • Presence: GUILD_PRESENCES (privileged)

Invite URL format:

https://discord.com/api/oauth2/authorize?client_id=YOUR_ID&permissions=PERMS&scope=bot%20applications.commands

Testing

Rust (serenity/poise)

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_parse_duration() {
        assert_eq!(parse_duration("1h"), Duration::hours(1));
        assert_eq!(parse_duration("30m"), Duration::minutes(30));
    }

    #[tokio::test]
    async fn test_ban_requires_permission() {
        // Test permission checks
        let result = check_ban_permission(user_without_perms).await;
        assert!(result.is_err());
    }
}

Python (discord.py)

import pytest
from unittest.mock import AsyncMock, MagicMock

@pytest.fixture
def mock_interaction():
    interaction = MagicMock()
    interaction.response.send_message = AsyncMock()
    interaction.user.guild_permissions.ban_members = True
    return interaction

@pytest.mark.asyncio
async def test_hello_command(mock_interaction):
    await hello(mock_interaction)
    mock_interaction.response.send_message.assert_called_once()

@pytest.mark.asyncio
async def test_ban_without_permission(mock_interaction):
    mock_interaction.user.guild_permissions.ban_members = False
    with pytest.raises(discord.errors.Forbidden):
        await ban(mock_interaction, MagicMock())

Node (discord.js)

import { describe, it, expect, vi } from 'vitest';

describe('Commands', () => {
  it('hello command replies', async () => {
    const interaction = {
      reply: vi.fn(),
      isChatInputCommand: () => true,
      commandName: 'hello',
    };

    await handleInteraction(interaction);
    expect(interaction.reply).toHaveBeenCalled();
  });
});

TDD Workflow

1. Task[tdd-test-writer]: "Create /ban slash command"
   → Writes test expecting permission check + success
   → cargo test / pytest / npm test → FAILS (RED)

2. Task[rust-developer]: "Implement /ban command"
   → Implements with permission checks
   → Tests PASS (GREEN)

3. Repeat for each command

4. Task[code-reviewer]: "Review bot implementation"
   → Checks security, permissions, rate limits

Security Checklist

  • Token in environment variable (never in code)
  • .env in .gitignore
  • pre-commit hooks with gitleaks
  • Permission checks on all moderation commands
  • Rate limiting for user commands
  • Input sanitization (no injection in embeds)
  • No privileged intents unless needed
  • Audit log for moderation actions

Project Structure

discord-bot/
├── src/
│   ├── main.rs
│   ├── commands/
│   │   ├── mod.rs
│   │   ├── moderation.rs
│   │   └── fun.rs
│   └── events.rs
├── tests/
│   └── commands_test.rs
├── Cargo.toml
├── .env.example
├── .env              # NOT committed
└── .gitignore