Skip to content
RustPluginGenerator

How to make a Rust chat mute plugin (timed, persistent)

A timed chat mute plugin: `/mute <player> <minutes> [reason]` stores a mute with an expiry in a data file, `/unmute <player>` clears it, and `OnPlayerChat` blocks global chat from muted players while team and local chat keep working.

Rust has no built-in mute. Admins who need to stop a spammer either kick them or install a large moderation suite. This page shows a small, self-contained alternative: one plugin that stores mutes in oxide/data/ChatMute.json with an absolute expiry timestamp, so a restart never loses a mute and an expired mute never needs manual cleanup.

The interesting part is deciding which chat channels to block. Blocking every channel breaks team coordination and private messages, so the plugin only cancels Chat.ChatChannel.Global and lets Team and Local through. Every muted attempt is rate-limited to one warning per ten seconds so a spammer cannot flood the server by talking faster.

The commands are designed for console and RCON use as well as chat, so a moderator can mute from the server console with /mute syntax or the chatmute.mute console command without being in game.

The plugin code

ChatMute.cs
using System.Linq;
using System;
using System.Collections.Generic;
using Facepunch.Math;
using Newtonsoft.Json;
using Oxide.Core;
using Oxide.Core.Libraries.Covalence;
using UnityEngine;

namespace Oxide.Plugins
{
    [Info("Chat Mute", "RustPluginGenerator", "1.0.0")]
    [Description("Timed chat mutes that block global chat, persist across restarts and expire automatically.")]
    public class ChatMute : RustPlugin
    {
        private const string PermMute = "chatmute.mute";

        private PluginConfig config;
        private Dictionary<ulong, MuteEntry> mutes = new Dictionary<ulong, MuteEntry>();
        private readonly Dictionary<ulong, float> lastWarning = new Dictionary<ulong, float>();
        private Timer sweepTimer;

        private class MuteEntry
        {
            [JsonProperty("Steam ID")] public ulong SteamId;
            [JsonProperty("Display name")] public string Name;
            [JsonProperty("Expires (unix)")] public int Expires;
            [JsonProperty("Reason")] public string Reason;
            [JsonProperty("Created by")] public string CreatedBy;

            public MuteEntry() { }

            public MuteEntry(ulong steamId, string name, int expires, string reason, string createdBy)
            {
                SteamId = steamId;
                Name = name;
                Expires = expires;
                Reason = reason;
                CreatedBy = createdBy;
            }
        }

        private class PluginConfig
        {
            [JsonProperty("Blocked channels")]
            public List<string> BlockedChannels = new List<string> { "Global" };

