Skip to content
RustPluginGenerator

OnGroupParentSet

OnGroupParentSet is called after an Oxide permission group's parent has been changed to another group. It fires from Permission.SetGroupParent once the new parent is written to the group data, so the change is already in effect when your hook runs.

Signature
void OnGroupParentSet(string name, string parent)

Parameters

Hook parameters
ParameterTypeDescription
namestringThe names of the group whose parent is being changed. This is the group that was just assigned a new parent, not the parent itself.
parentstringThe name of the group that became the new parent. The hook only fires for a non-empty parent; clearing a group's parent does not trigger it.

Return value

The return value is ignored. Oxide calls the hook after groupData.ParentGroup has already been set to the new parent, and Permission.SetGroupParent continues and returns true regardless of what your plugin returns, so you cannot block or revert the change from this hook.

How it works

This hook fires inside Oxide's own permission system, not a patched game method. Permission.SetGroupParent is called when an admin (or a plugin) runs a command like oxide.group parent <group> <parent> and every early return has been passed: the group exists, the parent exists, the two names differ, the parent is not already this group's parent, and the new link would not create a circular parent chain. The new parent is assigned to the group data before the hook is called, so name is a child group and parent is now its parent.

Only active assignments trigger it. Clearing a parent by passing an empty value, setting the same parent again, and every rejected case (missing group, missing parent, self-parenting, circular chain) return before the hook fires, so you cannot use it to observe "parent removed" events.

Both arguments are plain strings. Because the change is already committed, you cannot veto it by returning a value. Declaring a more specific parameter type is pointless here since both parameters are strings; Oxide calls your method when the arguments match the declared types, which they always do.

Useful for syncing group hierarchies to external systems, caching inheritance trees, or auditing admin actions alongside OnGroupCreated and OnUserGroupAdded.

Example plugin: Log and snapshot group hierarchy changes

GroupParentAudit.cs
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;

namespace Oxide.Plugins
{
    [Info("Group Parent Audit", "RustPluginGenerator", "1.0.0")]
    [Description("Tracks and logs changes to Oxide permission group parent links")]
    public class GroupParentAudit : RustPlugin
    {
        private const string PermShow = "groupparentaudit.show";
        private const string DataFile = "GroupParentAudit/Trail";

        private PluginConfig config;
        private AuditData data;

        private class PluginConfig
        {
            [JsonProperty("Maximum stored entries")]
            public int MaxEntries { get; set; } = 200;

            [JsonProperty("Broadcast to online admins")]
            public bool NotifyAdmins { get; set; } = true;
        }

        private class AuditData
        {
            [JsonProperty("Parents")]
            public Dictionary<string, string> Parents { get; set; } = new Dictionary<string, string>();

            [JsonProperty("History")]
            public List<Entry> History { get; set; } = new List<Entry>();
        }

        private class Entry
        {
            [JsonProperty("Group")]
            public string Group { get; set; }

            [JsonProperty("Parent")]
            public string Parent { get; set; }

            [JsonProperty("Time")]
            public string Time { get; set; }
        }

        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>
            {
                ["Reparented"] = "Group '{0}' now inherits from '{1}'.",
                ["History"] = "Recorded {0} group parent changes. Latest: {1}",
                ["NoHistory"] = "No group parent changes recorded yet.",
                ["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(PermShow, this);
            LoadData();
        }

        private void LoadData()
        {
            data = Interface.Oxide.DataFileSystem.ReadObject<AuditData>(DataFile) ?? new AuditData();
        }

        private void SaveData()
        {
            Interface.Oxide.DataFileSystem.WriteObject(DataFile, data);
        }

        private void OnGroupParentSet(string name, string parent)
        {
            if (string.IsNullOrEmpty(name) || string.IsNullOrEmpty(parent)) return;

            data.Parents[name] = parent;
            data.History.Add(new Entry { Group = name, Parent = parent, Time = DateTime.UtcNow.ToString("u") });

            while (data.History.Count > config.MaxEntries)
                data.History.RemoveAt(0);

            if (config.NotifyAdmins)
            {
                foreach (var player in BasePlayer.activePlayerList)
                {
                    if (player != null && player.IsAdmin)
                        player.ChatMessage(Msg("Reparented", player.UserIDString, name, parent));
                }
            }
        }

        private void OnServerSave()
        {
            SaveData();
        }

        [ChatCommand("groupparent")]
        private void CmdGroupParent(BasePlayer player, string command, string[] args)
        {
            if (player == null) return;
            if (!player.IsAdmin && !permission.UserHasPermission(player.UserIDString, PermShow))
            {
                player.ChatMessage(Msg("Denied", player.UserIDString));
                return;
            }

            if (data.History.Count == 0)
            {
                player.ChatMessage(Msg("NoHistory", player.UserIDString));
                return;
            }

            var last = data.History[data.History.Count - 1];
            player.ChatMessage(Msg("History", player.UserIDString, data.History.Count, string.Format("{0} -> {1} at {2}", last.Group, last.Parent, last.Time)));
        }

        private void Unload()
        {
            SaveData();
        }
    }
}

The plugin keeps a running snapshot of every group's parent in oxide/data and writes a line to the server log whenever an admin reparents a group. OnGroupParentSet updates the snapshot entry for name to the new parent and appends a timestamped record to a short in-memory audit trail, which is saved to data on Unload and OnServerSave. A console command groupparent.show prints the last recorded changes so admins can review who restructured the hierarchy. Since the hook only fires for real assignments, the trail contains no noise from cleared or rejected parent changes; those are handled separately in the same console command by reading the current permission data.

Common use cases

  • Mirror the hierarchy to Discord or a web dashboard

    Store the child/parent pair and push it to an external source when the server saves or on a short timer, so your staff panel shows the same group tree as Oxide.

  • Cache inherited permission trees

    Rebuild a cached map of group inheritance whenever a parent link changes, avoiding repeated walks through parent groups on every permission check in your own code.

  • Audit group restructuring

    Record the group, the new parent and a timestamp so you can trace who reorganised the permission hierarchy after a staff dispute.

Pitfalls

  • It does not fire when a parent is removed

    Clearing a group's parent returns early in Permission.SetGroupParent before the hook fires. If you need to track removal, compare your stored snapshot against the live data periodically or on save instead.

  • The change is already applied

    Returning a value does nothing. Treat this hook as a notification and revert explicitly with permission.SetGroupParent if you need to undo a change, accepting that this will not re-fire the hook for the same pair.

  • Not called for every SetGroupParent success path

    Setting the same parent the group already has, or passing an empty parent, both return true without firing the hook, so a return value of true from the command does not mean the hook ran.

Build a plugin with OnGroupParentSet

Describe what should happen and get a compiled plugin that uses this hook.

Prompt: Mirror the hierarchy to Discord or a web dashboard using the OnGroupParentSet hook, with a config file and a permission
Generate this plugin

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 OnGroupParentSet

Generate