Skip to content
RustPluginGenerator

How to make a Rust clan plugin (tag, invite, friendly fire block)

Adds a lightweight clan system with /clan create, invite, join, leave and info, stores clans in a data file, prefixes members' chat with their tag and blocks damage between clanmates via OnPlayerChat and OnEntityTakeDamage.

Rust's built-in team system is capped and gives you no persistent identity: no tag in chat, no roster that survives a wipe, and no friendly-fire switch beyond the team flag. Admins who run group play usually want something simpler than a full clan mod - a tag, an owner, a member list and no accidental teamkills.

This page builds exactly that. Clans live in a JSON data file under oxide/data keyed by a short tag, each holding an owner ID and a list of member IDs. Five chat commands cover the lifecycle: create, invite, join, leave and info. Chat messages from clan members are rewritten through OnPlayerChat to show [TAG] name: message. Damage between two members of the same clan is cancelled in OnEntityTakeDamage before Rust applies it, so rockets and melee alike do nothing to clanmates.

The plugin below is complete: config for tag length and friendly-fire behaviour, permission-gated administration, language strings, and a /clan info view that lists members by name.

The plugin code

SimpleClans.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using Oxide.Core;
using UnityEngine;

namespace Oxide.Plugins
{
    [Info("SimpleClans", "RustPluginGenerator", "1.0.0")]
    [Description("Adds lightweight clans with tags, invites and friendly fire protection.")]
    public class SimpleClans : RustPlugin
    {
        private const string PermUse = "simpleclans.use";
        private const string PermAdmin = "simpleclans.admin";

        private PluginConfig config;
        private StoredData data = new StoredData();
        private readonly Dictionary<ulong, string> invites = new Dictionary<ulong, string>();

        private class PluginConfig
        {
            [JsonProperty("Max tag length")]
            public int MaxTagLength = 5;

            [JsonProperty("Max members per clan")]
            public int MaxMembers = 20;

            [JsonProperty("Chat tag color")]
            public string TagColor = "#a8d8ff";

            [JsonProperty("Allow self damage")]
            public bool AllowSelfDamage = false;
        }

        private class ClanData
        {
            [JsonProperty("Owner ID")]
            public string OwnerId;

            [JsonProperty("Owner name")]
            public string OwnerName;

            [JsonProperty("Created (unix)")]
            public int Created;

            [JsonProperty("Members")]
            public List<string> Members = new List<string>();
        }