            [JsonProperty("Warning cooldown seconds")]
            public float WarningCooldownSeconds = 10f;
        }

        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.BlockedChannels == null) config.BlockedChannels = new List<string>();
            }
            catch
            {
                PrintWarning("Configuration file is invalid, using defaults");
                LoadDefaultConfig();
            }
            SaveConfig();
        }

        protected override void SaveConfig() => Config.WriteObject(config);

        protected override void LoadDefaultMessages()
        {
            lang.RegisterMessages(new Dictionary<string, string>
            {
                ["Mute.Success"] = "Muted {0} ({1}) for {2} minute(s).",
                ["Mute.Usage"] = "Usage: /mute <player> <minutes> [reason]",
                ["Unmute.Success"] = "Unmuted {0} ({1}).",
                ["Unmute.NotFound"] = "{0} is not muted.",
                ["Unmute.Usage"] = "Usage: /unmute <player>",
                ["List.Header"] = "Active mutes: {0}",
                ["List.Entry"] = "{0} ({1}) - {2} minute(s) left - {3}",
                ["List.Empty"] = "There are no active mutes.",
                ["Target.Muted"] = "You are muted for another {0} minute(s). Reason: {1}",
                ["Target.MutedNoReason"] = "You are muted for another {0} minute(s).",
                ["Error.NoPermission"] = "You do not have permission to do that.",
                ["Error.PlayerNotFound"] = "No player found matching '{0}'.",
                ["Error.Ambiguous"] = "Multiple players match '{0}', use a Steam ID.",
                ["Error.SelfTarget"] = "Use a Steam ID, not your own name.",
                ["Error.Input"] = "The minutes value must be a whole number.",
                ["Broadcast.Muted"] = "{0} was muted for {1} minute(s) by {2}.",
                ["Broadcast.Unmuted"] = "{0} was unmuted by {1}."
            }, this);
        }

        private string Msg(string key, string userId = null, params object[] args) =>
            string.Format(lang.GetMessage(key, this, userId), args);

        private void Init()
        {
            permission.RegisterPermission(PermMute, this);
        }

        private void OnServerInitialized()
        {
            LoadData();
            SweepExpired();
            sweepTimer = timer.Every(60f, SweepExpired);
            Puts($"Loaded {mutes.Count} active mute(s).");
        }

        private void Unload()
        {
            sweepTimer?.Destroy();
            SaveData();
        }

        private void LoadData()
        {
            mutes = Interface.Oxide.DataFileSystem.ReadObject<Dictionary<ulong, MuteEntry>>(Name)
                    ?? new Dictionary<ulong, MuteEntry>();
        }

        private void SaveData()
        {
            Interface.Oxide.DataFileSystem.WriteObject(Name, mutes);
        }

        private void SweepExpired()
        {
            if (mutes.Count == 0) return;
            int now = Epoch.Current;
            List<ulong> expired = null;
            foreach (var kvp in mutes)
            {
                if (kvp.Value == null || kvp.Value.Expires <= now)
                {
                    if (expired == null) expired = new List<ulong>();
                    expired.Add(kvp.Key);
                }
            }
            if (expired == null) return;
            foreach (ulong id in expired)
            {
                mutes.Remove(id);
                lastWarning.Remove(id);
            }
            SaveData();
        }

        private bool IsMuted(ulong userId)
        {
            if (!mutes.TryGetValue(userId, out MuteEntry entry) || entry == null) return false;
            if (entry.Expires <= Epoch.Current)
            {
                mutes.Remove(userId);
                lastWarning.Remove(userId);
                SaveData();
                return false;
            }
            return true;
        }

        private bool ChannelBlocked(ConVar.Chat.ChatChannel channel)
        {
            return config.BlockedChannels.Contains(channel.ToString());
        }

        private object OnPlayerChat(BasePlayer player, string message, ConVar.Chat.ChatChannel channel)
        {
            if (player == null || player.IsNpc) return null;
            if (!ChannelBlocked(channel)) return null;
            if (!IsMuted(player.userID.Get())) return null;

            MuteEntry entry = mutes[player.userID.Get()];
            float now = Time.realtimeSinceStartup;
            if (lastWarning.TryGetValue(player.userID.Get(), out float last) &&
                now - last < config.WarningCooldownSeconds) return false;
            lastWarning[player.userID.Get()] = now;

            int minutesLeft = Mathf.Max(1, (entry.Expires - Epoch.Current) / 60);
            player.ChatMessage(string.IsNullOrEmpty(entry.Reason)
                ? Msg("Target.MutedNoReason", player.UserIDString, minutesLeft)
                : Msg("Target.Muted", player.UserIDString, minutesLeft, entry.Reason));
            return false;
        }

        [ChatCommand("mute")]
        private void CmdMute(BasePlayer player, string command, string[] args)
        {
            if (player != null && !permission.UserHasPermission(player.UserIDString, PermMute))
            {
                SendReply(player, Msg("Error.NoPermission", player.UserIDString));
                return;
            }
            HandleMute(player, args, player?.displayName ?? "Console");
        }

        [ConsoleCommand("chatmute.mute")]
        private void CcmdMute(ConsoleSystem.Arg arg)
        {
            BasePlayer caller = arg.Player();
            if (caller != null && !permission.UserHasPermission(caller.UserIDString, PermMute))
            {
                arg.ReplyWith(Msg("Error.NoPermission", caller.UserIDString));
                return;
            }
            string[] args = arg.Args != null ? arg.Args.Select(x => x.ToString()).ToArray() : new string[0];
            string actor = caller?.displayName ?? "Console";
            string result = HandleMute(caller, args, actor);
            if (caller == null) arg.ReplyWith(result);
        }

        private string HandleMute(BasePlayer caller, string[] args, string actor)
        {
            if (args == null || args.Length < 2)
            {
                if (caller != null) SendReply(caller, Msg("Mute.Usage", caller.UserIDString));
                return Msg("Mute.Usage");
            }
            string targetArg = args[0];
            if (!int.TryParse(args[1], out int minutes) || minutes < 0)
            {
                if (caller != null) SendReply(caller, Msg("Error.Input", caller.UserIDString));
                return Msg("Error.Input");
            }
            string reason = args.Length > 2 ? string.Join(" ", args, 2, args.Length - 2) : string.Empty;

            ulong targetId;
            string targetName;
            string error = Resolve(targetArg, caller, out targetId, out targetName);
            if (error != null)
            {
                if (caller != null) SendReply(caller, error);
                return error;
            }

            if (minutes == 0)
            {
                mutes.Remove(targetId);
                lastWarning.Remove(targetId);
                SaveData();
                string done = Msg("Unmute.Success", caller?.UserIDString, targetName, targetId);
                if (caller != null) SendReply(caller, done);
                Server.Broadcast(Msg("Broadcast.Unmuted", null, targetName, actor));
                return done;
            }

            int expires = Epoch.Current + minutes * 60;
            mutes[targetId] = new MuteEntry(targetId, targetName, expires, reason, actor);
            SaveData();

            string text = Msg("Mute.Success", caller?.UserIDString, targetName, targetId, minutes);
            if (caller != null) SendReply(caller, text);
            Server.Broadcast(Msg("Broadcast.Muted", null, targetName, minutes, actor));

            BasePlayer target = BasePlayer.FindByID(targetId);
            if (target != null && target.IsConnected)
                target.ChatMessage(string.IsNullOrEmpty(reason)
                    ? Msg("Target.MutedNoReason", target.UserIDString, minutes)
                    : Msg("Target.Muted", target.UserIDString, minutes, reason));
            return text;
        }

        [ChatCommand("unmute")]
        private void CmdUnmute(BasePlayer player, string command, string[] args)
        {
            if (player != null && !permission.UserHasPermission(player.UserIDString, PermMute))
            {
                SendReply(player, Msg("Error.NoPermission", player.UserIDString));
                return;
            }
            string[] a = args ?? new string[0];
            string actor = player?.displayName ?? "Console";
            string result = HandleUnmute(player, a, actor);
            if (player == null) Puts(result);
        }

        [ConsoleCommand("chatmute.unmute")]
        private void CcmdUnmute(ConsoleSystem.Arg arg)
        {
            BasePlayer caller = arg.Player();
            if (caller != null && !permission.UserHasPermission(caller.UserIDString, PermMute))
            {
                arg.ReplyWith(Msg("Error.NoPermission", caller.UserIDString));
                return;
            }
            string[] args = arg.Args != null ? arg.Args.Select(x => x.ToString()).ToArray() : new string[0];
            string actor = caller?.displayName ?? "Console";
            string result = HandleUnmute(caller, args, actor);
            if (caller == null) arg.ReplyWith(result);
        }

        private string HandleUnmute(BasePlayer caller, string[] args, string actor)
        {
            if (args.Length < 1)
            {
                if (caller != null) SendReply(caller, Msg("Unmute.Usage", caller.UserIDString));
                return Msg("Unmute.Usage");
            }
            ulong targetId;
            string targetName;
            string error = Resolve(args[0], caller, out targetId, out targetName);
            if (error != null)
            {
                if (caller != null) SendReply(caller, error);
                return error;
            }
            if (!mutes.Remove(targetId))
            {
                string notMuted = Msg("Unmute.NotFound", caller?.UserIDString, targetName);
                if (caller != null) SendReply(caller, notMuted);
                return notMuted;
            }
            lastWarning.Remove(targetId);
            SaveData();
            string done = Msg("Unmute.Success", caller?.UserIDString, targetName, targetId);
            if (caller != null) SendReply(caller, done);
            Server.Broadcast(Msg("Broadcast.Unmuted", null, targetName, actor));
            return done;
        }

        [ChatCommand("mutelist")]
        private void CmdMuteList(BasePlayer player, string command, string[] args)
        {
            if (!permission.UserHasPermission(player.UserIDString, PermMute))
            {
                SendReply(player, Msg("Error.NoPermission", player.UserIDString));
                return;
            }
            SweepExpired();
            if (mutes.Count == 0)
            {
                SendReply(player, Msg("List.Empty", player.UserIDString));
                return;
            }
            SendReply(player, Msg("List.Header", player.UserIDString, mutes.Count));
            int now = Epoch.Current;
            foreach (var kvp in mutes)
            {
                MuteEntry entry = kvp.Value;
                int minutesLeft = Mathf.Max(1, (entry.Expires - now) / 60);
                string reason = string.IsNullOrEmpty(entry.Reason) ? "-" : entry.Reason;
                SendReply(player, Msg("List.Entry", player.UserIDString, entry.Name, entry.SteamId, minutesLeft, reason));
            }
        }

        private string Resolve(string input, BasePlayer caller, out ulong userId, out string name)
        {
            userId = 0;
            name = input;
            if (string.IsNullOrEmpty(input)) return Msg("Error.PlayerNotFound", caller?.UserIDString, input);

            if (ulong.TryParse(input, out ulong parsed))
            {
                BasePlayer byId = BasePlayer.FindAwakeOrSleeping(input);
                userId = parsed;
                name = byId != null ? byId.displayName : parsed.ToString();
                return null;
            }

            BasePlayer exact = BasePlayer.FindAwakeOrSleeping(input);
            if (exact != null && caller != null && exact.userID == caller.userID) return Msg("Error.SelfTarget", caller.UserIDString);

            BasePlayer match = null;
            int count = 0;
            foreach (BasePlayer p in BasePlayer.allPlayerList)
            {
                if (p == null || p.IsNpc) continue;
                if (exact != null && p.userID == exact.userID) { match = p; count = 1; break; }
                if (p.displayName != null && p.displayName.IndexOf(input, StringComparison.OrdinalIgnoreCase) >= 0)
                {
                    match = p;
                    count++;
                }
            }
            if (count == 0) return Msg("Error.PlayerNotFound", caller?.UserIDString, input);
            if (count > 1) return Msg("Error.Ambiguous", caller?.UserIDString, input);

            userId = match.userID.Get();
            name = match.displayName;
            return null;
        }
    }
}

