How to make a Rust team tags in chat plugin (Oxide/Carbon)
Lets the leader of a Rust team pick a 2-5 character tag with /tag, stores it per team in oxide/data, and prefixes every team member's chat line with [TAG] inside an OnPlayerChat hook that formats the message.
Rust teams already share a roster, but chat carries no sign that two players belong to the same group, so text callouts in a fight are impossible to attribute. This plugin gives every team a short tag managed by its leader and shows that tag in front of each member's chat line.
The leader types /tag WOLF (or any 2-5 letters/digits) to claim a tag. The tag is stored per team and persists across restarts via the data file; OnPlayerChat returns the formatted string, so the server shows [WOLF] PlayerName: message. Spaces, punctuation and anything outside A-Z, a-z and 0-9 are rejected. Members who leave or are kicked lose the tag display automatically because lookup is by player.currentTeam.
The plugin code
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("Team Chat Tags", "RustPluginGenerator", "1.0.0")]
[Description("Lets team leaders set a short tag that is shown before team members' chat messages.")]
public class TeamChatTags : RustPlugin
{
private const string PermUse = "teamchattags.use";
private const string DataFileName = "TeamChatTags";
private PluginConfig config;
private Dictionary<string, string> tags = new Dictionary<string, string>();
private class PluginConfig
{
[JsonProperty("Tag format ({0} tag, {1} name, {2} message)")]
public string TagFormat { get; set; } = "[{0}] {1}: {2}";
[JsonProperty("Minimum tag length")]
public int MinLength { get; set; } = 2;
[JsonProperty("Maximum tag length")]
public int MaxLength { get; set; } = 5;
}
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();
}
SaveConfig();
}
protected override void SaveConfig() => Config.WriteObject(config);
protected override void LoadDefaultMessages()
{
lang.RegisterMessages(new Dictionary<string, string>
{
["Set.Success"] = "Team tag set to [{0}].",
["Set.Cleared"] = "Your team tag has been removed.",
["Set.Invalid"] = "Tags must be {0}-{1} characters, letters and numbers only.",
["Set.NotLeader"] = "Only the team leader can change the team tag.",
["Set.NoTeam"] = "You are not in a team.",
["Set.None"] = "Your team has no tag. Use /tag <TAG> to set one.",
["Set.Current"] = "Your team tag is [{0}].",
["Set.InUse"] = "That tag is already used by another team.",
["NoPermission"] = "You do not have permission to use this command."
}, 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(PermUse, this);
}
private void OnServerInitialized()
{
tags = Interface.Oxide.DataFileSystem.ReadObject<Dictionary<string, string>>(DataFileName)
?? new Dictionary<string, string>();
}
private void Unload()
{
Interface.Oxide.DataFileSystem.WriteObject(DataFileName, tags);
}
private object OnPlayerChat(BasePlayer player, string message, ConVar.Chat.ChatChannel channel)
{
if (player == null || string.IsNullOrEmpty(message)) return null;
if (player.currentTeam == 0) return null;
string tag;
if (!tags.TryGetValue(player.currentTeam.ToString(), out tag) || string.IsNullOrEmpty(tag))
return null;
return string.Format(config.TagFormat, tag, player.displayName, message);
}
[ChatCommand("tag")]
private void CmdTag(BasePlayer player, string command, string[] args)
{
if (player == null) return;
if (!permission.UserHasPermission(player.UserIDString, PermUse))
{
SendReply(player, Msg("NoPermission", player.UserIDString));
return;
}
if (player.currentTeam == 0)
{
SendReply(player, Msg("Set.NoTeam", player.UserIDString));
return;
}
var team = RelationshipManager.ServerInstance.FindTeam(player.currentTeam);
if (team == null)
{
SendReply(player, Msg("Set.NoTeam", player.UserIDString));
return;
}
string teamId = player.currentTeam.ToString();
if (args == null || args.Length == 0)
{
string current;
SendReply(player, tags.TryGetValue(teamId, out current)
? Msg("Set.Current", player.UserIDString, current)
: Msg("Set.None", player.UserIDString));
return;
}
if (team.teamLeader != player.userID.Get())
{
SendReply(player, Msg("Set.NotLeader", player.UserIDString));
return;
}
string candidate = args[0].Trim();
if (!IsValidTag(candidate))
{
SendReply(player, Msg("Set.Invalid", player.UserIDString, config.MinLength, config.MaxLength));
return;
}
foreach (var pair in tags)
{
if (pair.Key != teamId && pair.Value.Equals(candidate, StringComparison.OrdinalIgnoreCase))
{
SendReply(player, Msg("Set.InUse", player.UserIDString));
return;
}
}
tags[teamId] = candidate;
SendReply(player, Msg("Set.Success", player.UserIDString, candidate));
}
[ChatCommand("tagclear")]
private void CmdTagClear(BasePlayer player, string command, string[] args)
{
if (player == null) return;
if (!permission.UserHasPermission(player.UserIDString, PermUse))
{
SendReply(player, Msg("NoPermission", player.UserIDString));
return;
}
if (player.currentTeam == 0)
{
SendReply(player, Msg("Set.NoTeam", player.UserIDString));
return;
}
var team = RelationshipManager.ServerInstance.FindTeam(player.currentTeam);
if (team == null)
{
SendReply(player, Msg("Set.NoTeam", player.UserIDString));
return;
}
if (team.teamLeader != player.userID.Get())
{
SendReply(player, Msg("Set.NotLeader", player.UserIDString));
return;
}
tags.Remove(player.currentTeam.ToString());
SendReply(player, Msg("Set.Cleared", player.UserIDString));
}
private bool IsValidTag(string tag)
{
if (string.IsNullOrEmpty(tag)) return false;
if (tag.Length < config.MinLength || tag.Length > config.MaxLength) return false;
for (int i = 0; i < tag.Length; i++)
{
char c = tag[i];
bool ok = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9');
if (!ok) return false;
}
return true;
}
}
}Hooks used: OnServerInitialized, OnPlayerChat
Config file
Created automatically on first load at oxide/config/TeamChatTags.json.
{
"Tag format ({0} tag, {1} name, {2} message)": "[{0}] {1}: {2}",
"Minimum tag length": 2,
"Maximum tag length": 5
}Commands and permissions
| Command | What it does | Permission |
|---|---|---|
/tag <TAG> | Sets or changes your team's chat tag. Team leader only, 2-5 letters or digits. | teamchattags.use |
/tag | Prints the tag currently assigned to your team, or tells you none is set. | teamchattags.use |
/tagclear | Removes the tag from your team. Team leader only. | teamchattags.use |
| Permission | Grants |
|---|---|
teamchattags.use | Grants access to /tag and /tagclear; without it the commands are refused. |
How the code works
Store tags per team
Tags live in
oxide/data/TeamChatTags.jsonas a dictionary keyed by the team's numeric id (player.currentTeam). The plugin loads it inOnServerInitializedviaInterface.Oxide.DataFileSystem.ReadObject<Dictionary<string, string>>(DataFileName)and writes it back inUnload. A team id of 0 means the player is not in a team, so no lookup is attempted.Validate what the leader types
/tag <TAG>checksargs[0].Trim()character by character and refuses anything outside letters and digits, then enforces theMinLengthandMaxLengthconfig keys (defaults 2 and 5). Tags outside that range are rejected, not truncated. A case-insensitive check across all stored tags rejects a candidate already used by another team.Keep the leader in charge
Before writing a tag the command handler finds the team with
RelationshipManager.ServerInstance.FindTeam(player.currentTeam)and comparesteam.teamLeaderagainstplayer.userID.Get(). Members get a chat refusal from the lang table, not an exception./tagclearuses the same check, so a member cannot blank the tag by accident.Inject the tag into chat
OnPlayerChatreturnsobject; returning non-null replaces the displayed text. The hook looks up the tag forplayer.currentTeamand, when found, returns the string built fromTagFormat(default[{0}] {1}: {2}). When no tag exists it returnsnullso vanilla chat formatting is untouched. Thechannelparameter is not inspected.Config and messages
The
PluginConfigclass exposesTagFormat(the string template with{0}for tag,{1}for player name,{2}for message),MinLengthandMaxLength. Chat replies use theLoadDefaultMessagesdictionary under keys likeSet.Success,Set.NotLeaderandSet.Invalid, read through theMsghelper so they can be translated inoxide/lang.Permissions and cleanup
Initregistersteamchattags.use. Both commands checkpermission.UserHasPermission(player.UserIDString, PermUse)first.Unloadwrites the tag store. Entries for disbanded teams are not removed automatically; the plugin never prunes them, and/tagclearrequires a live team, so to clear leftovers delete or edit the data file.
Install it on your server
Save TeamChatTags.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/TeamChatTags.json(Carbon:carbon/configs/TeamChatTags.json). After editing, reload it:oxide.reload TeamChatTags c.reload TeamChatTags # CarbonGrant the permission
Give it to every player, or swapdefaultfor a group such asvip:oxide.grant group default teamchattags.use c.grant group default teamchattags.use # Carbon
Variations
Each one opens the generator with a ready prompt you can tweak.
Colored tags
Wrap the tag in a Unity rich text color from the config before inserting it into the chat line.
Generate this versionTeam-only chat
Route tag-prefixed messages only to team members instead of the global channel.
Generate this versionTag request workflow
Refuse the tag write and store a pending request for admins to approve.
Generate this version
FAQ
What do players see when their team has no tag?
Nothing changes. OnPlayerChat returns null when the lookup misses, so the message falls through to Rust's default chat line. No empty brackets appear.
Does the tag survive a team leaving or kicking members?
The tag is keyed by the team id (player.currentTeam), not by a player. Any current member with that id sees it. Removing a member from the team means they stop matching the id, so they lose the tag display immediately.
Can members change the tag?
No. The command handler checks team.teamLeader == player.userID.Get() before writing. Members get the Set.NotLeader message instead. Non-leaders also cannot clear it with /tagclear.
Will a rich text tag break chat?
The default TagFormat is plain [{0}]. You can add markup to TagFormat in oxide/config/TeamChatTags.json, but tag values themselves are restricted to letters and digits, so the tag cannot carry color codes.
How do I install it on Oxide or Carbon?
Drop the .cs file into oxide/plugins. Both Oxide and Carbon compile and load the same file. The plugin writes oxide/config/TeamChatTags.json on first load. Grant teamchattags.use to any group that should use /tag.
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 teams & clans plugins
- Simple clansAdds a lightweight clan system with /clan create, invite, join, leave and info, stores clans in a data file, prefixes members' chat with their tagโฆOnServerInitializedOnPlayerChatOnEntityTakeDamage
- Team auto authorizationAutomatically authorizes a player on every tool cupboard, code lock and auto turret owned by their team when they join a team, and removes thatโฆOnTeamAcceptInviteOnTeamLeaveOnTeamKick
- Team death alertsSends a chat alert to every online teammate when a player dies, including the killer name and the map grid of the death.OnPlayerDeath
Last updated Oct 26, 2026.
Customize team tags in chat
Customize