Skip to content
RustPluginGenerator

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

WordFilter.cs
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.

WordFilter.json
{
  "Blocked Words": [
    "badword1",
    "badword2"
  ],
  "Show Sender Name In Brackets": true
}

Commands and permissions

Chat and console commands
CommandWhat it doesPermission
/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
/filterlistPrints the current blocked word list to chat.wordfilter.admin
Permissions
PermissionGrants
wordfilter.adminAllows adding, removing and listing blocked words with the chat commands.
wordfilter.bypassChat from players with this permission is never filtered.

How the code works

  1. Intercept chat with OnPlayerChat

    The OnPlayerChat(BasePlayer player, string message, Chat.ChatChannel channel) hook runs for every chat line. Null-check player and return null early to keep default behavior for team, clan or server channels you do not want to touch — this code only filters Chat.ChatChannel.Global. When a line needs masking, it returns false, which tells Oxide the original message was handled and should not be broadcast.

  2. Build the case-insensitive whole-word regex

    RebuildPattern() compiles one Regex from the Blocked Words list with Regex.Escape on each entry, \b word boundaries, and RegexOptions.IgnoreCase | RegexOptions.Compiled. \b only matches at the edge of alphanumeric runs, so a blocked word inside a longer word is skipped. The pattern is rebuilt on load in LoadConfig, on OnServerInitialized, and after every add or remove command, so it never goes stale.

  3. Filter and re-broadcast

    The OnPlayerChat hook calls MaskText, which runs pattern.Replace with a callback that returns a string of asterisks the same length as the match. If the result equals the original, the hook returns null and the untouched message is sent normally. Otherwise the plugin calls Server.Broadcast with the text prefixed by the player's displayName (bracketed when Show Sender Name In Brackets is true), then returns false.

  4. Store the word list in config

    PluginConfig holds a List<string> Blocked Words and a bool Show Sender Name In Brackets, both with [JsonProperty]. LoadConfig falls back to defaults and prints a warning if the file is unreadable. The filteradd and filterremove commands mutate the list, call SaveConfig(), then RebuildPattern(), so a change takes effect on the next chat line without a restart. Entries are compared with StringComparison.OrdinalIgnoreCase to prevent duplicates.

  5. Permission gates and bypass

    Init registers wordfilter.admin and wordfilter.bypass. OnPlayerChat returns null for staff with player.IsAdmin and for players holding wordfilter.bypass, so their lines are never scanned. Each chat command checks wordfilter.admin with permission.UserHasPermission and replies with Msg output through the language file, so every message can be translated without editing C#.

  6. Skip non-chat traffic

    OnPlayerChat returns null for NPC players, empty messages, and any channel other than Chat.ChatChannel.Global. It also returns null when pattern is 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

  1. Save WordFilter.cs

    Keep the file name exactly as the class name. Oxide matches them.
  2. 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.
  3. Edit the config if you want

    On first load the default config is written to oxide/config/WordFilter.json (Carbon: carbon/configs/WordFilter.json). After editing, reload it:oxide.reload WordFilter c.reload WordFilter # Carbon
  4. Grant the permission

    Give it to every player, or swap default for a group such as vip: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 version
  • Notify 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 version
  • Filter 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.

Prompt: Create a Rust Oxide plugin called WordFilter that masks blocked words in global chat with asterisks, case-insensitive and whole-word only, with the word list in the config and admin chat commands to add, remove and list words.
Customize for my server

Edit the prompt on the next screen. 2 free plugins a month, no card.

Last updated Oct 26, 2026.

Customize chat word filter

Customize