A moderation command that anyone can run, or that tries to ban someone above the bot in the role list, is a bug waiting to embarrass you. Real commands check three things before they act: does this user have permission, does the bot have permission, and is the target actually below both of us in the role hierarchy. This guide covers all three, plus the "gate the command in the first place" layer, for both discord.js and discord.py.
| At a glance | |
|---|---|
| You need | a working bot with slash commands (see Slash commands in depth) |
| Plan | Free or premium — permission logic runs the same on both |
| Time | about twenty minutes |
There are two layers, and good bots use both: hide the command from people who shouldn't see it, and check again in code before acting. The hide is convenience; the check is the real guard.
Layer 1: gate who even sees the command
When you register a command, setDefaultMemberPermissions tells Discord which permission a member needs before the command appears for them at all — a recap of what Slash commands in depth introduced:
const { SlashCommandBuilder, PermissionFlagsBits } = require('discord.js');
const kick = new SlashCommandBuilder()
.setName('kick')
.setDescription('Kick a member')
.setDefaultMemberPermissions(PermissionFlagsBits.KickMembers)
.addUserOption(o => o.setName('target').setDescription('Who to kick').setRequired(true));
Now /kick is hidden from anyone without Kick Members — and server admins can further tune who sees it under Server Settings → Integrations.
⚠️ Heads up: This is a UI gate, not a security boundary. A server admin can re-enable the command for roles you didn't intend, and it says nothing about whether the bot can act. Always re-check in code (Layer 2). Never rely on the default-permissions setting alone.
Layer 2: check the user's permission in code
Inside the handler, read the invoking member's permissions with memberPermissions.has(...) and refuse early if it's missing:
if (!interaction.memberPermissions.has(PermissionFlagsBits.KickMembers)) {
return interaction.reply({ content: "You don't have permission to do that.", ephemeral: true });
}
This guard clause — check, deny with an ephemeral reply, return — is the shape every protected command starts with. memberPermissions reflects the user's effective permissions in the current channel, including role and channel overrides.
Check the bot's own permission too
The user being allowed doesn't mean the bot can do the job. Check the bot's permissions before you try — appPermissions is what your bot has in the channel the command was used in:
if (!interaction.appPermissions.has(PermissionFlagsBits.KickMembers)) {
return interaction.reply({ content: "I don't have the Kick Members permission here.", ephemeral: true });
}
Checking first means a clean, explanatory reply instead of an error thrown deep in the action.
The role hierarchy: the check people forget
Discord enforces a simple rule: you can only act on members whose highest role is below yours — and the same applies to the bot. A bot can't kick the server owner or anyone with a role above its own, no matter what permissions it has. Check it with comparePositionTo, which returns a positive number when the first role is higher:
const target = interaction.options.getMember('target');
const me = interaction.guild.members.me;
// Bot must outrank the target:
if (me.roles.highest.comparePositionTo(target.roles.highest) <= 0) {
return interaction.reply({ content: "That member is above me in the role list — I can't act on them.", ephemeral: true });
}
// The user running it should outrank the target too:
if (interaction.member.roles.highest.comparePositionTo(target.roles.highest) <= 0) {
return interaction.reply({ content: "You can't moderate someone with a role equal to or above yours.", ephemeral: true });
}
await target.kick('Requested via /kick');
await interaction.reply(`Kicked ${target.user.tag}.`);
Put together, that's the check-before-acting pattern in full: verify the user, verify the bot, verify the hierarchy, then act.
The discord.py equivalents
Every piece has a direct counterpart.
Gate the command with @app_commands.default_permissions, and let the library check the user for you with @app_commands.checks.has_permissions:
from discord import app_commands
@app_commands.command(name="kick", description="Kick a member")
@app_commands.default_permissions(kick_members=True)
@app_commands.checks.has_permissions(kick_members=True)
async def kick(interaction: discord.Interaction, target: discord.Member):
...
If the user lacks the permission, discord.py raises MissingPermissions, which your error handler (see Crash-proof your bot) can answer cleanly. To check by hand instead, read interaction.user.guild_permissions.kick_members.
Check the bot with interaction.guild.me.guild_permissions, and the hierarchy by comparing roles directly — discord.py lets you use > on roles:
me = interaction.guild.me
if not me.guild_permissions.kick_members:
return await interaction.response.send_message("I don't have Kick Members.", ephemeral=True)
if me.top_role <= target.top_role:
return await interaction.response.send_message("That member is above me.", ephemeral=True)
if interaction.user.top_role <= target.top_role:
return await interaction.response.send_message("They outrank you.", ephemeral=True)
await target.kick(reason="Requested via /kick")
await interaction.response.send_message(f"Kicked {target}.")
A cheat sheet
| Check | discord.js | discord.py |
|---|---|---|
| Gate command visibility | .setDefaultMemberPermissions(...) |
@app_commands.default_permissions(...) |
| User has permission | interaction.memberPermissions.has(...) |
interaction.user.guild_permissions.<perm> |
| Bot has permission | interaction.appPermissions.has(...) |
interaction.guild.me.guild_permissions.<perm> |
| Bot outranks target | me.roles.highest.comparePositionTo(t) > 0 |
me.top_role > target.top_role |
🎯 Good to know: The Administrator permission is a master key — a member with it passes every permission check regardless of the specific flags. That includes the server owner, who always outranks everyone. Your hierarchy checks handle the owner case for free, since no role sits above them.
Troubleshooting
Missing Permissionserror when the bot acts — you checked the user but not the bot, or not the hierarchy. Add theappPermissionsandcomparePositionToguards above so the bot refuses gracefully instead of throwing.- The command is hidden even from admins —
setDefaultMemberPermissionsset to0hides it from everyone but admins; that's intended. Loosen the required permission, or have an admin adjust it under Server Settings → Integrations. getMemberreturns null — the user isn't in the guild, or you need the Server Members intent for full member data; enable it on the Bot page only if your bot genuinely needs it (see Your first Discord bot).- Everyone can still run a "hidden" command — default permissions only affect visibility. Someone can't run what they can't see in the UI, but you must still check in code, because integrations settings and future changes can expose it.
Next steps
- Slash commands in depth
- Crash-proof your bot
- One bot, many servers
- Full permission references: discord.js.org and discordpy.readthedocs.io