OnUserGroupAdded
OnUserGroupAdded is called by Oxide's permission system after a user has been successfully added to a group via Permission.AddUserGroup (the oxide.usergroup add command or Interface.Oxide.Permission.AddUserGroup). It fires once per successful assignment.
void OnUserGroupAdded(string playerId, string groupName)Parameters
| Parameter | Type | Description |
|---|---|---|
playerId | string | The Steam ID of the user being added to the group, as a string (the same id used by permission.userHasGroup and stored in oxide users data). It is not a display name. |
groupName | string | The name of the group being assigned, e.g. vip, admin or a custom group you created with oxide.group add. Already validated to exist when the hook fires. |
Return value
The return value is ignored. OnUserGroupAdded is a notification hook called after the group was added, so returning null, true or anything else cannot undo the assignment or change the flow; to reverse it, call Permission.RemoveUserGroup yourself.
How it works
The hook fires from inside Permission.AddUserGroup, after the group-existence check and after the group was actually inserted into the user's record. Both early returns (unknown group, user already in that group) happen before the hook, so every call means a real state change. There is no game method behind it: Oxide's permission layer invokes it directly, so it also triggers for assignments made by plugins or the server console.
playerId is the string Steam ID of the target user, which may be offline; the user record exists in the permission data even without a connected BasePlayer. groupName is the group that was granted. Because there is no type information involved, you cannot narrow the parameters by declaring a more specific type; both are plain strings.
Practical uses: keeping an external store in sync with permission groups (VIP expiry tables, Discord role channels through other plugins), granting a kit or welcome message on first group assignment, or writing an audit trail of who gained which group. The player is not necessarily online, so resolve them with BasePlayer.FindByID and guard for null before sending chat or UI.
Example plugin: Welcome message and audit log on group assignment
using System.Linq;
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("Group Join Notifier", "RustPluginGenerator", "1.0.0")]
[Description("Announces and logs permission group assignments for users")]
public class GroupJoinNotifier : RustPlugin
{
private const string PermAdmin = "groupjoinnotifier.admin";
private PluginConfig config;
private StoredData data;
private class PluginConfig
{
[JsonProperty("Announcements by group")]
public Dictionary<string, string> Announcements = new Dictionary<string, string>
{
["vip"] = "You were added to the VIP group. Thanks for your support!",
["staff"] = "You are now part of the staff group."
};
[JsonProperty("Max log entries per user")]
public int MaxLogEntries = 20;
}
private class StoredData
{
[JsonProperty("Assignment log")]
public Dictionary<string, List<string>> Log = new Dictionary<string, List<string>>();
}
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>
{
["Announce"] = "{0} was added to the '{1}' group.",
["NoLog"] = "No group assignments recorded for {0}.",
["LogHeader"] = "Group history for {0} ({1} entries):",
["Denied"] = "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(PermAdmin, this);
data = Interface.Oxide.DataFileSystem.ReadObject<StoredData>(Name) ?? new StoredData();
}
private void Unload()
{
Interface.Oxide.DataFileSystem.WriteObject(Name, data);
}
private void OnUserGroupAdded(string playerId, string groupName)
{
if (string.IsNullOrEmpty(playerId) || string.IsNullOrEmpty(groupName)) return;
if (!data.Log.TryGetValue(playerId, out List<string> entries))
{
entries = new List<string>();
data.Log[playerId] = entries;
}
entries.Add($"{DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} {groupName}");
while (entries.Count > Mathf.Max(1, config.MaxLogEntries)) entries.RemoveAt(0);
Interface.Oxide.DataFileSystem.WriteObject(Name, data);
if (!config.Announcements.TryGetValue(groupName, out string text) || string.IsNullOrEmpty(text)) return;
BasePlayer player = BasePlayer.FindByID(Convert.ToUInt64(playerId));
if (player == null || !player.IsConnected) return;
player.ChatMessage(text);
}
[ChatCommand("groupjoin.log")]
private void CmdGroupLog(BasePlayer player, string command, string[] args)
{
if (player == null) return;
if (!permission.UserHasPermission(player.UserIDString, PermAdmin))
{
player.ChatMessage(Msg("Denied", player.UserIDString));
return;
}
string targetId = args != null && args.Length > 0 ? args[0] : player.UserIDString;
if (!data.Log.TryGetValue(targetId, out List<string> entries) || entries.Count == 0)
{
player.ChatMessage(Msg("NoLog", player.UserIDString, targetId));
return;
}
player.ChatMessage(Msg("LogHeader", player.UserIDString, targetId, entries.Count));
foreach (string line in entries) player.ChatMessage(line);
}
}
}The plugin watches group assignments and reacts to them. In OnUserGroupAdded it looks up the configured announce text for the group; if none is set it only writes to the data file. When an online BasePlayer matches playerId, the message is sent through the language file with the player and group name filled in. Every assignment is appended to a per-player log capped by a configurable maximum, and the log is saved to oxide/data/GroupJoinNotifier immediately so a server crash does not lose the record. oxide.groupjoin.log <steamid> chat command lets an admin read back the history for a user, restricted by the groupjoinnotifier.admin permission.
Common use cases
VIP group audit trail
Record every steam ID that was added to your vip group with a timestamp, so you can reconcile purchases against the permission data after a restart.
Grant a starter kit on first group assignment
When a player is added to a whitelist or member group, hand out an item kit if they are online, using the playerId to resolve the BasePlayer.
Sync groups to an external roster
Keep an in-memory roster or data file of who belongs to a staff group, then broadcast it to other plugins with Call when a change happens.
Pitfalls
The user may be offline
Permission data covers offline users, so BasePlayer.FindByID can return null. Always check before sending chat, UI or giving items.
Bulk imports fire it many times
Loading a large permission file or running a loop of AddUserGroup calls triggers the hook for every user. Keep the handler cheap and avoid Puts/PrintWarning inside it.
Fires only on Add, not on re-add
If the user was already in the group, AddUserGroup returns early and the hook never runs. Do not rely on it as a periodic sync check.
Build a plugin with OnUserGroupAdded
Describe what should happen and get a compiled plugin that uses this hook.
Edit the prompt on the next screen. 2 free plugins a month, no card.
Last updated Oct 26, 2026. Found a mistake? Email support@rustplugingenerator.com.
Build a plugin with OnUserGroupAdded
Generate