How to make a Rust admin command logger plugin (Oxide)
Records every console command executed by a player with admin rights into an oxide/data file, optionally echoes it to the server console, and shows the last entries with /adminlog. It hooks OnServerCommand.
On a busy server you cannot tell who ran a console command from F1. Rust keeps no record, and once the command has run there is no trace of the player behind it. This plugin hooks OnServerCommand, which Oxide calls with the ConsoleSystem.Arg when a player runs a console command, and checks whether the calling player has the admin flag and arrived through a real connection. Matching commands are appended to an in-memory list with the player name, SteamID, the full command line and a UTC timestamp, and the list is written to oxide/data/AdminCommandLogger.json on a timer and on unload. The list is capped by Max entries kept so the file never grows without limit, and each entry can be echoed to the server console. Players with admincommandlogger.view run /adminlog to read the most recent entries in chat.
The plugin code
using UnityEngine;
using System.Linq;
using System;
using System.Collections.Generic;
using System.Text;
using Newtonsoft.Json;
using Oxide.Core;
namespace Oxide.Plugins
{
[Info("Admin Command Logger", "RustPluginGenerator", "1.0.0")]
[Description("Logs console commands run by admin players to a data file with name, SteamID, command and time.")]
public class AdminCommandLogger : RustPlugin
{
private const string PermView = "admincommandlogger.view";
private const string PermClear = "admincommandlogger.clear";
private const string DataFileName = "AdminCommandLogger";
private PluginConfig config;
private List<LogEntry> entries = new List<LogEntry>();
private Timer saveTimer;
private bool dirty;
private class PluginConfig
{
[JsonProperty("Max entries kept")]
public int MaxEntries = 500;
[JsonProperty("Save interval seconds")]
public int SaveIntervalSeconds = 60;
[JsonProperty("Echo to console")]
public bool EchoToConsole = true;
[JsonProperty("Default lines shown by /adminlog")]
public int DefaultShowLines = 10;
[JsonProperty("Maximum lines shown by /adminlog")]
public int MaxShowLines = 25;
[JsonProperty("Ignored commands")]
public List<string> IgnoredCommands = new List<string>
{
"global.kill",
"global.respawn"
};
}
private class LogEntry
{
[JsonProperty("PlayerName")]
public string PlayerName;
[JsonProperty("SteamId")]
public string SteamId;
[JsonProperty("Command")]
public string Command;
[JsonProperty("Time")]
public string Time;
public LogEntry()
{
PlayerName = string.Empty;
SteamId = string.Empty;
Command = string.Empty;
Time = string.Empty;
}
}
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>
{
["NoEntries"] = "The admin command log is empty.",
["Header"] = "Last {0} admin command(s):",
["Line"] = "{0} | {1} ({2}) | {3}",
["InvalidCount"] = "Usage: /adminlog [count]",
["Cleared"] = "Admin command log cleared.",
["NoPermission"] = "You do not have permission to use this command."
}, 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(PermView, this);
permission.RegisterPermission(PermClear, this);
}
private void OnServerInitialized()
{
entries = Interface.Oxide.DataFileSystem.ReadObject<List<LogEntry>>(DataFileName) ?? new List<LogEntry>();
saveTimer = timer.Every(Math.Max(10, config.SaveIntervalSeconds), SaveData);
}
private void Unload()
{
if (saveTimer != null)
{
saveTimer.Destroy();
saveTimer = null;
}
SaveData();
}
private object OnServerCommand(ConsoleSystem.Arg arg)
{
if (arg == null || arg.cmd == null || arg.Connection == null) return null;
var player = arg.Player();
if (player == null || !player.IsAdmin || player.IsNpc) return null;
string command = arg.cmd.FullName;
if (string.IsNullOrEmpty(command)) return null;
if (config.IgnoredCommands != null && config.IgnoredCommands.Contains(command)) return null;
var builder = new StringBuilder(command);
if (arg.Args != null)
{
for (int i = 0; i < arg.Args.Length; i++)
{
builder.Append(' ').Append(arg.Args[i].ToString());
}
}
var entry = new LogEntry
{
PlayerName = player.displayName,
SteamId = player.UserIDString,
Command = builder.ToString(),
Time = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss")
};
entries.Add(entry);
Trim();
dirty = true;
if (config.EchoToConsole)
{
Puts($"{entry.Time} | {entry.PlayerName} ({entry.SteamId}) | {entry.Command}");
}
return null;
}
private void Trim()
{
int max = Math.Max(1, config.MaxEntries);
while (entries.Count > max)
{
entries.RemoveAt(0);
}
}
private void SaveData()
{
if (!dirty) return;
Interface.Oxide.DataFileSystem.WriteObject(DataFileName, entries);
dirty = false;
}
[ChatCommand("adminlog")]
private void CmdAdminLog(BasePlayer player, string command, string[] args)
{
if (player == null) return;
if (!permission.UserHasPermission(player.UserIDString, PermView))
{
SendReply(player, Msg("NoPermission", player.UserIDString));
return;
}
if (entries.Count == 0)
{
SendReply(player, Msg("NoEntries", player.UserIDString));
return;
}
int count = config.DefaultShowLines;
if (args != null && args.Length > 0)
{
if (!int.TryParse(args[0], out count) || count <= 0)
{
SendReply(player, Msg("InvalidCount", player.UserIDString));
return;
}
}
count = Math.Min(count, Math.Max(1, config.MaxShowLines));
count = Math.Min(count, entries.Count);
SendReply(player, Msg("Header", player.UserIDString, count));
for (int i = entries.Count - 1; i >= entries.Count - count; i--)
{
var e = entries[i];
SendReply(player, Msg("Line", player.UserIDString, e.Time, e.PlayerName, e.SteamId, e.Command));
}
}
[ChatCommand("adminlog.clear")]
private void CmdAdminLogClear(BasePlayer player, string command, string[] args)
{
if (player == null) return;
if (!permission.UserHasPermission(player.UserIDString, PermClear))
{
SendReply(player, Msg("NoPermission", player.UserIDString));
return;
}
entries.Clear();
dirty = true;
SaveData();
SendReply(player, Msg("Cleared", player.UserIDString));
}
}
}Hooks used: OnServerInitialized, OnServerCommand
Config file
Created automatically on first load at oxide/config/AdminCommandLogger.json.
{
"Max entries kept": 500,
"Save interval seconds": 60,
"Echo to console": true,
"Default lines shown by /adminlog": 10,
"Maximum lines shown by /adminlog": 25,
"Ignored commands": [
"global.kill",
"global.respawn"
],
"PlayerName": "",
"SteamId": "",
"Command": "",
"Time": ""
}Commands and permissions
| Command | What it does | Permission |
|---|---|---|
/adminlog [count] | Shows the most recent logged admin commands in chat, 10 by default, capped by the config. | admincommandlogger.view |
/adminlog.clear | Wipes the saved log file and the in-memory copy. | admincommandlogger.clear |
| Permission | Grants |
|---|---|
admincommandlogger.view | Allows running /adminlog to read the recorded entries. |
admincommandlogger.clear | Allows running /adminlog.clear to delete all recorded entries. |
How the code works
Detect the calling player
The hook is
private object OnServerCommand(ConsoleSystem.Arg arg). It returnsnullon every path so the command still executes. First it checksarg == null,arg.cmd == nullandarg.Connection == null; RCON and the server terminal have no connection, so those calls are skipped. Thenarg.Player()produces theBasePlayer, and the method skips the entry when the player is null, is an NPC, orplayer.IsAdminis false. The admin check is what excludes non-admin players using the in-game console.Skip noisy and ignored commands
The command name comes from
arg.cmd.FullName, which is read only after the null guards. A config list namedIgnored commandsholds command names such as the two defaults, and the hook returns early whenconfig.IgnoredCommands.Contains(command)is true. Ignoring the noisiest entries keeps the data file readable and reduces how often the cap trims old lines out.Build the entry
Arguments arrive in
arg.Args, which can be null. AStringBuilderstarts with the command name and appends each argument with a space, so the stored line can be pasted back into the console. A nestedLogEntryclass with[JsonProperty]names storesPlayerName,SteamId,CommandandTime; the timestamp isDateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss"). Those four fields are the content of each record inoxide/data/AdminCommandLogger.json, so a stored line can be read straight from the file.Persist and cap
OnServerInitializedloads the list withInterface.Oxide.DataFileSystem.ReadObject<List<LogEntry>>(DataFileName)and falls back to an empty list when nothing is stored. After each additionTrimremoves entries from the front while the count exceedsMax entries kept, keeping the newest. A flag marks the list dirty and atimer.Everyrunning everyMath.Max(10, Save interval seconds)writes it withWriteObject;Unloaddestroys the timer and saves once more, so a restart does not lose recent lines.Echo to the console
When
Echo to consoleis true, the same formatted line goes out throughPuts. This runs only after the connection, admin and ignore checks passed, so the terminal sees exactly the entries that reach the file. The echo includes the name and SteamID from the sameLogEntryobject, so console output and file contents never differ.The /adminlog commands
[ChatCommand("adminlog")]checkspermission.UserHasPermission(player.UserIDString, PermView)first and answers with theNoPermissionlang key otherwise. The optional argument is parsed withint.TryParse, starts atDefault lines shown by /adminlog, and is clamped toMath.Max(1, Maximum lines shown by /adminlog)and then to the number of stored entries. Entries are sent newest first withSendReply, one per line.[ChatCommand("adminlog.clear")]performs the same check againstadmincommandlogger.clear, empties the list, saves and confirms.
Install it on your server
Save AdminCommandLogger.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/AdminCommandLogger.json(Carbon:carbon/configs/AdminCommandLogger.json). After editing, reload it:oxide.reload AdminCommandLogger c.reload AdminCommandLogger # CarbonGrant the permission
Give it to every player, or swapdefaultfor a group such asvip:oxide.grant group default admincommandlogger.view c.grant group default admincommandlogger.view # Carbon
Variations
Each one opens the generator with a ready prompt you can tweak.
Discord webhook copy
Sends each logged admin command to a Discord channel for live moderation.
Generate this versionPlayer and non-admin filtering
Also log commands from non-admins so you can see players abusing F1 input.
Generate this versionPer-command counters
Tracks how often each command was used today.
Generate this version
FAQ
Does this log commands run from RCON?
No. The hook returns early when arg.Connection is null, which is the case for RCON and the server terminal, so there is no SteamID to record. If you need RCON auditing you would have to log without a player, accepting that the initiating person is unknown.
Why do some admin commands not appear?
The player must be connected, must have player.IsAdmin set, and must not be an NPC. Names listed in Ignored commands are skipped too. Anything that still shows up is a command where arg.Player() returned a real admin with an open connection.
Will this slow down the server?
No measurable impact. OnServerCommand fires only for console commands, not for gameplay, and the data file is written on a timer (at least every 10 seconds, controlled by Save interval seconds) rather than on every command. Max entries kept keeps the list and the file small.
How do I install it on Oxide or Carbon?
Drop AdminCommandLogger.cs into oxide/plugins and load the plugin, then grant admincommandlogger.view to your staff group. Carbon loads the same file from carbon/plugins. Adjust Max entries kept, Save interval seconds and Echo to console in oxide/config/AdminCommandLogger.json and reload.
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 admin tools plugins
- Admin entity removerAdds an admin-only /kill command that raycast-targets the entity you are looking at and removes it, plus /killall <shortprefabname> which asks for aโฆOnServerInitialized
- Admin radarA permission-gated admin tool that lists every player within a configurable radius with distance, compass direction and sleep state, then drawsโฆ
- Admin teleport commandsAdds permission-gated /tp, /tphere and /tppos chat commands that resolve players by partial name and confirm the move in chat.
Last updated Oct 26, 2026.
Customize admin command logger
Customize