Check permissions before your bot acts

Before a command kicks, bans, or deletes, it should confirm the user is allowed — and that the bot itself can. Permission checks and role-hierarchy rules for discord.js and discord.py.

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 Permissions error when the bot acts — you checked the user but not the bot, or not the hierarchy. Add the appPermissions and comparePositionTo guards above so the bot refuses gracefully instead of throwing.
  • The command is hidden even from adminssetDefaultMemberPermissions set to 0 hides it from everyone but admins; that's intended. Loosen the required permission, or have an admin adjust it under Server Settings → Integrations.
  • getMember returns 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

Was this guide helpful?