This recipe adds the three moderation commands almost every server wants — /kick, /ban, and /timeout — to a discord.js bot. Each one takes a member and an optional reason, refuses to act on someone the bot can't touch, and writes the reason into Discord's audit log.
| At a glance | |
|---|---|
| You need | a working bot from Host a discord.js bot |
| Plan | Free or premium |
| Time | About twenty minutes |
New to bots? Build the discord.js starter first — this recipe assumes you already have a server running the Node.js application with your token in .env, and it reuses that guide's Client + Events structure.
The code
This is the whole index.js. It registers three commands and answers them in one InteractionCreate handler.
require('dotenv').config();
const {
Client, Events, GatewayIntentBits,
SlashCommandBuilder, PermissionFlagsBits,
} = require('discord.js');
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
const commands = [
new SlashCommandBuilder()
.setName('kick')
.setDescription('Kick a member')
.addUserOption(o => o.setName('member').setDescription('Who to kick').setRequired(true))
.addStringOption(o => o.setName('reason').setDescription('Why').setMaxLength(400))
.setDefaultMemberPermissions(PermissionFlagsBits.KickMembers),
new SlashCommandBuilder()
.setName('ban')
.setDescription('Ban a member')
.addUserOption(o => o.setName('member').setDescription('Who to ban').setRequired(true))
.addStringOption(o => o.setName('reason').setDescription('Why').setMaxLength(400))
.setDefaultMemberPermissions(PermissionFlagsBits.BanMembers),
new SlashCommandBuilder()
.setName('timeout')
.setDescription('Time a member out')
.addUserOption(o => o.setName('member').setDescription('Who to time out').setRequired(true))
.addIntegerOption(o => o.setName('minutes').setDescription('How long, in minutes').setRequired(true).setMinValue(1).setMaxValue(40320))
.addStringOption(o => o.setName('reason').setDescription('Why').setMaxLength(400))
.setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers),
];
client.once(Events.ClientReady, async (c) => {
await c.application.commands.set(commands);
console.log(`Listening as ${c.user.tag}`);
});
client.on(Events.InteractionCreate, async (interaction) => {
if (!interaction.isChatInputCommand()) return;
const target = interaction.options.getMember('member');
const reason = interaction.options.getString('reason') ?? 'No reason given';
if (!target) {
return interaction.reply({ content: 'That user is not in this server.', ephemeral: true });
}
if (interaction.commandName === 'kick') {
if (!target.kickable) {
return interaction.reply({ content: "I can't kick that member — my role is too low or they're above me.", ephemeral: true });
}
await target.kick(reason);
await interaction.reply(`👢 Kicked **${target.user.tag}** — ${reason}`);
}
if (interaction.commandName === 'ban') {
if (!target.bannable) {
return interaction.reply({ content: "I can't ban that member — my role is too low or they're above me.", ephemeral: true });
}
await target.ban({ reason, deleteMessageSeconds: 60 * 60 * 24 });
await interaction.reply(`🔨 Banned **${target.user.tag}** — ${reason}`);
}
if (interaction.commandName === 'timeout') {
if (!target.moderatable) {
return interaction.reply({ content: "I can't time out that member — my role is too low or they're above me.", ephemeral: true });
}
const minutes = interaction.options.getInteger('minutes');
await target.timeout(minutes * 60 * 1000, reason);
await interaction.reply(`⏳ Timed out **${target.user.tag}** for ${minutes} min — ${reason}`);
}
});
client.login(process.env.DISCORD_TOKEN);
Paste it into index.js, save, and restart. After the usual first-time delay, /kick, /ban, and /timeout appear.
The parts that matter
Only a few pieces are new compared with the /ping bot:
.setDefaultMemberPermissions(PermissionFlagsBits.KickMembers)hides the command from anyone who lacks that permission. This is Discord's own gate — a member without Kick Members never even sees/kickin the menu, so you don't have to check permissions in code.interaction.options.getMember('member')returns the guild member the moderator picked (not just aUser), which is what you need to kick or ban. Discord sends this member inside the interaction, so it works without the Server Members intent. It'snullif the person already left — hence the early "not in this server" reply.kickable/bannable/moderatableare the safety checks. Each istrueonly when the bot can act on that member — the bot's highest role is above theirs and the bot has the permission. Checking these first turns a red error in your console into a clean "I can't touch that member" reply.target.timeout(ms, reason)takes milliseconds, which is why the code multiplies minutes by60 * 1000. ThesetMaxValue(40320)caps it at Discord's real limit: 28 days.deleteMessageSecondson the ban wipes the last 24 hours of the banned user's messages. Set it to0to keep their history.- The
reasonpassed tokick/ban/timeoutis what shows up in Server Settings → Audit Log — worth filling in so future-you knows why.
Permissions and intents
This bot needs no privileged intents — GatewayIntentBits.Guilds is enough, and nothing extra to toggle in the Developer Portal. What it does need is permission inside the server, plus the right role position.
| Command | Bot permission it needs |
|---|---|
/kick |
Kick Members |
/ban |
Ban Members |
/timeout |
Moderate Members |
⚠️ Heads up: Discord's role hierarchy is absolute. Your bot can only kick, ban, or time out members whose highest role sits below the bot's highest role — no permission overrides this. Drag the bot's role up in Server Settings → Roles so it's above the members it should manage. That's exactly what the
kickable/bannable/moderatablechecks are protecting you from.
Grant the permissions when you invite the bot (tick them in the OAuth2 URL generator) or by giving the bot's role those permissions later.
Making it yours
- Log actions to a channel. Instead of only replying, build an embed and send it to a mod-log channel so there's a record.
- Add
/unban. Bans are the one action that outlives the member leaving —interaction.guild.bans.remove(userId, reason)reverses one. Use a String option for the user ID (an ex-member won't show in a user picker). - Require a reason. Add
.setRequired(true)to thereasonoption if you want to force moderators to explain themselves. - Stop it clashing with your other commands.
application.commands.set()replaces your whole command list, so if you already have commands, add these to the same array rather than callingsettwice.
Troubleshooting
- "Missing Permissions" in the console — the bot's role doesn't have Kick/Ban/Moderate Members, or it isn't high enough in the role list. Fix both in Server Settings → Roles.
- The command replies "I can't touch that member" — that's the hierarchy check doing its job: the target's role is at or above the bot's. Move the bot's role higher.
/timeouterrors on a big number — timeouts max out at 28 days; thesetMaxValue(40320)keeps the input in range, but check you're passing minutes, not seconds.- Commands don't appear — first-time global registration lags a few minutes. Still missing? Re-invite with the
applications.commandsscope. See Bot appears offline.