        private class StoredData
        {
            [JsonProperty("Clans")]
            public Dictionary<string, ClanData> Clans = new Dictionary<string, ClanData>();
        }

        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>
            {
                ["NoPermission"] = "You do not have permission to use that.",
                ["UsageCreate"] = "Usage: /clan create <tag>",
                ["UsageInvite"] = "Usage: /clan invite <player>",
                ["UsageJoin"] = "Usage: /clan join <tag>",
                ["AlreadyInClan"] = "You are already in clan {0}.",
                ["NotInClan"] = "You are not in a clan.",
                ["TagTooLong"] = "Clan tags cannot be longer than {0} characters.",
                ["TagTaken"] = "A clan with tag {0} already exists.",
                ["ClanFull"] = "That clan already has {0} members.",
                ["ClanCreated"] = "Clan {0} created. You are the owner.",
                ["ClanGone"] = "That clan no longer exists.",
                ["NoInvite"] = "You have no pending invite to {0}.",
                ["InviteSent"] = "Invited {0} to clan {1}.",
                ["InviteReceived"] = "You were invited to clan {0}. Use /clan join {0}.",
                ["InviteFull"] = "Clan {0} is full.",
                ["Joined"] = "You joined clan {0}.",
                ["MemberJoined"] = "{0} joined clan {1}.",
                ["Left"] = "You left clan {0}.",
                ["MemberLeft"] = "{0} left clan {1}.",
                ["ClanDeleted"] = "Clan {0} was deleted.",
                ["OwnerOnly"] = "Only the clan owner can do that.",
                ["NotMember"] = "{0} is not in your clan.",
                ["Kicked"] = "You removed {0} from clan {1}.",
                ["TargetKicked"] = "You were removed from clan {0}.",
                ["InfoHeader"] = "Clan {0} - owner {1}, {2} member(s)",
                ["InfoMember"] = " - {0}{1}",
                ["InfoInvites"] = "Pending invites: {0}",
                ["InfoNoClan"] = "You are in no clan and have no pending invites.",
                ["PlayerNotFound"] = "No online player found matching {0}."
            }, 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);
            permission.RegisterPermission(PermAdmin, this);
        }

        private void OnServerInitialized()
        {
            LoadData();
        }

        private void LoadData()
        {
            data = Interface.Oxide.DataFileSystem.ReadObject<StoredData>(Name) ?? new StoredData();
            if (data.Clans == null) data.Clans = new Dictionary<string, ClanData>();
        }

        private void SaveData() => Interface.Oxide.DataFileSystem.WriteObject(Name, data);

        private void Unload() => SaveData();

        private string GetClanTag(ulong userId)
        {
            string id = userId.ToString();
            foreach (var kvp in data.Clans)
            {
                if (kvp.Value.Members != null && kvp.Value.Members.Contains(id))
                    return kvp.Key;
            }
            return null;
        }

        private string Prefix(string tag) => string.Format("<color={0}>[{1}]</color>", config.TagColor, tag);

        [ChatCommand("clan")]
        private void CmdClan(BasePlayer player, string command, string[] args)
        {
            if (player == null) return;
            if (!permission.UserHasPermission(player.UserIDString, PermUse))
            {
                SendReply(player, Msg("NoPermission", player.UserIDString));
                return;
            }

            string sub = args != null && args.Length > 0 ? args[0].ToLower() : "info";
            switch (sub)
            {
                case "create": CmdCreate(player, args); break;
                case "invite": CmdInvite(player, args); break;
                case "join": CmdJoin(player, args); break;
                case "leave": CmdLeave(player); break;
                case "info": CmdInfo(player); break;
                case "kick": CmdKick(player, args); break;
                case "disband": CmdDisband(player); break;
                default: CmdInfo(player); break;
            }
        }

        private void CmdCreate(BasePlayer player, string[] args)
        {
            if (args == null || args.Length < 2)
            {
                SendReply(player, Msg("UsageCreate", player.UserIDString));
                return;
            }

            string current = GetClanTag(player.userID);
            if (current != null)
            {
                SendReply(player, Msg("AlreadyInClan", player.UserIDString, current));
                return;
            }

            string tag = args[1].ToUpper();
            if (tag.Length > config.MaxTagLength)
            {
                SendReply(player, Msg("TagTooLong", player.UserIDString, config.MaxTagLength));
                return;
            }
            if (data.Clans.ContainsKey(tag))
            {
                SendReply(player, Msg("TagTaken", player.UserIDString, tag));
                return;
            }

            data.Clans[tag] = new ClanData
            {
                OwnerId = player.UserIDString,
                OwnerName = player.displayName,
                Created = Facepunch.Math.Epoch.Current,
                Members = new List<string> { player.UserIDString }
            };
            SaveData();
            SendReply(player, Msg("ClanCreated", player.UserIDString, tag));
        }

        private void CmdInvite(BasePlayer player, string[] args)
        {
            if (args == null || args.Length < 2)
            {
                SendReply(player, Msg("UsageInvite", player.UserIDString));
                return;
            }

            string tag = GetClanTag(player.userID);
            if (tag == null)
            {
                SendReply(player, Msg("NotInClan", player.UserIDString));
                return;
            }
            ClanData clan = data.Clans[tag];
            if (clan.OwnerId != player.UserIDString)
            {
                SendReply(player, Msg("OwnerOnly", player.UserIDString));
                return;
            }
            if (clan.Members.Count >= config.MaxMembers)
            {
                SendReply(player, Msg("ClanFull", player.UserIDString, config.MaxMembers));
                return;
            }

            BasePlayer target = FindPlayer(string.Join(" ", args.Skip(1)));
            if (target == null)
            {
                SendReply(player, Msg("PlayerNotFound", player.UserIDString, args[1]));
                return;
            }
            if (GetClanTag(target.userID) != null)
            {
                SendReply(player, Msg("AlreadyInClan", player.UserIDString, GetClanTag(target.userID)));
                return;
            }

            invites[target.userID] = tag;
            SendReply(player, Msg("InviteSent", player.UserIDString, target.displayName, tag));
            SendReply(target, Msg("InviteReceived", target.UserIDString, tag));
        }

        private void CmdJoin(BasePlayer player, string[] args)
        {
            if (args == null || args.Length < 2)
            {
                SendReply(player, Msg("UsageJoin", player.UserIDString));
                return;
            }
            if (GetClanTag(player.userID) != null)
            {
                SendReply(player, Msg("AlreadyInClan", player.UserIDString, GetClanTag(player.userID)));
                return;
            }

            string tag = args[1].ToUpper();
            string invited;
            if (!invites.TryGetValue(player.userID, out invited) || invited != tag)
            {
                SendReply(player, Msg("NoInvite", player.UserIDString, tag));
                return;
            }
            if (!data.Clans.ContainsKey(tag))
            {
                invites.Remove(player.userID);
                SendReply(player, Msg("ClanGone", player.UserIDString));
                return;
            }
            ClanData clan = data.Clans[tag];
            if (clan.Members.Count >= config.MaxMembers)
            {
                SendReply(player, Msg("InviteFull", player.UserIDString, tag));
                return;
            }

            clan.Members.Add(player.UserIDString);
            invites.Remove(player.userID);
            SaveData();
            SendReply(player, Msg("Joined", player.UserIDString, tag));
            BroadcastToClan(tag, Msg("MemberJoined", null, player.displayName, tag), player.userID);
        }

        private void CmdLeave(BasePlayer player)
        {
            string tag = GetClanTag(player.userID);
            if (tag == null)
            {
                SendReply(player, Msg("NotInClan", player.UserIDString));
                return;
            }

            ClanData clan = data.Clans[tag];
            clan.Members.Remove(player.UserIDString);
            SendReply(player, Msg("Left", player.UserIDString, tag));

            if (clan.Members.Count == 0)
            {
                data.Clans.Remove(tag);
                SendReply(player, Msg("ClanDeleted", player.UserIDString, tag));
            }
            else if (clan.OwnerId == player.UserIDString)
            {
                string newOwner = clan.Members[0];
                clan.OwnerId = newOwner;
                BasePlayer ownerPlayer = BasePlayer.FindByID(ulong.Parse(newOwner));
                clan.OwnerName = ownerPlayer != null ? ownerPlayer.displayName : newOwner;
            }

            BroadcastToClan(tag, Msg("MemberLeft", null, player.displayName, tag));
            SaveData();
        }

        private void CmdInfo(BasePlayer player)
        {
            string tag = GetClanTag(player.userID);
            if (tag == null)
            {
                string pending;
                if (invites.TryGetValue(player.userID, out pending))
                {
                    SendReply(player, Msg("InfoInvites", player.UserIDString, pending));
                    return;
                }
                SendReply(player, Msg("InfoNoClan", player.UserIDString));
                return;
            }

            ClanData clan = data.Clans[tag];
            SendReply(player, Msg("InfoHeader", player.UserIDString, tag, clan.OwnerName, clan.Members.Count));
            foreach (string memberId in clan.Members)
            {
                BasePlayer member = BasePlayer.FindByID(ulong.Parse(memberId));
                string name = member != null ? member.displayName : (memberId == clan.OwnerId ? clan.OwnerName : memberId);
                SendReply(player, Msg("InfoMember", player.UserIDString, name, memberId == clan.OwnerId ? " (owner)" : ""));
            }
        }

        private void CmdKick(BasePlayer player, string[] args)
        {
            if (!permission.UserHasPermission(player.UserIDString, PermAdmin))
            {
                SendReply(player, Msg("NoPermission", player.UserIDString));
                return;
            }
            if (args == null || args.Length < 2)
            {
                SendReply(player, Msg("UsageInvite", player.UserIDString));
                return;
            }

            string tag = GetClanTag(player.userID);
            if (tag == null)
            {
                SendReply(player, Msg("NotInClan", player.UserIDString));
                return;
            }
            ClanData clan = data.Clans[tag];

            BasePlayer target = FindPlayer(string.Join(" ", args.Skip(1)));
            if (target == null || !clan.Members.Contains(target.UserIDString))
            {
                SendReply(player, Msg("NotMember", player.UserIDString, args[1]));
                return;
            }
            if (target == player)
            {
                SendReply(player, Msg("OwnerOnly", player.UserIDString));
                return;
            }

            clan.Members.Remove(target.UserIDString);
            SendReply(player, Msg("Kicked", player.UserIDString, target.displayName, tag));
            SendReply(target, Msg("TargetKicked", target.UserIDString, tag));
            SaveData();
        }

        private void CmdDisband(BasePlayer player)
        {
            if (!permission.UserHasPermission(player.UserIDString, PermAdmin))
            {
                SendReply(player, Msg("NoPermission", player.UserIDString));
                return;
            }
            string tag = GetClanTag(player.userID);
            if (tag == null)
            {
                SendReply(player, Msg("NotInClan", player.UserIDString));
                return;
            }
            ClanData clan = data.Clans[tag];
            if (clan.OwnerId != player.UserIDString && !player.IsAdmin)
            {
                SendReply(player, Msg("OwnerOnly", player.UserIDString));
                return;
            }

            data.Clans.Remove(tag);
            BroadcastToClan(tag, Msg("ClanDeleted", null, tag), 0);
            SaveData();
        }

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

            Server.Broadcast(string.Format("{0} {1}: {2}", Prefix(tag), player.displayName, message));
            return true;
        }

        private object OnEntityTakeDamage(BaseCombatEntity entity, HitInfo hitInfo)
        {
            if (entity == null || hitInfo == null) return null;
            BasePlayer victim = entity as BasePlayer;
            BasePlayer attacker = hitInfo.InitiatorPlayer;
            if (victim == null || attacker == null || attacker.IsNpc) return null;
            if (victim == attacker && config.AllowSelfDamage) return null;

            string victimTag = GetClanTag(victim.userID);
            if (victimTag == null) return null;
            string attackerTag = GetClanTag(attacker.userID);
            if (attackerTag == null || attackerTag != victimTag) return null;

            return true;
        }

        private void BroadcastToClan(string tag, string message, ulong excludeId = 0)
        {
            ClanData clan;
            if (!data.Clans.TryGetValue(tag, out clan)) return;
            foreach (string memberId in clan.Members)
            {
                ulong parsed;
                if (!ulong.TryParse(memberId, out parsed) || parsed == excludeId) continue;
                BasePlayer member = BasePlayer.FindByID(parsed);
                if (member != null && member.IsConnected) member.ChatMessage(message);
            }
        }

        private BasePlayer FindPlayer(string nameOrId)
        {
            ulong id;
            if (ulong.TryParse(nameOrId, out id))
            {
                BasePlayer byId = BasePlayer.FindByID(id);
                if (byId != null) return byId;
            }
            return BasePlayer.activePlayerList.FirstOrDefault(p =>
                p.displayName.Equals(nameOrId, StringComparison.OrdinalIgnoreCase));
        }
    }
}