Hooks used: OnServerInitialized, OnPlayerChat

Config file

Created automatically on first load at oxide/config/ChatMute.json.

ChatMute.json
{
  "Blocked channels": [
    "Global"
  ],
  "Warning cooldown seconds": 10
}

Commands and permissions

Chat and console commands
CommandWhat it doesPermission
/mute <player> <minutes> [reason]Mute a player for the given number of minutes. Minutes of 0 clears the mute. The player argument accepts a display name, a Steam ID or a partial name match.chatmute.mute
/unmute <player>Remove an active mute from a player immediately.chatmute.mute
/mutelistLists active mutes with remaining time and reason.chatmute.mute
chatmute.mute <player> <minutes> [reason]Console/RCON version of /mute.chatmute.mute
chatmute.unmute <player>Console/RCON version of /unmute.chatmute.mute
Permissions
PermissionGrants
chatmute.muteGrants /mute, /unmute, /mutelist and the chatmute.mute / chatmute.unmute console commands.

How the code works

  1. Blocking only global chat

    OnPlayerChat(BasePlayer player, string message, Chat.ChatChannel channel) is called before the message is broadcast. Returning a non-null object cancels the send, so the plugin returns false when the sender has an active mute and the channel is Chat.ChatChannel.Global. Team and Local messages return null and go through unchanged. The hook is never called for commands that start with / because Oxide consumes those first, which means a muted player can still use /mute-style plugins they have permission for.

  2. Storing mutes with an expiry

    Each mute is a small class with SteamId, Name, Expires (unix seconds from Facepunch.Math.Epoch.Current), Reason and CreatedBy. They live in a Dictionary<ulong, MuteEntry> serialized to oxide/data/ChatMute.json through Interface.Oxide.DataFileSystem. Because the expiry is stored as an absolute time rather than a countdown, a server restart or a long downtime does not extend or shorten a mute.

  3. Expiring automatically

    IsMuted(player.userID) drops the entry and returns false when Expires <= Epoch.Current. A timer.Every(60f, ...) sweep also removes expired entries from the dictionary and saves only when something was removed, which keeps the data file clean without hammering the disk. The timer handle is stored in a field and destroyed in Unload.

  4. Resolving the player argument

    The first argument can be a Steam ID (any string where ulong.TryParse succeeds), an exact display name, or a case-insensitive partial match. BasePlayer.FindAwakeOrSleeping covers online and sleeping players by name or ID; if that fails the plugin scans BasePlayer.allPlayerList for a partial match and rejects the command when more than one player matches, so a moderator does not mute the wrong person.

  5. Commands and permissions

    All four command entry points check permission.UserHasPermission(user.UserIDString, PermMute), registered in Init. Console commands use arg.Player() which is null under RCON, so the reply goes through arg.ReplyWith while chat commands use SendReply. When a player is muted, everyone online with the permission gets a chat notice, and the muted player is told the remaining time and reason once.

  6. Messages and config

    Every player-facing string lives in LoadDefaultMessages and is read through the Msg helper, so admins can translate or reword it with a language file. PluginConfig only holds BlockedChannels (default ["Global"]) and WarningCooldownSeconds (default 10). LoadConfig falls back to defaults on a malformed file and prints a warning instead of failing.

  7. Unload behavior

    Unload destroys the sweep timer and writes the data file. No CUI is created, so there is nothing to clean up per player. Mutes that were still active stay in the file and are read back by LoadConfig-style loading in OnServerInitialized, which also logs the number of active mutes so the admin can see the state after a restart.

