Skip to content
RustPluginGenerator

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

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

TeamChatTags.json
{
  "Tag format ({0} tag, {1} name, {2} message)": "[{0}] {1}: {2}",
  "Minimum tag length": 2,
  "Maximum tag length": 5
}

Commands and permissions

Chat and console commands
CommandWhat it doesPermission
/tag <TAG>Sets or changes your team's chat tag. Team leader only, 2-5 letters or digits.teamchattags.use
/tagPrints the tag currently assigned to your team, or tells you none is set.teamchattags.use
/tagclearRemoves the tag from your team. Team leader only.teamchattags.use
Permissions
PermissionGrants
teamchattags.useGrants access to /tag and /tagclear; without it the commands are refused.

How the code works

  1. Store tags per team

    Tags live in oxide/data/TeamChatTags.json as a dictionary keyed by the team's numeric id (player.currentTeam). The plugin loads it in OnServerInitialized via Interface.Oxide.DataFileSystem.ReadObject<Dictionary<string, string>>(DataFileName) and writes it back in Unload. A team id of 0 means the player is not in a team, so no lookup is attempted.

  2. Validate what the leader types

    /tag <TAG> checks args[0].Trim() character by character and refuses anything outside letters and digits, then enforces the MinLength and MaxLength config 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.

  3. Keep the leader in charge

    Before writing a tag the command handler finds the team with RelationshipManager.ServerInstance.FindTeam(player.currentTeam) and compares team.teamLeader against player.userID.Get(). Members get a chat refusal from the lang table, not an exception. /tagclear uses the same check, so a member cannot blank the tag by accident.

  4. Inject the tag into chat

    OnPlayerChat returns object; returning non-null replaces the displayed text. The hook looks up the tag for player.currentTeam and, when found, returns the string built from TagFormat (default [{0}] {1}: {2}). When no tag exists it returns null so vanilla chat formatting is untouched. The channel parameter is not inspected.

  5. Config and messages

    The PluginConfig class exposes TagFormat (the string template with {0} for tag, {1} for player name, {2} for message), MinLength and MaxLength. Chat replies use the LoadDefaultMessages dictionary under keys like Set.Success, Set.NotLeader and Set.Invalid, read through the Msg helper so they can be translated in oxide/lang.

  6. Permissions and cleanup

    Init registers teamchattags.use. Both commands check permission.UserHasPermission(player.UserIDString, PermUse) first. Unload writes the tag store. Entries for disbanded teams are not removed automatically; the plugin never prunes them, and /tagclear requires a live team, so to clear leftovers delete or edit the data file.

Install it on your server

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

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

    Route tag-prefixed messages only to team members instead of the global channel.

    Generate this version
  • Tag 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.

Prompt: Write a Rust Oxide plugin where team leaders set a 2-5 character tag via /tag, stored in oxide/data, and every team member's chat message is prefixed with [TAG] using OnPlayerChat.
Customize for my server

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

Last updated Oct 26, 2026.

Customize team tags in chat

Customize