Hooks used: OnServerInitialized, OnPlayerChat, OnEntityTakeDamage

Config file

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

SimpleClans.json
{
  "Max tag length": 5,
  "Max members per clan": 20,
  "Chat tag color": "#a8d8ff",
  "Allow self damage": false,
  "Owner ID": "",
  "Owner name": "",
  "Created (unix)": 0,
  "Members": [],
  "Clans": {}
}

Commands and permissions

Chat and console commands
CommandWhat it doesPermission
/clan create <tag>Creates a clan with you as owner. Fails if you are already in a clan or the tag is taken.simpleclans.use
/clan invite <player>Sends a clan invitation to an online player. Owner only.simpleclans.use
/clan join <tag>Accepts a pending invite to the clan with that tag.simpleclans.use
/clan leaveLeaves your current clan. If the owner leaves, ownership passes to the next member or the clan is disbanded.simpleclans.use
/clan infoShows your clan tag, owner and member list, or your invite list if you have no clan.simpleclans.use
/clan kick <player>Removes a member from your clan. Requires the admin permission.simpleclans.admin
/clan disbandDeletes your clan and clears all memberships. Owner or server admin only.simpleclans.admin
Permissions
PermissionGrants
simpleclans.useGrants access to the /clan create, invite, join, leave and info commands.
simpleclans.adminGrants access to /clan kick and /clan disband.

