OnUserUnbanned
OnUserUnbanned is called when a banned Steam ID is removed from the server's ban list, either through the `banid` removal path, the server console, or a plugin calling the unban API.
void OnUserUnbanned(string playerName, string steamId, string address)Parameters
| Parameter | Type | Description |
|---|---|---|
playerName | string | Display name of the unbanned player, looked up from Covalence's player cache. Falls back to "Unnamed" when the player is not in the cache (for example after a server restart). |
steamId | string | The Steam ID of the unbanned player as a string. Use it as the key for stored bans, permission checks and lookups. |
address | string | Last known IP address of the player as a string, or "0" when the cache has no address. Treat it as a hint only; players change IPs. |
Return value
The hook is called with its return value ignored, and the original method returns void, so returning a value has no effect on the unban. Return nothing (or null) and use the hook only for notification or bookkeeping.
How it works
This hook fires from Oxide's RustCore.OnServerUserRemove, the callback Facepunch's server runs when an entry is removed from ServerUsers.users while it belongs to the Banned group. It only fires for actual unbans: removing a non-banned user or removing an admin/owner entry does not trigger it. The player does not need to be connected; the values come from Covalence's player cache, so playerName is "Unnamed" and address is "0" when the player was never seen since the last server start.
All three parameters are strings, so you cannot narrow the hook by declaring a more specific parameter type. The first useful signal is steamId, which is the same value you should use as a key in your own ban records; address is a weak secondary identifier because it changes with ISP reassignment and VPNs.
Because the call happens server-side with no player entity involved, do not expect to message or teleport anyone from inside the hook. Pair it with OnUserBanned if you keep a mirror of the ban list, and with OnPlayerConnected when you want to notify the player once they actually rejoin.
Example plugin: Sync unban records with an oxide data file
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
namespace Oxide.Plugins
{
[Info("Unban Audit Log", "RustPluginGenerator", "1.0.0")]
[Description("Tracks bans and lifts them in a data file when a Steam ID is unbanned.")]
public class UnbanAuditLog : RustPlugin
{
private const string PermAdmin = "unbanauditlog.admin";
private PluginConfig config;
private AuditData data;
private class PluginConfig
{
[JsonProperty("Save after every change")]
public bool SaveAfterEveryChange { get; set; } = true;
[JsonProperty("Message on connect when a lifted ban exists")]
public bool NotifyOnConnect { get; set; } = true;
}
private class AuditData
{
[JsonProperty("Active bans")]
public Dictionary<string, BanRecord> ActiveBans { get; set; } = new Dictionary<string, BanRecord>();
[JsonProperty("Lifted bans")]
public List<BanRecord> LiftedBans { get; set; } = new List<BanRecord>();
}
private class BanRecord
{
[JsonProperty("Name")]
public string Name { get; set; }
[JsonProperty("Steam ID")]
public string SteamId { get; set; }
[JsonProperty("Address")]
public string Address { get; set; }
[JsonProperty("Banned at")]
public DateTime BannedAt { get; set; }
[JsonProperty("Unbanned at")]
public DateTime? UnbannedAt { 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>
{
["Lifted"] = "The ban on {0} ({1}) was lifted at {2}.",
["WelcomeBack"] = "Your ban on this server has been lifted. Welcome back.",
["NoPermission"] = "You do not have permission to use this command.",
["Report"] = "Tracked bans: {0} active, {1} lifted.",
["ReportLine"] = "{0} ({1}) banned {2}{3}"
}, 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);
LoadData();
}
private void LoadData()
{
try
{
data = Interface.Oxide.DataFileSystem.ReadObject<AuditData>(Name);
}
catch
{
data = null;
}
if (data == null) data = new AuditData();
if (data.ActiveBans == null) data.ActiveBans = new Dictionary<string, BanRecord>();
if (data.LiftedBans == null) data.LiftedBans = new List<BanRecord>();
}
private void SaveData()
{
if (config.SaveAfterEveryChange) Interface.Oxide.DataFileSystem.WriteObject(Name, data);
}
private void OnUserBanned(string playerName, string playerId, string address, string reason, ulong duration, string bannedBy)
{
if (string.IsNullOrEmpty(playerId)) return;
data.ActiveBans[playerId] = new BanRecord
{
Name = string.IsNullOrEmpty(playerName) ? "Unnamed" : playerName,
SteamId = playerId,
Address = string.IsNullOrEmpty(address) ? "0" : address,
BannedAt = DateTime.UtcNow
};
SaveData();
}
private void OnUserUnbanned(string playerName, string steamId, string address)
{
if (string.IsNullOrEmpty(steamId)) return;
BanRecord record;
if (!data.ActiveBans.TryGetValue(steamId, out record))
{
record = new BanRecord
{
Name = string.IsNullOrEmpty(playerName) ? "Unnamed" : playerName,
SteamId = steamId,
Address = string.IsNullOrEmpty(address) ? "0" : address,
BannedAt = DateTime.UtcNow
};
}
record.UnbannedAt = DateTime.UtcNow;
data.ActiveBans.Remove(steamId);
data.LiftedBans.Add(record);
if (data.LiftedBans.Count > 500) data.LiftedBans.RemoveRange(0, data.LiftedBans.Count - 500);
SaveData();
LogToFile("unbans", string.Format("{0} {1} {2} lifted {3}", record.UnbannedAt.Value.ToString("u"), record.Name, record.SteamId, record.BannedAt.ToString("u")), this);
}
private void OnPlayerConnected(BasePlayer player)
{
if (!config.NotifyOnConnect || player == null) return;
if (!HasLiftedBan(player.UserIDString)) return;
player.ChatMessage(Msg("WelcomeBack", player.UserIDString));
}
private bool HasLiftedBan(string steamId)
{
for (int i = data.LiftedBans.Count - 1; i >= 0; i--)
{
if (data.LiftedBans[i].SteamId == steamId) return true;
}
return false;
}
[ChatCommand("unbans")]
private void CmdUnbans(BasePlayer player, string command, string[] args)
{
if (player == null) return;
if (!permission.UserHasPermission(player.UserIDString, PermAdmin))
{
player.ChatMessage(Msg("NoPermission", player.UserIDString));
return;
}
player.ChatMessage(Msg("Report", player.UserIDString, data.ActiveBans.Count, data.LiftedBans.Count));
int shown = 0;
foreach (var record in data.ActiveBans.Values)
{
string suffix = record.UnbannedAt.HasValue ? " (lifted)" : "";
player.ChatMessage(Msg("ReportLine", player.UserIDString, record.Name, record.SteamId, record.BannedAt.ToString("u"), suffix));
if (++shown >= 10) break;
}
}
private void Unload()
{
Interface.Oxide.DataFileSystem.WriteObject(Name, data);
}
}
}The plugin keeps a small ban mirror in oxide/data/UnbanAuditLog.json, written by OnUserBanned with the player name, Steam ID, address and a timestamp. When the server removes a ban, OnUserUnbanned looks the record up by Steam ID, marks it as lifted, stores the unbanned time, and removes the entry from the active list so the audit trail stays accurate. Admins can run /unbans to see how many tracked bans are still in place, and a permission (unbanauditlog.admin) controls access. Data is saved after every change and again in Unload, which is the only place timers and UI would need cleanup here.
Common use cases
Keep an external ban list in sync
If your server mirrors bans into a data file or a custom table, use this hook to flip the matching record to lifted instead of polling the server's ban list.
Audit admin behavior
Record the exact time a ban was removed so staff disputes can be resolved from a log rather than from memory.
Re-apply plugin-specific penalties
When a player is unbanned from the game level but still has an active plugin-level restriction, use this hook to decide whether to clear or keep that restriction.
Pitfalls
Only real unbans fire it
The call is guarded by a check that the user exists and belongs to the Banned group, so scripted removals of non-banned users or admin entries produce no hook call.
Names and addresses may be placeholders
When the player is not in the Covalence cache the hook receives "Unnamed" and "0" as the name and address. Match on
steamIdonly when you need a reliable identifier.No player entity to reference
There is no
BasePlayerhere, so calls that require a connected player (chat, CUI, teleport) will fail or do nothing. Notify the player on their nextOnPlayerConnectedinstead.
Build a plugin with OnUserUnbanned
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 OnUserUnbanned
Generate