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
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.
{
"Blocked channels": [
"Global"
],
"Warning cooldown seconds": 10
}Commands and permissions
| Command | What it does | Permission |
|---|---|---|
/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 |
/mutelist | Lists 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 |
| Permission | Grants |
|---|---|
chatmute.mute | Grants /mute, /unmute, /mutelist and the chatmute.mute / chatmute.unmute console commands. |
How the code works
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 returnsfalsewhen the sender has an active mute and the channel isChat.ChatChannel.Global. Team and Local messages returnnulland 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.Storing mutes with an expiry
Each mute is a small class with
SteamId,Name,Expires(unix seconds fromFacepunch.Math.Epoch.Current),ReasonandCreatedBy. They live in aDictionary<ulong, MuteEntry>serialized tooxide/data/ChatMute.jsonthroughInterface.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.Expiring automatically
IsMuted(player.userID)drops the entry and returns false whenExpires <= Epoch.Current. Atimer.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 inUnload.Resolving the player argument
The first argument can be a Steam ID (any string where
ulong.TryParsesucceeds), an exact display name, or a case-insensitive partial match.BasePlayer.FindAwakeOrSleepingcovers online and sleeping players by name or ID; if that fails the plugin scansBasePlayer.allPlayerListfor a partial match and rejects the command when more than one player matches, so a moderator does not mute the wrong person.Commands and permissions
All four command entry points check
permission.UserHasPermission(user.UserIDString, PermMute), registered inInit. Console commands usearg.Player()which is null under RCON, so the reply goes througharg.ReplyWithwhile chat commands useSendReply. 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.Messages and config
Every player-facing string lives in
LoadDefaultMessagesand is read through theMsghelper, so admins can translate or reword it with a language file.PluginConfigonly holdsBlockedChannels(default["Global"]) andWarningCooldownSeconds(default 10).LoadConfigfalls back to defaults on a malformed file and prints a warning instead of failing.Unload behavior
Unloaddestroys 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 byLoadConfig-style loading inOnServerInitialized, which also logs the number of active mutes so the admin can see the state after a restart.
Install it on your server
Save ChatMute.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/ChatMute.json(Carbon:carbon/configs/ChatMute.json). After editing, reload it:oxide.reload ChatMute c.reload ChatMute # CarbonGrant the permission
Give it to every player, or swapdefaultfor a group such asvip: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 versionWord filter instead of full mute
Replaces explicit words in global chat with asterisks instead of cancelling the message.
Generate this versionEscalating 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.
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 word filterThis plugin replaces blocked words in player chat with asterisks using case-insensitive whole-word matching and re-broadcasts the cleaned line underโฆOnServerInitializedOnPlayerChat
Last updated Oct 26, 2026.
Customize chat mute
Customize