How to make a Rust admin menu plugin (CUI panel)
Builds a CUI admin panel that opens with /am and renders one button per action defined in the config, each button running a console command through a plugin console command. No gameplay hooks are required.
Admins on a busy server end up typing F1 console commands just to switch time of day or clear the sky. A single permission-gated CUI panel keeps those actions one click away and lets you change which actions exist without recompiling anything.
This plugin registers an adminmenu.use permission, reads a list of actions from its config (label, console command, optional confirm flag), and draws one button per action plus a close button. Clicking a button calls the plugin console command adminmenu.run <index>, which is dispatched on behalf of the admin. Because the console command requires arg.Player() and rechecks the permission, a modified client cannot queue those commands from the server console or bypass the check. Actions marked Confirm in the config require a second click on the same button within ten seconds before the command runs.
The plugin code
using System.Linq;
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
using Oxide.Core.Plugins;
using Oxide.Game.Rust.Cui;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("Admin Menu", "RustPluginGenerator", "1.0.0")]
[Description("A permission-gated CUI admin panel whose buttons run config-defined console commands.")]
public class AdminMenu : RustPlugin
{
private const string PermUse = "adminmenu.use";
private const string PanelName = "AdminMenu.Panel";
private const float ConfirmWindow = 10f;
private PluginConfig config;
private readonly Dictionary<ulong, Pending> pending = new Dictionary<ulong, Pending>();
private class MenuAction
{
[JsonProperty("Label")]
public string Label { get; set; } = "";
[JsonProperty("Command")]
public string Command { get; set; } = "";
[JsonProperty("Confirm")]
public bool Confirm { get; set; }
}
private class PluginConfig
{
[JsonProperty("Panel Title")]
public string Title { get; set; } = "ADMIN MENU";
[JsonProperty("Actions")]
public List<MenuAction> Actions { get; set; } = new List<MenuAction>
{
new MenuAction { Label = "Set Time: Day", Command = "env.time 12", Confirm = false },
new MenuAction { Label = "Set Time: Night", Command = "env.time 0", Confirm = false },
new MenuAction { Label = "Kill All Patrol Helicopters", Command = "killall patrolhelicopter", Confirm = true }
};
}
private class Pending
{
public int Index;
public float Time;
}
protected override void LoadDefaultConfig() => config = new PluginConfig();
protected override void LoadConfig()
{
base.LoadConfig();
try
{
config = Config.ReadObject<PluginConfig>();
if (config == null) throw new Exception();
if (config.Actions == null) config.Actions = new List<MenuAction>();
}
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 the admin menu.",
["NoAction"] = "That action does not exist.",
["NoCommand"] = "This action has no command configured.",
["ConfirmPrompt"] = "Click the same button again within {0} seconds to confirm: {1}",
["Ran"] = "Executed: {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 Unload()
{
pending.Clear();
foreach (var player in BasePlayer.activePlayerList)
{
if (player == null) continue;
CuiHelper.DestroyUi(player, PanelName);
}
}
[ChatCommand("am")]
private void CmdAm(BasePlayer player, string command, string[] args)
{
if (player == null) return;
if (!permission.UserHasPermission(player.UserIDString, PermUse))
{
SendReply(player, Msg("NoPermission", player.UserIDString));
return;
}
DrawMenu(player);
}
[ConsoleCommand("adminmenu.close")]
private void CcmdClose(ConsoleSystem.Arg arg)
{
var player = arg.Player();
if (player == null) return;
if (!permission.UserHasPermission(player.UserIDString, PermUse)) return;
pending.Remove(player.userID.Get());
CuiHelper.DestroyUi(player, PanelName);
}
[ConsoleCommand("adminmenu.run")]
private void CcmdRun(ConsoleSystem.Arg arg)
{
var player = arg.Player();
if (player == null) return;
if (!permission.UserHasPermission(player.UserIDString, PermUse))
{
SendReply(player, Msg("NoPermission", player.UserIDString));
return;
}
int index = arg.GetInt(0, -1);
if (index < 0 || index >= config.Actions.Count)
{
SendReply(player, Msg("NoAction", player.UserIDString));
return;
}
var action = config.Actions[index];
if (action == null || string.IsNullOrEmpty(action.Command))
{
SendReply(player, Msg("NoCommand", player.UserIDString));
return;
}
ulong id = player.userID.Get();
if (action.Confirm)
{
float now = Time.realtimeSinceStartup;
Pending existing;
if (pending.TryGetValue(id, out existing) && existing.Index == index && now - existing.Time <= ConfirmWindow)
{
pending.Remove(id);
Execute(player, action);
return;
}
pending[id] = new Pending { Index = index, Time = now };
SendReply(player, Msg("ConfirmPrompt", player.UserIDString, (int)ConfirmWindow, action.Label));
return;
}
Execute(player, action);
}
private void Execute(BasePlayer player, MenuAction action)
{
pending.Remove(player.userID.Get());
Server.Command(action.Command);
SendReply(player, Msg("Ran", player.UserIDString, action.Label));
CuiHelper.DestroyUi(player, PanelName);
}
private void DrawMenu(BasePlayer player)
{
CuiHelper.DestroyUi(player, PanelName);
var container = new CuiElementContainer();
const float width = 380f;
const float rowHeight = 32f;
const float padding = 10f;
int count = Mathf.Max(config.Actions.Count, 1);
float height = 44f + count * (rowHeight + padding) + padding + rowHeight + padding;
string anchorMin = "0.5 " + (0.5f - height / 1080f * 0.5f);
string anchorMax = "0.5 " + (0.5f + height / 1080f * 0.5f);
container.Add(new CuiPanel
{
Image = { Color = "0.08 0.08 0.08 0.95" },
RectTransform = { AnchorMin = anchorMin, AnchorMax = anchorMax, OffsetMin = (-width / 2) + " 0", OffsetMax = (width / 2) + " 0" },
CursorEnabled = true
}, "Overlay", PanelName);
container.Add(new CuiLabel
{
Text = { Text = config.Title ?? "ADMIN MENU", FontSize = 18, Align = TextAnchor.MiddleCenter, Color = "1 1 1 1" },
RectTransform = { AnchorMin = "0 1", AnchorMax = "1 1", OffsetMin = "0 -44", OffsetMax = "0 -8" }
}, PanelName);
float top = -44f - padding;
for (int i = 0; i < config.Actions.Count; i++)
{
var action = config.Actions[i];
string label = action != null ? action.Label : "";
container.Add(new CuiButton
{
Button = { Command = "adminmenu.run " + i, Color = "0.25 0.35 0.55 0.95" },
RectTransform = { AnchorMin = "0 1", AnchorMax = "1 1", OffsetMin = padding + " " + (top - rowHeight), OffsetMax = (-padding) + " " + top },
Text = { Text = label, FontSize = 14, Align = TextAnchor.MiddleCenter, Color = "1 1 1 1" }
}, PanelName);
top -= rowHeight + padding;
}
container.Add(new CuiButton
{
Button = { Command = "adminmenu.close", Color = "0.55 0.2 0.2 0.95" },
RectTransform = { AnchorMin = "0 0", AnchorMax = "1 0", OffsetMin = padding + " " + padding, OffsetMax = (-padding) + " " + (padding + rowHeight) },
Text = { Text = "CLOSE", FontSize = 14, Align = TextAnchor.MiddleCenter, Color = "1 1 1 1" }
}, PanelName);
CuiHelper.AddUi(player, container);
}
}
}Config file
Created automatically on first load at oxide/config/AdminMenu.json.
{
"Panel Title": "ADMIN MENU",
"Actions": [
{
"Label": "Set Time: Day",
"Command": "env.time 12",
"Confirm": false
},
{
"Label": "Set Time: Night",
"Command": "env.time 0",
"Confirm": false
},
{
"Label": "Kill All Patrol Helicopters",
"Command": "killall patrolhelicopter",
"Confirm": true
}
]
}Commands and permissions
| Command | What it does | Permission |
|---|---|---|
/am | Opens the admin panel. | adminmenu.use |
adminmenu.run <index> | Runs the configured action at the given index for the calling player. Called by the CUI buttons. | Everyone |
adminmenu.close | Closes the panel for the calling player. Called by the close button. | Everyone |
| Permission | Grants |
|---|---|
adminmenu.use | Grants access to /am and the action buttons. |
How the code works
Config drives the buttons
The nested
PluginConfigclass holds aPanel Titlestring and aList<MenuAction>namedActions. EachMenuActionhasLabel,Command, andConfirm. On load,LoadDefaultConfigfills in three actions: set time to day (env.time 12), set time to night (env.time 0), and kill all Patrol Helicopters viakillall patrolhelicopter. Editingoxide/config/AdminMenu.jsonchanges the panel, so new actions do not need code changes.LoadConfigfalls back to defaults if the file cannot be parsed.Rendering the panel
DrawMenu(BasePlayer player)builds aCuiElementContainer, adds a centredCuiPanel, then loops overconfig.Actions. Each button'sCommandisadminmenu.run <i>, which Oxide dispatches to the console command below.CuiHelper.DestroyUi(player, PanelName)runs beforeCuiHelper.AddUiso reopening the panel does not stack duplicate elements. The label usesTextAnchor.MiddleCenterand a fixed row height constant so the panel size matches the action count.Running the action
The
[ConsoleCommand("adminmenu.run")]method readsarg.Player(); any invocation from the server console or RCON returns without executing, so panel actions cannot be queued from outside. It rechecksadminmenu.use, reads the index viaarg.GetInt(0, -1), and validates it againstconfig.Actions.Count. If the entry hasConfirmset, it stores the player's Steam ID and a timestamp inpendingand sends a prompt instead of running the command.Confirmations without a modal
When a
Confirmaction is clicked, the plugin savesTime.realtimeSinceStartupunder the player's Steam ID in thependingdictionary and callsSendReplywith theConfirmPromptmessage. A second click on the same index within ten seconds (ConfirmWindow) passes the check and callsExecute. Clicking any other action replaces the stored entry, so a slow second click cannot confirm the wrong action.Executing on the server
Execute(BasePlayer player, MenuAction action)clears the player's pending entry, callsServer.Command(action.Command), sends theRanmessage viaSendReply, and destroys the panel withCuiHelper.DestroyUi. The command runs with no player context, exactly like typing it in the F1 console.CuiHelper.DestroyUiis also called by theadminmenu.closeconsole command wired to the CLOSE button.Cleanup
UnloaditeratesBasePlayer.activePlayerListand callsCuiHelper.DestroyUi(player, PanelName)for each, so a plugin reload does not leave orphan panels on screen. Thependingdictionary is cleared on unload, and nothing is written tooxide/data. The config is saved on load, so any admin editingActionssees changes after/reload AdminMenu.
Install it on your server
Save AdminMenu.cs
Keep the file name exactly as the class name. Oxide matches them.Upload it to the plugins folder
Oxide:oxide/plugins/. Carbon:carbon/plugins/. The server compiles and loads it within a few seconds, no restart needed.Edit the config if you want
On first load the default config is written tooxide/config/AdminMenu.json(Carbon:carbon/configs/AdminMenu.json). After editing, reload it:oxide.reload AdminMenu c.reload AdminMenu # CarbonGrant the permission
Give it to every player, or swapdefaultfor a group such asvip:oxide.grant group default adminmenu.use c.grant group default adminmenu.use # Carbon
Variations
Each one opens the generator with a ready prompt you can tweak.
Add a search box
Show only actions whose label contains the typed text.
Generate this versionLog actions
Write every executed action to a log file for accountability.
Generate this versionGroup actions into tabs
Split buttons into categories like Environment, Entities, and Players.
Generate this version
FAQ
Why does clicking a button do nothing?
Check the server console for a permission error. The player needs adminmenu.use, and the console command requires a player context, so a call arriving without one is refused by design. Also confirm the button's index exists in Actions.
Can I add my own actions?
Yes. Add an object to Actions in oxide/config/AdminMenu.json with Label, Command, and Confirm. The command is passed to Server.Command, so use the same text you would type in F1. Reload the plugin so the CUI reads the updated list.
Does this plugin work on Carbon?
Yes. Carbon loads Oxide C# plugins and handles the same CuiHelper, [ChatCommand], [ConsoleCommand], permission, lang, and Server.Command APIs. Drop the same .cs file into carbon/plugins and reload it from the Carbon console.
How do I install it?
Drop AdminMenu.cs into oxide/plugins (or carbon/plugins), wait for the file to compile, then grant the permission with oxide.grant user <steamid> adminmenu.use. /reload AdminMenu re-reads the config after you edit Actions.
Why does the panel close after each action?
Actions execute on the server with no player context, and Rust often closes CUI after a server-side state change. The plugin deliberately closes the panel after running an action so the player is not stuck behind a stale overlay.
Customize it for your server
Change the reward, add a cooldown, restrict it to VIPs. Start from this prompt and edit anything.
Edit the prompt on the next screen. 2 free plugins a month, no card.
More ui plugins
- Announcement bannerAdds an /announce chat command that draws a CUI banner at the top of every connected player's screen for a configurable duration, plus timedโฆOnServerInitializedOnPlayerConnected
- Command shortcut menuAdds a /menu chat command that opens a CUI grid of configurable buttons; each button closes the panel and runs its configured chat command for thatโฆ
- Kill feedDraws a CUI kill feed on the right side of every player's screen showing the last 5 player kills with killer, weapon and victim, using OnEntityDeath.OnServerInitializedOnEntityDeathOnPlayerSleepEnded
Last updated Oct 26, 2026.
Customize admin menu
Customize