OnServerUserSet
OnServerUserSet is called from ServerUsers.Set right before an entry is written into the server's users file, after the entry object has been built with the given values.
void OnServerUserSet(ulong uid, ServerUsers.UserGroup group, string username, string notes, long expiry)Parameters
| Parameter | Type | Description |
|---|---|---|
uid | ulong | The Steam ID (64-bit) of the server user entry being written to the users file. |
group | ServerUsers.UserGroup | The ServerUsers.UserGroup the entry is assigned to: None, Banned, Whitelisted, or Owner. |
username | string | The stored player name for the entry, or an empty string when the entry was created without one. |
notes | string | The free-form notes string stored with the entry, e.g. ban reason text or empty. |
expiry | long | Unix timestamp (seconds) at which the entry expires; 0 means it never expires. |
Return value
Returning a value has no effect: Oxide calls the hook, ignores the return value, and continues with users.Add(uid, value). The user entry is always written.
How it works
The hook fires inside ServerUsers.Set, the single method that writes a user entry into the server users table (bans, whitelist, owners). It runs after the ServerUsers.User record is constructed from your arguments but before that record is added to the internal users dictionary, so it fires once per set operation, not on every file save.
Use it to mirror server user changes into your own storage: ban records, whitelist audits, owner lists. group tells you which list the entry lands in, notes usually carries the ban reason, and expiry is a Unix timestamp in seconds (0 = permanent). Because the hook returns nothing meaningful, it cannot veto the write.
Declaring group as the specific ServerUsers.UserGroup enum is the natural signature; there is no narrower player type to declare here since the arguments are plain values. The hook does not fire when entries are removed (ServerUsers.Remove has its own hook) and it does not tell you whether this was a new entry or an overwrite. Pair it with OnServerUserRemove for a complete audit trail.
Example plugin: Audit server user changes to a data file
using System.Linq;
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("ServerUserAudit", "RustPluginGenerator", "1.0.0")]
[Description("Records every server user entry (ban, whitelist, owner) written to the users file.")]
public class ServerUserAudit : RustPlugin
{
private const string PermShow = "serveruseraudit.show";
private PluginConfig config;
private AuditData data;
private Timer flushTimer;
private class PluginConfig
{
[JsonProperty("Maximum stored entries")]
public int MaxEntries = 500;
}
private class AuditEntry
{
[JsonProperty("Time (UTC)")]
public string Time;
[JsonProperty("Steam ID")]
public string Uid;
[JsonProperty("Group")]
public string Group;
[JsonProperty("Username")]
public string Username;
[JsonProperty("Notes")]
public string Notes;
[JsonProperty("Expiry")]
public string Expiry;
}
private class AuditData
{
[JsonProperty("Entries")]
public List<AuditEntry> Entries = new List<AuditEntry>();
}
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 this command.",
["Empty"] = "No server user entries have been recorded yet.",
["Header"] = "{0} recent server user entries:",
["Line"] = "{0} | {1} | {2} | expires {3} | {4}"
}, 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);
data = Interface.Oxide.DataFileSystem.ReadObject<AuditData>(Name) ?? new AuditData();
}
private void Unload()
{
SaveData();
flushTimer?.Destroy();
}
private void OnServerUserSet(ulong uid, ServerUsers.UserGroup group, string username, string notes, long expiry)
{
string expiryText = expiry == 0 ? "never" : DateTimeOffset.FromUnixTimeSeconds(expiry).UtcDateTime.ToString("yyyy-MM-dd HH:mm");
data.Entries.Add(new AuditEntry
{
Time = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss"),
Uid = uid.ToString(),
Group = group.ToString(),
Username = string.IsNullOrEmpty(username) ? "-" : username,
Notes = string.IsNullOrEmpty(notes) ? "-" : notes,
Expiry = expiryText
});
while (data.Entries.Count > config.MaxEntries)
data.Entries.RemoveAt(0);
flushTimer?.Destroy();
flushTimer = timer.Once(1f, SaveData);
}
private void SaveData()
{
Interface.Oxide.DataFileSystem.WriteObject(Name, data);
}
[ChatCommand("useraudit")]
private void CmdUserAudit(BasePlayer player, string command, string[] args)
{
if (!permission.UserHasPermission(player.UserIDString, PermShow))
{
player.ChatMessage(Msg("NoPermission", player.UserIDString));
return;
}
SendLog(player, player.UserIDString);
}
[ConsoleCommand("useraudit.print")]
private void CcmdUserAuditPrint(ConsoleSystem.Arg arg)
{
var player = arg.Player();
if (player != null && !permission.UserHasPermission(player.UserIDString, PermShow))
{
arg.ReplyWith(Msg("NoPermission", player.UserIDString));
return;
}
SendLog(player, player?.UserIDString);
}
private void SendLog(BasePlayer player, string userId)
{
if (data.Entries.Count == 0)
{
Reply(player, Msg("Empty", userId));
return;
}
Reply(player, Msg("Header", userId, data.Entries.Count));
int start = Math.Max(0, data.Entries.Count - 10);
for (int i = start; i < data.Entries.Count; i++)
{
var e = data.Entries[i];
Reply(player, Msg("Line", userId, e.Time, e.Uid, e.Group, e.Expiry, e.Notes));
}
}
private void Reply(BasePlayer player, string message)
{
if (player != null) player.ChatMessage(message);
else Puts(message);
}
}
}The plugin records every ServerUsers.Set call into oxide/data/ServerUserAudit as a list of the most recent entries (capped by config). OnServerUserSet builds an AuditEntry from uid, group, username, notes and expiry, formats the expiry with DateTimeOffset.FromUnixTimeSeconds when it is non-zero, and stamps the event with UTC time. Entries are written to disk on a one-second debounce timer so a batch of ban commands does not cause repeated file writes. An admin command prints the stored log to chat or console.
Common use cases
Ban and whitelist audit log
Persist every user entry written to the server users file with the acting values, so staff can review who was banned or whitelisted and why.
Sync bans to a web panel or Discord relay
Push new
Bannedentries to an external moderation system using a plugin reference, keeping the game server as the source of truth.Owner and whitelist change alerts
Notify online admins when an entry with group
OwnerorWhitelistedis written, flagging unexpected privilege changes.
Pitfalls
It fires for overwrites too
Re-setting the same user writes a new entry and fires the hook again, even if the stored values are identical. Deduplicate in your own log if you only care about changes.
Expiry is Unix seconds, not a duration
The value is the absolute expiry timestamp in seconds (
0for permanent). Converting it as a remaining duration will produce wrong output.No veto and no removal coverage
The hook cannot cancel the write, and it does not fire when entries are removed. Listen to
OnServerUserRemovefor deletes.
Build a plugin with OnServerUserSet
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 OnServerUserSet
Generate