How to make a Rust entity cleanup plugin (tool cupboard aware)
Adds a console command and an optional timed job that delete configured entity types by short prefab name unless they are inside a tool cupboard range, then reports the removal count. It uses OnServerInitialized to start the timer and BaseNetworkable.serverEntities to scan.
Stray sleeping bags, campfires, small stashes and boxes pile up on any server that has been running for a while. Vanilla Rust only cleans up entities that decay naturally, and many deployables never do, so the entity count keeps growing and saves get slower. The usual fix is a broad wipe of all entities, which is too blunt and can hit player-built items inside bases.
This plugin takes a more careful approach. It walks the server's network entity list, matches entities against a list of short prefab names from the config, and skips anything standing inside the range of a tool cupboard. Everything else is killed. You can trigger a pass manually from the server console, or let a repeating timer do it. Both output a chat and console summary with how many entities were checked, removed and skipped because they were protected.
The plugin code
using System.Linq;
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("Entity Cleanup", "RustPluginGenerator", "1.0.0")]
[Description("Removes configured entity types that are not protected by a tool cupboard.")]
public class EntityCleanup : RustPlugin
{
private const string PermUse = "entitycleanup.use";
private PluginConfig config;
private Timer cleanupTimer;
private HashSet<string> targetNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
private bool running;
private class PluginConfig
{
[JsonProperty("Auto Cleanup Enabled")]
public bool AutoCleanupEnabled = false;
[JsonProperty("Auto Cleanup Interval (Seconds)")]
public float AutoCleanupInterval = 3600f;
[JsonProperty("Minimum Entities To Start")]
public int MinimumEntitiesToStart = 500;
[JsonProperty("Warn Before Run")]
public bool WarnBeforeRun = true;
[JsonProperty("Warn Seconds Before")]
public float WarnSecondsBefore = 30f;
[JsonProperty("Broadcast to Players")]
public bool BroadcastToPlayers = true;
[JsonProperty("Protect Building Privilege")]
public bool ProtectBuildingPrivilege = true;
[JsonProperty("Require Authorization")]
public bool RequireAuthorization = false;
[JsonProperty("Skip Entities Owned By Players")]
public bool SkipOwnedEntities = false;
[JsonProperty("Entity Short Prefab Names")]
public List<string> EntityShortPrefabNames = new List<string>
{
"sleepingbag",
"campfire",
"stash.small",
"box.wooden",
"lantern.deployed"
};
}
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>
{
["Removed"] = "Cleanup removed {0} entities ({1} checked, {2} protected). Took {3} ms.",
["Skipped"] = "Cleanup skipped: only {0} entities online, minimum is {1}.",
["Warn"] = "Entity cleanup starts in {0} seconds.",
["Running"] = "A cleanup pass is already running.",
["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(PermUse, this);
}
private void OnServerInitialized()
{
RebuildNameSet();
if (!config.AutoCleanupEnabled) return;
float interval = Mathf.Max(60f, config.AutoCleanupInterval);
cleanupTimer = timer.Repeat(interval, 0, () =>
{
if (config.WarnBeforeRun && config.BroadcastToPlayers)
Server.Broadcast(Msg("Warn", null, Mathf.RoundToInt(config.WarnSecondsBefore)));
if (config.WarnBeforeRun)
timer.Once(Mathf.Max(1f, config.WarnSecondsBefore), () => RunCleanup(null));
else
RunCleanup(null);
});
}
private void Unload()
{
cleanupTimer?.Destroy();
cleanupTimer = null;
}
[ConsoleCommand("entitycleanup.run")]
private void CmdRun(ConsoleSystem.Arg arg)
{
var player = arg.Player();
if (player != null && !permission.UserHasPermission(player.UserIDString, PermUse))
{
arg.ReplyWith(Msg("NoPermission", player.UserIDString));
return;
}
RunCleanup(arg);
}
private void RunCleanup(ConsoleSystem.Arg arg)
{
if (running)
{
arg?.ReplyWith(Msg("Running"));
return;
}
int total = 0;
foreach (var entity in BaseNetworkable.serverEntities)
{
if (entity != null) total++;
}
if (total < config.MinimumEntitiesToStart)
{
string skipped = Msg("Skipped", null, total, config.MinimumEntitiesToStart);
arg?.ReplyWith(skipped);
if (config.BroadcastToPlayers) Server.Broadcast(skipped);
return;
}
RebuildNameSet();
running = true;
float started = UnityEngine.Time.realtimeSinceStartup;
int checkedCount = 0;
int removed = 0;
int protectedCount = 0;
foreach (var entry in BaseNetworkable.serverEntities)
{
var entity = entry as BaseNetworkable;
if (entity == null || entity.IsDestroyed) continue;
if (string.IsNullOrEmpty(entity.ShortPrefabName)) continue;
if (!targetNames.Contains(entity.ShortPrefabName)) continue;
checkedCount++;
var owned = entity as BaseEntity;
if (config.SkipOwnedEntities && owned != null && owned.OwnerID != 0) continue;
if (config.ProtectBuildingPrivilege && IsProtected(entity))
{
protectedCount++;
continue;
}
entity.Kill();
removed++;
}
running = false;
int elapsed = Mathf.RoundToInt((UnityEngine.Time.realtimeSinceStartup - started) * 1000f);
string summary = Msg("Removed", null, removed, checkedCount, protectedCount, elapsed);
Puts(summary);
arg?.ReplyWith(summary);
if (config.BroadcastToPlayers && arg == null) Server.Broadcast(summary);
}
private bool IsProtected(BaseNetworkable entity)
{
var entityBase = entity as BaseEntity;
if (entityBase == null) return false;
var cupboard = entityBase.GetBuildingPrivilege();
if (cupboard == null) return false;
if (!config.RequireAuthorization) return true;
return cupboard.OwnerID == entityBase.OwnerID || cupboard.IsAuthed(entityBase as BasePlayer);
}
private void RebuildNameSet()
{
targetNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
if (config.EntityShortPrefabNames == null) return;
foreach (var name in config.EntityShortPrefabNames)
{
if (!string.IsNullOrEmpty(name)) targetNames.Add(name.Trim());
}
}
}
}Hooks used: OnServerInitialized
Config file
Created automatically on first load at oxide/config/EntityCleanup.json.
{
"Auto Cleanup Enabled": false,
"Auto Cleanup Interval (Seconds)": 3600,
"Minimum Entities To Start": 500,
"Warn Before Run": true,
"Warn Seconds Before": 30,
"Broadcast to Players": true,
"Protect Building Privilege": true,
"Require Authorization": false,
"Skip Entities Owned By Players": false,
"Entity Short Prefab Names": [
"sleepingbag",
"campfire",
"stash.small",
"box.wooden",
"lantern.deployed"
]
}Commands and permissions
| Command | What it does | Permission |
|---|---|---|
entitycleanup.run | Runs a cleanup pass immediately and prints how many entities were removed and how many were skipped near a tool cupboard. | entitycleanup.use |
| Permission | Grants |
|---|---|
entitycleanup.use | Grants access to the entitycleanup.run console command when the caller is not a server admin. |
How the code works
Configuring which entities are targeted
The config holds a
Entity Short Prefab Nameslist. Each entry is compared case-insensitively against theShortPrefabNameof aBaseNetworkablefromBaseNetworkable.serverEntities. Short prefab names are the asset names without the path or.prefabsuffix, for examplesleepingbag,campfire,box.woodenorstash.small. If a name is misspelled the plugin simply matches nothing, so check the list against what actually spawns on your server before enabling the timer.Scanning the network entity list safely
The scan iterates
BaseNetworkable.serverEntities, casts each entry withas BaseNetworkable, and skips anything null, already destroyed (IsDestroyed), or with an emptyShortPrefabName. It then checks the prefab name against aHashSet<string>built from the config so the comparison does not allocate in the loop. This runs on the main thread, which is required becauseKill()must not be called from a background thread.Skipping entities inside tool cupboard range
Protection uses
GetBuildingPrivilege()on the entity. When the entity is inside a building privilege area, Rust returns theBuildingPrivlidgecovering it. The config has two switches:Protect Building Privilegeturns the check on, andRequire Authorizationdecides whether the entity is skipped only when itsOwnerIDis authorized on that cupboard. With authorization required, a stray bag next to a cupboard that has no matching owner is still removed.The console command and permission
entitycleanup.runis a[ConsoleCommand]method takingConsoleSystem.Arg. Whenarg.Player()is null the command came from the server console or RCON and runs without a permission check; otherwisepermission.UserHasPermissionis checked againstentitycleanup.use. Results go back througharg.ReplyWith, so RCON clients see the summary too. Chat output to online admins is optional and controlled byBroadcast to Players.The timed job
When
Auto Cleanup Enabledis true,OnServerInitializedstarts atimer.Repeatwith the configured interval in seconds. The timer reference is stored in a field and destroyed inUnload.Minimum Entities To Startavoids pointless scans on a fresh wipe: if the total network entity count is below that value the pass is skipped.Warn Before Runsends a chat message a few seconds ahead so players can finish what they are doing.Reporting and protecting player deployables
Every pass records checked, removed, skipped and elapsed milliseconds and sends the summary through the language file, so you can translate the output.
Skip Entities Owned By Playerskeeps deployables with a non-zeroOwnerIDalive even outside cupboard range, which is a good default if you only want to catch truly abandoned items. Set it to false when you want a stricter cleanup.
Install it on your server
Save EntityCleanup.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/EntityCleanup.json(Carbon:carbon/configs/EntityCleanup.json). After editing, reload it:oxide.reload EntityCleanup c.reload EntityCleanup # CarbonGrant the permission
Give it to every player, or swapdefaultfor a group such asvip:oxide.grant group default entitycleanup.use c.grant group default entitycleanup.use # Carbon
Variations
Each one opens the generator with a ready prompt you can tweak.
Dry run mode
Log what would be removed without actually killing anything, useful before the first live pass.
Generate this versionPer-type age limit
Only remove entities older than a configured number of minutes.
Generate this versionScheduled warning plus countdown
Announce the cleanup with a countdown and let players opt out of the message.
Generate this version
FAQ
Where do I find the short prefab name for an entity?
Spawn the entity and read ShortPrefabName on the BaseNetworkable. Several admin mods and the server's own entity dumps print it. Common ones: sleepingbag, campfire, box.wooden, stash.small, lantern.deployed. Names are case-insensitive in the config but the casing must otherwise match, including dots.
Will this remove entities inside player bases?
Not when Protect Building Privilege is true and the base has a tool cupboard. Any entity whose GetBuildingPrivilege() returns a cupboard is skipped, or skipped only if authorized depending on Require Authorization. Bases without a cupboard, or entities far from one, are eligible for removal.
Does the cleanup cause lag?
It runs on the main thread and touches every network entity once. On a large server that is a short spike, usually well under a second for tens of thousands of entities, since killing is deferred by the game. Run it during low population hours or on a longer interval if you see hitches.
How do I install it on Oxide or Carbon?
Drop the compiled .cs file into oxide/plugins on Oxide or carbon/plugins on Carbon and let it compile. Edit oxide/config/EntityCleanup.json to set the prefab list, then reload the plugin. Carbon loads the same Oxide plugin file unchanged.
Why did my pass remove nothing?
Check that Auto Cleanup Enabled and the manual command both build the same list, that Minimum Entities To Start is below the current entity count, and that Skip Entities Owned By Players is not hiding everything. Also verify the prefab names, a single typo removes that type from the match list.
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 command loggerRecords every console command executed by a player with admin rights into an oxide/data file, optionally echoes it to the server console, and showsโฆOnServerInitializedOnServerCommand
- 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โฆ
Last updated Oct 26, 2026.
Customize entity cleanup
Customize