OnPluginUnloaded
OnPluginUnloaded is called by Oxide after a plugin has been removed from the plugin manager, so other plugins can react when a dependency or tool plugin goes away.
void OnPluginUnloaded(Plugin plugin)Parameters
| Parameter | Type | Description |
|---|---|---|
plugin | Plugin | The Oxide plugin instance that has just been removed from the plugin manager. Use plugin.Name, plugin.Title, plugin.Version or plugin.Author to identify it. |
Return value
The return value is ignored. The hook fires after the plugin has already been removed from the manager, so nothing you return can prevent or delay the unload.
How it works
The hook fires inside OxideMod.UnloadPlugin, after the target plugin is removed from the root plugin manager and only if the removed plugin was still marked as loaded. It runs for manual oxide.reload, oxide.unload, plugin reloads triggered by file changes, and shutdown sequences initiated through the same code path, so it is a reliable signal that the plugin's runtime instance is gone.
The plugin parameter is the Oxide.Core.Plugins.Plugin object itself, not a name string. Read plugin.Name (file name without extension) to compare against a dependency you track, or plugin.Title for anything user-facing. The instance is being torn down, so do not call into it or assume its hooks will still run.
There is no filtering by parameter type here: Oxide always passes the removed plugin instance. Declare Plugin exactly as given. Register this hook in a long-lived plugin (a coordinator, API, or permissions plugin) and re-check soft dependencies with plugins.Find(name) instead of caching references after the event.
Example plugin: Track and report plugins that depend on a companion plugin
using System.Linq;
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
using Oxide.Core.Plugins;
namespace Oxide.Plugins
{
[Info("Dependency Watcher", "RustPluginGenerator", "1.0.0")]
[Description("Watches key companion plugins and records when they are unloaded.")]
public class DependencyWatcher : RustPlugin
{
private const string PermAdmin = "dependencywatcher.admin";
private PluginConfig config;
private readonly Dictionary<string, DateTime> lastUnload = new Dictionary<string, DateTime>(StringComparer.OrdinalIgnoreCase);
private class PluginConfig
{
[JsonProperty("Watched plugin names")]
public List<string> WatchedPlugins = new List<string> { "Economics", "ServerRewards", "PermissionsManager" };
}
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.",
["NoWatched"] = "No plugins are being watched.",
["Header"] = "Watched plugins:",
["Line"] = "{0} - {1}",
["Loaded"] = "loaded",
["UnloadedAt"] = "last unloaded at {0} UTC",
["Cleared"] = "Unload records cleared."
}, 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);
}
private void OnPluginUnloaded(Plugin plugin)
{
if (plugin == null || string.IsNullOrEmpty(plugin.Name)) return;
if (!config.WatchedPlugins.Contains(plugin.Name)) return;
lastUnload[plugin.Name] = DateTime.UtcNow;
foreach (var player in BasePlayer.activePlayerList)
{
if (player != null && permission.UserHasPermission(player.UserIDString, PermAdmin))
player.ChatMessage(Msg("Line", player.UserIDString, plugin.Name, Msg("UnloadedAt", player.UserIDString, DateTime.UtcNow.ToString("HH:mm:ss"))));
}
}
[ChatCommand("depwatch")]
private void CmdDepwatch(BasePlayer player, string command, string[] args)
{
if (player == null) return;
if (!permission.UserHasPermission(player.UserIDString, PermAdmin))
{
player.ChatMessage(Msg("NoPermission", player.UserIDString));
return;
}
if (args.Length > 0 && args[0].Equals("clear", StringComparison.OrdinalIgnoreCase))
{
lastUnload.Clear();
player.ChatMessage(Msg("Cleared", player.UserIDString));
return;
}
if (config.WatchedPlugins.Count == 0)
{
player.ChatMessage(Msg("NoWatched", player.UserIDString));
return;
}
player.ChatMessage(Msg("Header", player.UserIDString));
foreach (var name in config.WatchedPlugins)
{
string state;
DateTime stamp;
if (lastUnload.TryGetValue(name, out stamp))
state = Msg("UnloadedAt", player.UserIDString, stamp.ToString("yyyy-MM-dd HH:mm:ss"));
else
state = Msg("Loaded", player.UserIDString);
player.ChatMessage(Msg("Line", player.UserIDString, name, state));
}
}
}
}The plugin keeps a configurable list of watched plugin names. When any plugin is unloaded, OnPluginUnloaded compares plugin.Name (case-insensitive) against that list and records the event with a UTC timestamp. The admin command depwatch prints the current state of each watched plugin and the last time it went down, and depwatch clear resets the records. Because the check runs in memory only, it adds no cost during normal operation; the config list is the only persisted setting.
Common use cases
Invalidate cached plugin references
If your plugin caches a reference obtained through [PluginReference], clear it when that plugin unloads so later calls fall back to a null check instead of calling into a dead instance.
Notify staff when an economy or permissions plugin drops
Compare plugin.Name against a list of critical dependencies and send an admin-only chat or log warning so the server owner notices before players hit broken commands.
Clean up CUI owned by a removed plugin
When a UI-heavy plugin unloads, destroy any of its leftover panels for active players so stale elements do not block input on the client.
Pitfalls
The plugin is already removed
The hook fires after RootPluginManager.RemovePlugin, so plugins.Find(name) and dependencies on the removed instance will be gone. Do not try to call methods on the passed instance.
Guard against unloading your own plugin
UnloadPlugin calls this hook while removing the target; if your handler runs for your own class name, skip work that relies on your own registered hooks, timers or UI, since they are being torn down too.
Name matching is exact
plugin.Name is the file name without the .cs extension. Matching on Title or Author is fragile because those come from plugin metadata and can differ from the file name.
Build a plugin with OnPluginUnloaded
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 OnPluginUnloaded
Generate