Skip to content
RustPluginGenerator

OnUserPermissionRevoked

OnUserPermissionRevoked is called after Oxide removes a single permission from a user's stored permission list, whether the change came from a plugin calling permission.RevokeUserPermission or from a console command.

Signature
void OnUserPermissionRevoked(string playerId, string permission)

Parameters

Hook parameters
ParameterTypeDescription
playerIdstringThe Steam ID of the user whose permission was removed, as a string. Use it with permission.UserHasPermission or BasePlayer.FindByID when you need the player object.
permissionstringThe full permission string that was removed, e.g. 'mytool.vip' or 'vipkit.daily'. Compare it against your own registered permission names.

Return value

Return values are ignored for OnUserPermissionRevoked. Returning null or any other value has no effect on the revoke operation. The permission has already been removed before the hook is called.

How it works

The hook fires inside Oxide's Permission.RevokeUserPermission, right after the permission string has been removed from the user's record. It fires only when the removal actually changed something: if the user did not have that permission, Oxide returns early and the hook is skipped. It fires for the specific player and the specific permission, not for group permissions.

The hook is not tied to a connected client. It runs whenever a permission is revoked on a user record that Oxide has loaded, including revocations issued while the player is offline or sleeping. The playerId is the string Steam ID; the permission is the exact name that was passed to RevokeUserPermission. Both can be used to filter before doing anything expensive.

Because this hook is invoked by Oxide rather than a patched game method, it always arrives with the same two strings. There is no narrower parameter type to declare here, so check the permission value against your own registered names instead. Administrators can also strip permissions with the oxide.usergroup or revoke console commands, which route through the same method. Do not assume a player object exists; look it up or keep state keyed by user ID.

Example plugin: Track and warn players when a permission is revoked

PermissionRevokeWatcher.cs
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
using UnityEngine;

namespace Oxide.Plugins
{
    [Info("Permission Revoke Watcher", "RustPluginGenerator", "1.0.0")]
    [Description("Tracks revocations of a monitored permission and reports them to players and admins")]
    public class PermissionRevokeWatcher : RustPlugin
    {
        private const string PermUse = "permissionrevokewatcher.use";

        private PluginConfig config;
        private WatcherData data;
        private Timer saveTimer;

        private class PluginConfig
        {
            [JsonProperty("Monitored permission")]
            public string MonitoredPermission = "vip.access";

            [JsonProperty("Notify player on revoke")]
            public bool NotifyPlayer = true;

            [JsonProperty("Announce to server")]
            public bool Announce = false;
        }

        private class WatcherData
        {
            [JsonProperty("Revocations")]
            public Dictionary<string, RevokeRecord> Revocations = new Dictionary<string, RevokeRecord>();
        }

        private class RevokeRecord
        {
            [JsonProperty("Count")]
            public int Count;

            [JsonProperty("Last Revoked Utc")]
            public string LastRevokedUtc;
        }

        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>
            {
                ["Revoked"] = "You no longer have the '{0}' permission.",
                ["Announce"] = "{0} lost the '{0}' permission.",
                ["Report"] = "Revocations of '{0}': {1}",
                ["EntryLine"] = "User {0} - {1} time(s), last {2}",
                ["ReportEmpty"] = "No revocations recorded for '{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);
        }

        private void OnServerInitialized()
        {
            data = Interface.Oxide.DataFileSystem.ReadObject<WatcherData>(Name) ?? new WatcherData();
            saveTimer = timer.Every(120f, SaveData);
        }

        private void Unload()
        {
            saveTimer?.Destroy();
            SaveData();
        }

        private void SaveData()
        {
            if (data != null)
                Interface.Oxide.DataFileSystem.WriteObject(Name, data);
        }

        private void OnUserPermissionRevoked(string playerId, string permission)
        {
            if (string.IsNullOrEmpty(playerId) || string.IsNullOrEmpty(permission))
                return;

            if (!string.Equals(permission, config.MonitoredPermission, StringComparison.OrdinalIgnoreCase))
                return;

            RevokeRecord record;
            if (!data.Revocations.TryGetValue(playerId, out record))
            {
                record = new RevokeRecord();
                data.Revocations[playerId] = record;
            }

            record.Count++;
            record.LastRevokedUtc = DateTime.UtcNow.ToString("u");

            var player = BasePlayer.FindByID(Convert.ToUInt64(playerId));
            if (player == null)
                return;

            if (config.NotifyPlayer)
                player.ChatMessage(Msg("Revoked", player.UserIDString, permission));

            if (config.Announce)
                Server.Broadcast(string.Format(lang.GetMessage("Announce", this), player.displayName, permission));
        }

        [ConsoleCommand("permrevoked.report")]
        private void CmdReport(ConsoleSystem.Arg arg)
        {
            var player = arg.Player();
            if (player != null && !permission.UserHasPermission(player.UserIDString, PermUse))
            {
                arg.ReplyWith("You do not have permission to use this command.");
                return;
            }

            if (data.Revocations.Count == 0)
            {
                arg.ReplyWith(Msg("ReportEmpty", null, config.MonitoredPermission));
                return;
            }

            arg.ReplyWith(Msg("Report", null, config.MonitoredPermission, data.Revocations.Count));
            foreach (var entry in data.Revocations)
                arg.ReplyWith(Msg("EntryLine", null, entry.Key, entry.Value.Count, entry.Value.LastRevokedUtc));
        }
    }
}

The plugin registers a permission in Init and logs each revocation of that permission. OnUserPermissionRevoked compares the removed permission string against the registered one, so unrelated changes from other plugins are ignored. When it matches, the user ID is recorded with a UTC timestamp and the number of revocations is incremented. If a connected player is found, they receive a localized chat message and the revocation is announced to the whole server. A console command prints the stored records for the requesting admin. Data is saved to oxide/data on unload and reloaded on startup through DataFileSystem. There is no logging inside the hook that fires on every revoke until an admin requests the report.

Common use cases

  • Audit permission changes

    Record which user IDs lost which permission and when, then expose it through an admin console command. Useful when several plugins manage permissions automatically.

  • Revoke dependent perks

    When a core permission like 'vip.access' is removed, clean up related stored data such as kit allowances or wallet balances and inform the player their status changed.

  • Trigger instant re-evaluation

    Push a UI or gameplay refresh for online players who just lost a permission, so gated features disappear immediately instead of on next reconnect.

Pitfalls

  • Manual permission grants

    Any plugin or admin running a revoke command passes through this hook, including scripts that toggle your permission temporarily. Filter on your own permission strings and be careful not to fight automated toggles.

  • Player may be offline

    The hook fires regardless of whether the user is connected. BasePlayer.FindByID can return null; always handle that case and store results by user ID.

  • No partial removal signal

    The hook only fires when the permission existed on that user. A revoke of a permission the user did not have silently does nothing, so do not rely on this hook to confirm a failed removal.

Build a plugin with OnUserPermissionRevoked

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

Prompt: Audit permission changes using the OnUserPermissionRevoked 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 OnUserPermissionRevoked

Generate