How to make a Rust name filter plugin (kick on join)
Blocks connections when a player's display name contains a blocked word, looks like advertising, or is shorter than a minimum length. Uses the OnPlayerConnected hook to kick the player with a configurable message.
Rust does not police display names. A player can join with a name containing a URL, a Discord tag, a slur, or a single character, and the only feedback you get is the chat box. The practical fix is to validate the name the moment the connection is accepted and remove the player before they interact with anything.
This plugin runs in OnPlayerConnected, which fires server-side after the player object exists but before normal gameplay starts. It lowercases the name once, then checks three independent rules: length against Minimum Name Length, substrings against Blocked Words, and regular expressions against Blocked Patterns (the defaults cover URLs and invite-style domains). Matching is done on the lowercased name and regexes are compiled once at load, so the hook stays cheap. A matching player gets Kick(Banned Name Kick Message), and admins with the bypass permission are skipped. Every refusal is written to the server console with the Steam ID and the offending name so you can review it later.
The plugin code
using System.Linq;
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using Newtonsoft.Json;
using Oxide.Core;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("Name Filter", "RustPluginGenerator", "1.0.0")]
[Description("Kicks connecting players whose display name is too short, contains blocked words, or looks like advertising.")]
public class NameFilter : RustPlugin
{
private const string PermBypass = "namefilter.bypass";
private const string PermAdmin = "namefilter.admin";
private PluginConfig config;
private readonly List<Regex> patterns = new List<Regex>();
private class PluginConfig
{
[JsonProperty("Minimum Name Length")]
public int MinimumNameLength = 3;
[JsonProperty("Blocked Words")]
public string[] BlockedWords =
{
"admin",
"owner",
"moderator",
"server",
"vac_ban",
"freevip"
};
[JsonProperty("Blocked Patterns")]
public string[] BlockedPatterns =
{
@"https?://",
@"www\.",
@"discord\.gg",
@"\.com\b",
@"\.gg\b",
@"\b(?:discord|steam)\b.*#\d{4}",
@"(?:discord|steam|skype|telegram)[\.\-_]"
};
[JsonProperty("Kick Message")]
public string KickMessage = "Your display name is not allowed on this server. Please change it and reconnect.";
[JsonProperty("Log Blocked Connections")]
public bool LogBlockedConnections = true;
}
protected override void LoadDefaultConfig() => config = new PluginConfig();
protected override void LoadConfig()
{
base.LoadConfig();
try
{
config = Config.ReadObject<PluginConfig>();
if (config == null) throw new Exception();
}
catch
{
PrintWarning("Configuration file is invalid, using defaults");
LoadDefaultConfig();
}
CompilePatterns();
SaveConfig();
}
protected override void SaveConfig() => Config.WriteObject(config);
protected override void LoadDefaultMessages()
{
lang.RegisterMessages(new Dictionary<string, string>
{
["CheckClean"] = "{0} ({1}) passes the name filter.",
["CheckBlocked"] = "{0} ({1}) is blocked: {2}",
["TargetNotFound"] = "No player found for '{0}'.",
["Reloaded"] = "NameFilter configuration reloaded."
}, this);
}
private void Init()
{
permission.RegisterPermission(PermBypass, this);
permission.RegisterPermission(PermAdmin, this);
}
private void CompilePatterns()
{
patterns.Clear();
if (config.BlockedPatterns == null) return;
foreach (string pattern in config.BlockedPatterns)
{
if (string.IsNullOrEmpty(pattern)) continue;
try
{
patterns.Add(new Regex(pattern, RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant));
}
catch (Exception ex)
{
PrintWarning("Invalid pattern '" + pattern + "': " + ex.Message);
}
}
}
private void OnPlayerConnected(BasePlayer player)
{
if (player == null || player.IsNpc) return;
if (permission.UserHasPermission(player.UserIDString, PermBypass)) return;
string name = player.displayName ?? string.Empty;
string reason;
if (!IsNameBlocked(name, out reason)) return;
if (config.LogBlockedConnections)
Puts("Blocked connection: " + name + " (" + player.UserIDString + ") - " + reason);
player.Kick(config.KickMessage);
}
private bool IsNameBlocked(string name, out string reason)
{
reason = null;
if (name.Length < config.MinimumNameLength)
{
reason = "name shorter than " + config.MinimumNameLength + " characters";
return true;
}
string lower = name.ToLowerInvariant();
if (config.BlockedWords != null)
{
foreach (string word in config.BlockedWords)
{
if (string.IsNullOrEmpty(word)) continue;
if (lower.Contains(word.ToLowerInvariant()))
{
reason = "contains blocked word '" + word + "'";
return true;
}
}
}
foreach (Regex regex in patterns)
{
if (regex.IsMatch(name))
{
reason = "matches pattern '" + regex + "'";
return true;
}
}
return false;
}
private string Msg(string key, string userId = null, params object[] args)
{
return string.Format(lang.GetMessage(key, this, userId), args);
}
[ConsoleCommand("namefilter.check")]
private void CmdCheck(ConsoleSystem.Arg arg)
{
if (!arg.IsAdmin && arg.Player() != null && !permission.UserHasPermission(arg.Player().UserIDString, PermAdmin))
{
arg.ReplyWith("You do not have permission to use this command.");
return;
}
string target = arg.GetString(0, string.Empty);
if (string.IsNullOrEmpty(target))
{
arg.ReplyWith("Usage: namefilter.check <player name or steamid>");
return;
}
var player = BasePlayer.FindAwakeOrSleeping(target);
if (player == null)
{
arg.ReplyWith(Msg("TargetNotFound", null, target));
return;
}
string reason;
if (IsNameBlocked(player.displayName ?? string.Empty, out reason))
arg.ReplyWith(Msg("CheckBlocked", null, player.displayName, player.UserIDString, reason));
else
arg.ReplyWith(Msg("CheckClean", null, player.displayName, player.UserIDString));
}
[ConsoleCommand("namefilter.reload")]
private void CmdReload(ConsoleSystem.Arg arg)
{
if (!arg.IsAdmin && arg.Player() != null && !permission.UserHasPermission(arg.Player().UserIDString, PermAdmin))
{
arg.ReplyWith("You do not have permission to use this command.");
return;
}
LoadConfig();
arg.ReplyWith(Msg("Reloaded"));
}
}
}Hooks used: OnPlayerConnected
Config file
Created automatically on first load at oxide/config/NameFilter.json.
{
"Minimum Name Length": 3,
"Blocked Words": [
"admin",
"owner",
"moderator",
"server",
"vac_ban",
"freevip"
],
"Blocked Patterns": [
"https?://",
"www\\.",
"discord\\.gg",
"\\.com\\b",
"\\.gg\\b",
"\\b(?:discord|steam)\\b.*#\\d{4}",
"(?:discord|steam|skype|telegram)[\\.\\-_]"
],
"Kick Message": "Your display name is not allowed on this server. Please change it and reconnect.",
"Log Blocked Connections": true
}Commands and permissions
| Command | What it does | Permission |
|---|---|---|
namefilter.check <player name or steamid> | Runs the configured name rules against a player's current display name and reports the result to console. | namefilter.admin |
namefilter.reload | Reloads the plugin configuration from oxide/config/NameFilter.json without restarting the plugin. | namefilter.admin |
| Permission | Grants |
|---|---|
namefilter.bypass | Skips the name filter for this player or group. |
namefilter.admin | Allows use of the namefilter.check and namefilter.reload console commands. |
How the code works
Hook the connection and validate early
OnPlayerConnected(BasePlayer player)fires once per accepted connection. Null-check the player, then bail out ifplayer.IsNpcor the player hasnamefilter.bypass. Everything else goes throughIsNameBlocked(string name, out string reason), which returns true for the first failing rule. When it returns true, callplayer.Kick(config.KickMessage)and log the Steam ID, name and reason withPuts. Because the kick happens here, the player never reaches spawn.Length rule
Minimum Name Length(default 3) usesname.Lengthon the raw display name, not the lowercased copy, so trailing whitespace still counts. Set it to 1 to disable the rule in practice, since Rust already forbids empty names. The length check runs first because it is the cheapest test; blocking a two-character name should not spend time on a regex scan.Blocked word list
Blocked Wordsis a string array in the config. At load the plugin lowercases the name once (name.ToLowerInvariant()) and compares withContainsagainst each entry, which is also lowercased when loading. Uppercase staff impersonation is covered without listing both cases. Keep the list focused on unambiguous substrings; short entries likeadwill match legitimate names such asAdam, so use the pattern list for anything that needs context.Advertising and URL patterns
Blocked Patternsholds .NET regex strings. The plugin builds aList<Regex>inLoadConfigwithRegexOptions.IgnoreCase | RegexOptions.Compiled, and a broken expression is caught, warned about withPrintWarningand skipped rather than crashing the load. The defaults catchhttp://,www.,discord.gg,.com/.ggstyle domains and discord tag syntax with a four-digit discriminator. Matching a.ggdomain on the bare suffix means names ending in.ggare blocked too.Kick message and logging
KickMessageis a single string in the config, defaulting to a short explanation with the server restart advice.player.Kick(string)takes the reason shown in the client disconnect dialog, so the text should tell the player what to change. Console output usesPutsonly inside the connect hook, which is rare enough to be safe. Nothing is sent to chat, so a kicked player never sees a chat line.Admin commands and reload
namefilter.checkresolves the target withBasePlayer.FindAwakeOrSleeping, then runs the sameIsNameBlockedpath and prints the reason viaarg.ReplyWith.namefilter.reloadcalls the baseLoadConfigand recompiles the regex list, so you can edit the JSON and apply it without unloading the plugin. Both are console commands gated onnamefilter.admin;arg.Player()can be null under RCON. Player-facing replies go through the language file via theMsghelper.
Install it on your server
Save NameFilter.cs
Keep the file name exactly as the class name. Oxide matches them.Upload it to the plugins folder
Oxide:oxide/plugins/. Carbon:carbon/plugins/. The server compiles and loads it within a few seconds, no restart needed.Edit the config if you want
On first load the default config is written tooxide/config/NameFilter.json(Carbon:carbon/configs/NameFilter.json). After editing, reload it:oxide.reload NameFilter c.reload NameFilter # CarbonGrant the permission
Give it to every player, or swapdefaultfor a group such asvip:oxide.grant group default namefilter.bypass c.grant group default namefilter.bypass # Carbon
Variations
Each one opens the generator with a ready prompt you can tweak.
Whitelist instead of blocklist
Only characters matching a safe set are allowed in names.
Generate this versionRename instead of kick
Offending players get a generated name and stay on the server.
Generate this versionDiscord alert on refusal
Push every refused name to a webhook for moderators.
Generate this version
FAQ
Does OnPlayerConnected fire before or after the player spawns?
It fires after the connection is accepted and the player object exists, but before normal spawning and gameplay. Kicking there removes the player from the server cleanly; they see the disconnect dialog with your KickMessage and are not left as a ghost entity.
Will this catch names that are only whitespace or zero-width characters?
The length rule counts every character, so zero-width characters can pad a name past the minimum. If that matters to you, add a regex such as \p{C} to Blocked Patterns; .NET regex supports Unicode categories, and the compiled list runs on every connection.
Can admins with the bypass permission still join with a bad name?
Yes. namefilter.bypass is checked at the top of the hook, so staff and moderators can keep whatever name they use. The connect is still logged only when a rule would have matched and the player was skipped, which helps you confirm the permission is granting what you expect.
How do I install it on Oxide or Carbon?
Drop NameFilter.cs into oxide/plugins, let the server compile it, then edit oxide/config/NameFilter.json and run namefilter.reload in the console. Carbon loads the same file from its plugins directory; no changes are needed. Grant namefilter.admin to your staff group with oxide.grant.
Does the filter apply to renamed players?
No. Only OnPlayerConnected is handled, so a player who changes their name mid-session is not re-checked. Rust name changes usually require reconnecting, at which point the filter runs again; if you need per-change enforcement, check the name in OnPlayerChat or a periodic timer instead.
Customize it for your server
Change the reward, add a cooldown, restrict it to VIPs. Start from this prompt and edit anything.
Edit the prompt on the next screen. 2 free plugins a month, no card.
More moderation plugins
- AFK kickTracks each connected player's movement and look direction with a repeating timer and kicks anyone who stays idle past a configured limit after aโฆOnServerInitializedOnPlayerConnected
- Chat anti-spamBlocks chat messages that exceed a rate limit or duplicate the player's previous message, and mutes repeat offenders for a configurable time.OnPlayerChat
- Chat muteA timed chat mute plugin: /mute <player> <minutes> [reason] stores a mute with an expiry in a data file, /unmute <player> clears it, andโฆOnServerInitializedOnPlayerChat
Last updated Oct 26, 2026.
Customize player name filter
Customize