How to make a Rust chat word filter plugin (whole-word asterisk masking)
This plugin replaces blocked words in player chat with asterisks using case-insensitive whole-word matching and re-broadcasts the cleaned line under the sender's name. It hooks OnPlayerChat and returns a non-null value to suppress the original message.
Public chat on a Rust server drifts fast without a filter, and the built-in tools give you nothing between ignoring a player and banning them. This plugin intercepts every chat line before it reaches other players, scans it against a configurable word list, and swaps matches for asterisks of the same length.
Matching is case-insensitive and whole-word only, so a blocked word inside a longer word is left alone unless you list the longer word too. The hook OnPlayerChat runs first; the plugin builds the filtered string, suppresses the original line, and re-sends the cleaned text through Server.Broadcast in the [Name] message format. If nothing is masked, the original message passes through untouched. Admins and players with wordfilter.bypass skip filtering. The word list lives in oxide/config/WordFilter.json and is managed with the /filteradd, /filterremove and /filterlist chat commands.
The plugin code
using System.Linq;
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using Newtonsoft.Json;
using Oxide.Core;
using Oxide.Core.Libraries.Covalence;
namespace Oxide.Plugins
{
[Info("WordFilter", "RustPluginGenerator", "1.0.0")]
[Description("Masks blocked words in global chat with asterisks and re-broadcasts the filtered message.")]
public class WordFilter : RustPlugin
{
private const string PermAdmin = "wordfilter.admin";
private const string PermBypass = "wordfilter.bypass";
private PluginConfig config;
private Regex pattern;
private class PluginConfig
{
[JsonProperty("Blocked Words")]
public List<string> BlockedWords = new List<string>
{
"badword1",
"badword2"
};
[JsonProperty("Show Sender Name In Brackets")]
public bool ShowBrackets = 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();
if (config.BlockedWords == null) config.BlockedWords = new List<string>();
}
catch
{
PrintWarning("Configuration file is invalid, using defaults");
LoadDefaultConfig();
}
SaveConfig();
RebuildPattern();
}
protected override void SaveConfig() => Config.WriteObject(config);
protected override void LoadDefaultMessages()
{
lang.RegisterMessages(new Dictionary<string, string>
{
["Added"] = "Word '{0}' was added to the filter.",
["AlreadyPresent"] = "Word '{0}' is already in the filter.",
["Removed"] = "Word '{0}' was removed from the filter.",
["NotFound"] = "Word '{0}' is not in the filter.",
["Usage"] = "Usage: {0}",
["ListHeader"] = "Blocked words ({0}):",
["ListEmpty"] = "The word filter list is empty.",
["NoPermission"] = "You do not have permission to use this command."
}, this);
}
private string Msg(string key, string userId = null, params object[] args)
{
return string.Format(lang.GetMessage(key, this, userId), args);
}
private void Init()
{
permission.RegisterPermission(PermAdmin, this);
permission.RegisterPermission(PermBypass, this);
}
private void OnServerInitialized()
{
RebuildPattern();
}
private void RebuildPattern()
{
if (config == null || config.BlockedWords == null || config.BlockedWords.Count == 0)
{
pattern = null;
return;
}
var pieces = new List<string>();
foreach (var word in config.BlockedWords)
{
if (string.IsNullOrWhiteSpace(word)) continue;
pieces.Add("\\b" + Regex.Escape(word.Trim()) + "\\b");
}
if (pieces.Count == 0)
{
pattern = null;
return;
}
pattern = new Regex(string.Join("|", pieces.ToArray()), RegexOptions.IgnoreCase | RegexOptions.Compiled);
}
private object OnPlayerChat(BasePlayer player, string message, ConVar.Chat.ChatChannel channel)
{
if (player == null || player.IsNpc || string.IsNullOrEmpty(message)) return null;
if (channel != ConVar.Chat.ChatChannel.Global) return null;
if (player.IsAdmin) return null;
if (permission.UserHasPermission(player.UserIDString, PermBypass)) return null;
if (pattern == null) return null;
string filtered = MaskText(message);
if (filtered == message) return null;
string name = config.ShowBrackets ? "[" + player.displayName + "]" : player.displayName;
Server.Broadcast(name + " " + filtered);
return false;
}
private string MaskText(string text)
{
return pattern.Replace(text, m => new string('*', m.Value.Length));
}
[ChatCommand("filteradd")]
private void CmdFilterAdd(BasePlayer player, string command, string[] args)
{
if (!permission.UserHasPermission(player.UserIDString, PermAdmin))
{
SendReply(player, Msg("NoPermission", player.UserIDString));
return;
}
if (args == null || args.Length == 0)
{
SendReply(player, Msg("Usage", player.UserIDString, "/filteradd <word>"));
return;
}
string word = args[0].Trim();
if (word.Length == 0) return;
foreach (var existing in config.BlockedWords)
{
if (string.Equals(existing, word, StringComparison.OrdinalIgnoreCase))
{
SendReply(player, Msg("AlreadyPresent", player.UserIDString, word));
return;
}
}
config.BlockedWords.Add(word);
SaveConfig();
RebuildPattern();
SendReply(player, Msg("Added", player.UserIDString, word));
}
[ChatCommand("filterremove")]
private void CmdFilterRemove(BasePlayer player, string command, string[] args)
{
if (!permission.UserHasPermission(player.UserIDString, PermAdmin))
{
SendReply(player, Msg("NoPermission", player.UserIDString));
return;
}
if (args == null || args.Length == 0)
{
SendReply(player, Msg("Usage", player.UserIDString, "/filterremove <word>"));
return;
}
string word = args[0].Trim();
int index = config.BlockedWords.FindIndex(w => string.Equals(w, word, StringComparison.OrdinalIgnoreCase));
if (index < 0)
{
SendReply(player, Msg("NotFound", player.UserIDString, word));
return;
}
config.BlockedWords.RemoveAt(index);
SaveConfig();
RebuildPattern();
SendReply(player, Msg("Removed", player.UserIDString, word));
}
[ChatCommand("filterlist")]
private void CmdFilterList(BasePlayer player, string command, string[] args)
{
if (!permission.UserHasPermission(player.UserIDString, PermAdmin))
{
SendReply(player, Msg("NoPermission", player.UserIDString));
return;
}
if (config.BlockedWords == null || config.BlockedWords.Count == 0)
{
SendReply(player, Msg("ListEmpty", player.UserIDString));
return;
}
SendReply(player, Msg("ListHeader", player.UserIDString, config.BlockedWords.Count));
SendReply(player, string.Join(", ", config.BlockedWords.ToArray()));
}
}
}Hooks used: OnServerInitialized, OnPlayerChat
Config file
Created automatically on first load at oxide/config/WordFilter.json.
{
"Blocked Words": [
"badword1",
"badword2"
],
"Show Sender Name In Brackets": true
}Commands and permissions
| Command | What it does | Permission |
|---|---|---|
/filteradd <word> | Adds a word to the blocked list and saves the config. | wordfilter.admin |
/filterremove <word> | Removes a word from the blocked list and saves the config. | wordfilter.admin |
/filterlist | Prints the current blocked word list to chat. | wordfilter.admin |
| Permission | Grants |
|---|---|
wordfilter.admin | Allows adding, removing and listing blocked words with the chat commands. |
wordfilter.bypass | Chat from players with this permission is never filtered. |
How the code works
Intercept chat with OnPlayerChat
The
OnPlayerChat(BasePlayer player, string message, Chat.ChatChannel channel)hook runs for every chat line. Null-checkplayerand returnnullearly to keep default behavior for team, clan or server channels you do not want to touch — this code only filtersChat.ChatChannel.Global. When a line needs masking, it returnsfalse, which tells Oxide the original message was handled and should not be broadcast.Build the case-insensitive whole-word regex
RebuildPattern()compiles oneRegexfrom theBlocked Wordslist withRegex.Escapeon each entry,\bword boundaries, andRegexOptions.IgnoreCase | RegexOptions.Compiled.\bonly matches at the edge of alphanumeric runs, so a blocked word inside a longer word is skipped. The pattern is rebuilt on load inLoadConfig, onOnServerInitialized, and after every add or remove command, so it never goes stale.Filter and re-broadcast
The
OnPlayerChathook callsMaskText, which runspattern.Replacewith a callback that returns a string of asterisks the same length as the match. If the result equals the original, the hook returnsnulland the untouched message is sent normally. Otherwise the plugin callsServer.Broadcastwith the text prefixed by the player'sdisplayName(bracketed whenShow Sender Name In Bracketsis true), then returnsfalse.Store the word list in config
PluginConfigholds aList<string> Blocked Wordsand abool Show Sender Name In Brackets, both with[JsonProperty].LoadConfigfalls back to defaults and prints a warning if the file is unreadable. Thefilteraddandfilterremovecommands mutate the list, callSaveConfig(), thenRebuildPattern(), so a change takes effect on the next chat line without a restart. Entries are compared withStringComparison.OrdinalIgnoreCaseto prevent duplicates.Permission gates and bypass
Initregisterswordfilter.adminandwordfilter.bypass.OnPlayerChatreturnsnullfor staff withplayer.IsAdminand for players holdingwordfilter.bypass, so their lines are never scanned. Each chat command checkswordfilter.adminwithpermission.UserHasPermissionand replies withMsgoutput through the language file, so every message can be translated without editing C#.Skip non-chat traffic
OnPlayerChatreturnsnullfor NPC players, empty messages, and any channel other thanChat.ChatChannel.Global. It also returnsnullwhenpatternis null, which happens when the word list is empty. This keeps team and other channels working as normal and stops the filter from interfering with plugins that already format chat.
Install it on your server
Save WordFilter.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/WordFilter.json(Carbon:carbon/configs/WordFilter.json). After editing, reload it:oxide.reload WordFilter c.reload WordFilter # CarbonGrant the permission
Give it to every player, or swapdefaultfor a group such asvip:oxide.grant group default wordfilter.admin c.grant group default wordfilter.admin # Carbon
Variations
Each one opens the generator with a ready prompt you can tweak.
Mute on triple offence
Adds an automatic timed mute when a player triggers the filter several times in a short window.
Generate this versionNotify staff privately
Sends a private chat line to all online admins each time a word is masked, showing the original and filtered text.
Generate this versionFilter team and clan chat too
Broadcasts the masked line to the player's team members only when the channel is Team.
Generate this version
FAQ
Does this filter partial matches?
No. The regex uses \b word boundaries, so a blocked word inside a longer word is not replaced. To block a substring, list the exact longer form as its own entry. This avoids false positives like a blocked three-letter word appearing inside a normal word.
Why does the filtered message come from the server instead of the player?
When OnPlayerChat returns false, Oxide drops the original line. The plugin then calls Server.Broadcast with the player's displayName so the masked text is still visible to everyone in the same format.
How do I reload the word list without restarting the server?
Use /filteradd <word> and /filterremove <word>, which save the config and rebuild the regex immediately. You can also edit oxide/config/WordFilter.json and reload the plugin. In both cases the new list applies to the next chat line.
Will this filter other plugins' chat?
No. It only sees lines that go through Rust's OnPlayerChat hook. Messages sent directly with Server.Broadcast, PrintToChat or CUI are not affected. If a plugin prints chat on its own, that output bypasses this filter.
How do I install it on Oxide or Carbon?
Drop WordFilter.cs into oxide/plugins on Oxide or into the Carbon plugins folder, then load the plugin. The config is generated automatically at oxide/config/WordFilter.json with two placeholder entries — replace them with your own words before going live.
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 chat word filter
Customize