How the code works

  1. Data model and the data file

    A nested ClanData class holds Owner ID, Owner name, Created (unix) and a Members list of IDs. A StoredData wrapper maps each tag to a clan under the Clans key. LoadData reads it with Interface.Oxide.DataFileSystem.ReadObject<StoredData>(Name) and creates an empty object when the file is missing or corrupt. SaveData writes it back. Nothing touches disk until a command changes state, so the plugin stays quiet on a busy server.

  2. Tag prefix in chat with OnPlayerChat

    OnPlayerChat(BasePlayer player, string message, Chat.ChatChannel channel) looks up the speaker's clan. If a clan exists, it calls Server.Broadcast(string.Format("{0} {1}: {2}", tag, player.displayName, message)) and returns true to swallow the original message. Return null for everyone else so Rust prints the message normally. Skip NPCs with player.IsNpc; the hook fires on all channels, so strip or gate non-global channels if you do not want prefixes there.

  3. Blocking friendly fire in OnEntityTakeDamage

    The hook receives BaseCombatEntity entity and HitInfo hitInfo. Get the attacker with hitInfo.InitiatorPlayer, bail out when either side is null, and compare GetClanTag for both players. When the tags match and are non-empty, return true - Oxide treats a true return as "damage handled" and Rust never applies it. Self-damage is gated by Allow self damage, so set it to true if you want players to hurt themselves.

  4. Create, join and leave

    /clan create <tag> validates length against Max tag length, uppercases the tag, rejects duplicates and stores the caller as owner. /clan invite <player> writes a pending entry into an in-memory dictionary of invited IDs to tags, which is not persisted so invites vanish on restart. /clan join <tag> checks that dictionary and adds the player. /clan leave removes the member; if the owner leaves and members remain, ownership transfers to the next member, otherwise the clan is deleted.

  5. Config and language strings

    PluginConfig exposes Max tag length (default 5), Max members per clan, Chat tag color and Allow self damage. LoadConfig falls back to defaults and warns when the file cannot be parsed. Every reply goes through Msg(), which reads from lang registered in LoadDefaultMessages, so {0} placeholders carry the tag or player name. No chat text is hard-coded in the command handlers.

  6. Permissions and cleanup

    Init registers simpleclans.use and simpleclans.admin. Command handlers check permission.UserHasPermission(player.UserIDString, PermUse) and reply with the NoPermission language key. Unload calls SaveData() so pending membership changes land on disk during a reload. There is no timer and no UI, so nothing else needs tearing down - a reload never leaves stale panels behind.

