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
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.
{
"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
| Command | What it does | Permission |
|---|---|---|
/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 leave | Leaves your current clan. If the owner leaves, ownership passes to the next member or the clan is disbanded. | simpleclans.use |
/clan info | Shows 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 disband | Deletes your clan and clears all memberships. Owner or server admin only. | simpleclans.admin |
| Permission | Grants |
|---|---|
simpleclans.use | Grants access to the /clan create, invite, join, leave and info commands. |
simpleclans.admin | Grants access to /clan kick and /clan disband. |
How the code works
Data model and the data file
A nested
ClanDataclass holdsOwner ID,Owner name,Created (unix)and aMemberslist of IDs. AStoredDatawrapper maps each tag to a clan under theClanskey.LoadDatareads it withInterface.Oxide.DataFileSystem.ReadObject<StoredData>(Name)and creates an empty object when the file is missing or corrupt.SaveDatawrites it back. Nothing touches disk until a command changes state, so the plugin stays quiet on a busy server.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 callsServer.Broadcast(string.Format("{0} {1}: {2}", tag, player.displayName, message))and returnstrueto swallow the original message. Returnnullfor everyone else so Rust prints the message normally. Skip NPCs withplayer.IsNpc; the hook fires on all channels, so strip or gate non-global channels if you do not want prefixes there.Blocking friendly fire in OnEntityTakeDamage
The hook receives
BaseCombatEntity entityandHitInfo hitInfo. Get the attacker withhitInfo.InitiatorPlayer, bail out when either side is null, and compareGetClanTagfor both players. When the tags match and are non-empty, returntrue- Oxide treats atruereturn as "damage handled" and Rust never applies it. Self-damage is gated byAllow self damage, so set it to true if you want players to hurt themselves.Create, join and leave
/clan create <tag>validates length againstMax 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 leaveremoves the member; if the owner leaves and members remain, ownership transfers to the next member, otherwise the clan is deleted.Config and language strings
PluginConfigexposesMax tag length(default 5),Max members per clan,Chat tag colorandAllow self damage.LoadConfigfalls back to defaults and warns when the file cannot be parsed. Every reply goes throughMsg(), which reads fromlangregistered inLoadDefaultMessages, so{0}placeholders carry the tag or player name. No chat text is hard-coded in the command handlers.Permissions and cleanup
Initregisterssimpleclans.useandsimpleclans.admin. Command handlers checkpermission.UserHasPermission(player.UserIDString, PermUse)and reply with theNoPermissionlanguage key.UnloadcallsSaveData()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
Save SimpleClans.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/SimpleClans.json(Carbon:carbon/configs/SimpleClans.json). After editing, reload it:oxide.reload SimpleClans c.reload SimpleClans # CarbonGrant the permission
Give it to every player, or swapdefaultfor a group such asvip: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 versionPersistent tag in chat and console
Show the clan tag in server broadcasts and expose a console command listing every clan.
Generate this versionAlly 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.
Edit the prompt on the next screen. 2 free plugins a month, no card.
More teams & clans plugins
- 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
- Team info commandAdds a /team chat command that lists every member of the caller's Rust team with online status, health and map grid for online members.
Last updated Oct 26, 2026.
Customize simple clans
Customize