Install it on your server

  1. Save ChatMute.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/ChatMute.json (Carbon: carbon/configs/ChatMute.json). After editing, reload it:oxide.reload ChatMute c.reload ChatMute # Carbon
  4. Grant the permission

    Give it to every player, or swap default for a group such as vip:oxide.grant group default chatmute.mute c.grant group default chatmute.mute # Carbon

Variations

Each one opens the generator with a ready prompt you can tweak.

  • Mute by IP for ban evasion

    Adds a separate mute list keyed by connection IP so a reconnecting alt account is still muted.

    Generate this version
  • Word filter instead of full mute

    Replaces explicit words in global chat with asterisks instead of cancelling the message.

    Generate this version
  • Escalating mute lengths

    Tracks mute counts per player and multiplies the duration for repeat offenders.

    Generate this version

FAQ

Do mutes survive a server restart?

Yes. Mutes are written to oxide/data/ChatMute.json with an absolute expiry time from Epoch.Current. After a restart the plugin reads the file and a mute only ends when that time has passed, regardless of how often the server restarts.

Why can a muted player still talk in team chat?

OnPlayerChat reports the channel, and the plugin only cancels Chat.ChatChannel.Global by default. Change BlockedChannels in the config to ["Global", "Team", "Local"] if you want a full mute instead of a public-chat mute.

Can I mute someone who is offline?

Yes. BasePlayer.FindAwakeOrSleeping finds sleeping players by name or Steam ID, and a raw numeric ID also works for people who are not on the server at all, since the mute is keyed by the 64-bit Steam ID.

How do I run this on Oxide or Carbon?

Drop ChatMute.cs into oxide/plugins, wait for compilation, then grant the permission with oxide.grant group admin chatmute.mute. Carbon loads the same file from its plugins folder and reads the same data and config paths.

Does the plugin spam the muted player?

No. A muted player receives one warning per WarningCooldownSeconds window (10 by default) no matter how many messages they send, and the message is sent through the language layer so you can reword it.

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 chat mute plugin with /mute <player> <minutes> [reason], /unmute <player> and /mutelist behind chatmute.mute, persisting expiry-stamped mutes to oxide/data/ChatMute.json and blocking only global chat.
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 mute

Customize