Install it on your server

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

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

Variations

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

  • Clan bank and scrap cost

    Charge scrap to create a clan and add a shared bank members can deposit into.

    Generate this version
  • Persistent tag in chat and console

    Show the clan tag in server broadcasts and expose a console command listing every clan.

    Generate this version
  • Ally clans without friendly fire

    Let two clans declare an alliance that also cancels damage between them.

    Generate this version

FAQ

Does the clan tag replace Rust's team system?

No. Clans are independent of teams. Both can coexist, but only team damage rules are handled by Rust, and only clan damage is blocked by this plugin. If you run both, set the Rust team friendly-fire flag to your preferred default so the two systems do not conflict.

Why does chat lose the clan prefix after a reload?

Only the clan roster is persisted. Pending invites live in memory and are cleared on unload, along with any in-progress state. Owners simply reinvite. This is intentional so a wiped database cannot resurrect stale invites.

Who becomes owner when the owner leaves?

The first member in the clan's member list is promoted and their name is written to Owner name. If no members remain, the clan entry is deleted from the data file entirely.

How do I install this on Oxide or Carbon?

Drop SimpleClans.cs into oxide/plugins, run oxide.reload SimpleClans from the server console, then grant simpleclans.use to your default group with oxide.grant group default simpleclans.use. Carbon loads the same file from its own plugins folder.

Can a player be in two clans?

No. Every command checks the player's current clan first and refuses to create or join while a membership exists. Leaving is always required before joining another clan, which keeps the lookup table consistent.

Customize it for your server

Change the reward, add a cooldown, restrict it to VIPs. Start from this prompt and edit anything.

Prompt: Build a simple clan plugin with /clan create, invite, join, leave and info, tags in chat, friendly fire blocked between clanmates, clans stored in oxide/data, max tag length 5 and max 20 members.
Customize for my server

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

Last updated Oct 26, 2026.

Customize simple clans

Customize