Skip to content
RustPluginGenerator

How to make a Rust admin entity remover plugin (raycast kill and prefab killall)

Adds an admin-only /kill command that raycast-targets the entity you are looking at and removes it, plus /killall <shortprefabname> which asks for a confirmation within 10 seconds before wiping every matching entity. Uses OnServerInitialized to log readiness and no gameplay hooks.

Deleting entities on a live Rust server is usually done one of two ways: spawn a hammer or grenade, or drop into the F1 console and look up entity IDs by hand. Both are slow when a player placed 300 barricades or a bugged lootbox got stuck inside a wall. This plugin gives admins two chat commands driven by the server's own entity registry. /kill takes the admin's eye position and forward direction, raycasts against the world, and kills only the entity under the crosshair. /killall <shortprefabname> builds a list of matching entities from BaseNetworkable.serverEntities (or reuses the last /kill target if you pass last), reports the count, and waits for the admin to type /killall confirm inside a 10 second window. Everything is permission-gated, the confirmation timer is tracked and destroyed in Unload, and every response goes through the language files.

The plugin code

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

namespace Oxide.Plugins
{
    [Info("Admin Entity Remover", "RustPluginGenerator", "1.0.0")]
    [Description("Gives admins raycast /kill and prefab-based /killall entity removal with a confirmation window")]
    public class AdminEntityRemover : RustPlugin
    {
        private const string PermUse = "adminentityremover.use";
        private const int PlayerLayer = 1 << 0;

        private PluginConfig config;
        private readonly Dictionary<ulong, PendingBatch> pending = new Dictionary<ulong, PendingBatch>();

        private class PendingBatch
        {
            public List<BaseNetworkable> Entities = new List<BaseNetworkable>();
            public string Prefab;
            public float ExpiresAt;
            public Timer Timeout;
        }

        private class PluginConfig
        {
            [JsonProperty("Maximum ray distance")]
            public float MaxRayDistance = 10f;

            [JsonProperty("Confirmation window (seconds)")]
            public float ConfirmSeconds = 10f;

            [JsonProperty("Maximum entities per batch")]
            public int MaxBatchSize = 500;

            [JsonProperty("Excluded prefabs")]
            public List<string> ExcludedPrefabs = new List<string> { "player", "santasleepingbag" };
        }

        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.",
                ["NoTarget"] = "No entity found under your crosshair.",
                ["Killed"] = "Removed {0} (id {1}).",
                ["Usage"] = "Usage: /killall <shortprefabname|last> | /killall confirm | /killall cancel",
                ["NoMatches"] = "No entities matched '{0}'.",
                ["Excluded"] = "'{0}' is in the excluded prefabs list.",
                ["AlreadyPending"] = "A batch for '{0}' is already waiting for confirmation. Type /killall confirm.",
                ["BatchReady"] = "{0} entities match '{1}'. Type /killall confirm within {2} seconds to remove them.",
                ["BatchTrimmed"] = "Listed {0} entities, trimmed to the configured limit of {1}.",
                ["ConfirmExpired"] = "The confirmation window has expired. Run /killall again.",
                ["NothingPending"] = "You have no pending batch to confirm.",
                ["Cancelled"] = "Pending batch cleared.",
                ["Timeout"] = "The confirmation window for '{0}' closed. Nothing was removed.",
                ["RemovedCount"] = "Removed {0} of {1} entities matching '{2}'."
            }, 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()
        {
            Puts($"AdminEntityRemover ready. Ray distance {config.MaxRayDistance}m, confirm window {config.ConfirmSeconds}s.");
        }

        private void Unload()
        {
            foreach (var batch in pending.Values)
            {
                batch.Timeout?.Destroy();
            }
            pending.Clear();
        }

        [ChatCommand("kill")]
        private void CmdKill(BasePlayer player, string command, string[] args)
        {
            if (!HasPermission(player)) return;
            if (player.IsDead()) return;

            RaycastHit hit;
            var ray = new Ray(player.eyes.position, player.eyes.HeadForward());
            if (!Physics.Raycast(ray, out hit, config.MaxRayDistance, ~PlayerLayer, QueryTriggerInteraction.Ignore))
            {
                player.ChatMessage(Msg("NoTarget", player.UserIDString));
                return;
            }

            var entity = hit.collider.GetComponentInParent<BaseEntity>();
            if (entity == null || entity == player)
            {
                player.ChatMessage(Msg("NoTarget", player.UserIDString));
                return;
            }

            var label = entity.ShortPrefabName ?? entity.PrefabName;
            var id = entity.net?.ID.Value ?? 0;
            entity.Kill();
            player.ChatMessage(Msg("Killed", player.UserIDString, label, id));
        }

        [ChatCommand("killall")]
        private void CmdKillAll(BasePlayer player, string command, string[] args)
        {
            if (!HasPermission(player)) return;

            if (args == null || args.Length == 0)
            {
                player.ChatMessage(Msg("Usage", player.UserIDString));
                return;
            }

            var arg = args[0].ToLowerInvariant();
            if (arg == "confirm")
            {
                Confirm(player);
                return;
            }
            if (arg == "cancel")
            {
                ClearPending(player.userID);
                player.ChatMessage(Msg("Cancelled", player.UserIDString));
                return;
            }

            if (pending.ContainsKey(player.userID))
            {
                player.ChatMessage(Msg("AlreadyPending", player.UserIDString, pending[player.userID].Prefab));
                return;
            }

            var matches = new List<BaseNetworkable>();
            string prefabLabel;

            if (arg == "last")
            {
                RaycastHit hit;
                var ray = new Ray(player.eyes.position, player.eyes.HeadForward());
                if (!Physics.Raycast(ray, out hit, config.MaxRayDistance, ~PlayerLayer, QueryTriggerInteraction.Ignore))
                {
                    player.ChatMessage(Msg("NoTarget", player.UserIDString));
                    return;
                }
                var entity = hit.collider.GetComponentInParent<BaseEntity>();
                if (entity == null || entity == player)
                {
                    player.ChatMessage(Msg("NoTarget", player.UserIDString));
                    return;
                }
                prefabLabel = entity.ShortPrefabName;
                matches.Add(entity);
            }
            else
            {
                prefabLabel = args[0];
                if (IsExcluded(prefabLabel))
                {
                    player.ChatMessage(Msg("Excluded", player.UserIDString, prefabLabel));
                    return;
                }

                foreach (var networkable in BaseNetworkable.serverEntities)
                {
                    var entity = networkable as BaseNetworkable;
                    if (entity == null || entity.IsDestroyed) continue;
                    if (string.IsNullOrEmpty(entity.ShortPrefabName)) continue;
                    if (!entity.ShortPrefabName.Equals(prefabLabel, StringComparison.OrdinalIgnoreCase)) continue;
                    if (IsExcluded(entity.ShortPrefabName)) continue;
                    matches.Add(entity);
                }
            }

            if (matches.Count == 0)
            {
                player.ChatMessage(Msg("NoMatches", player.UserIDString, prefabLabel));
                return;
            }

            var trimmed = false;
            if (matches.Count > config.MaxBatchSize)
            {
                matches.RemoveRange(config.MaxBatchSize, matches.Count - config.MaxBatchSize);
                trimmed = true;
            }

            var batch = new PendingBatch
            {
                Entities = matches,
                Prefab = prefabLabel,
                ExpiresAt = Time.realtimeSinceStartup + config.ConfirmSeconds
            };
            batch.Timeout = timer.Once(config.ConfirmSeconds, () => OnConfirmTimeout(player.userID));
            pending[player.userID] = batch;

            if (trimmed)
            {
                player.ChatMessage(Msg("BatchTrimmed", player.UserIDString, matches.Count, config.MaxBatchSize));
            }
            player.ChatMessage(Msg("BatchReady", player.UserIDString, matches.Count, prefabLabel, config.ConfirmSeconds));
        }

        private void Confirm(BasePlayer player)
        {
            PendingBatch batch;
            if (!pending.TryGetValue(player.userID, out batch))
            {
                player.ChatMessage(Msg("NothingPending", player.UserIDString));
                return;
            }

            if (Time.realtimeSinceStartup > batch.ExpiresAt)
            {
                ClearPending(player.userID);
                player.ChatMessage(Msg("ConfirmExpired", player.UserIDString));
                return;
            }

            var queued = batch.Entities.Count;
            var removed = 0;
            foreach (var entity in batch.Entities)
            {
                if (entity == null || entity.IsDestroyed) continue;
                entity.Kill();
                removed++;
            }

            var label = batch.Prefab;
            ClearPending(player.userID);
            player.ChatMessage(Msg("RemovedCount", player.UserIDString, removed, queued, label));
        }

        private void OnConfirmTimeout(ulong userId)
        {
            PendingBatch batch;
            if (!pending.TryGetValue(userId, out batch)) return;

            var prefab = batch.Prefab;
            pending.Remove(userId);

            var player = BasePlayer.FindByID(userId);
            if (player != null && player.IsConnected)
            {
                player.ChatMessage(Msg("Timeout", player.UserIDString, prefab));
            }
        }

        private void ClearPending(ulong userId)
        {
            PendingBatch batch;
            if (!pending.TryGetValue(userId, out batch)) return;
            batch.Timeout?.Destroy();
            pending.Remove(userId);
        }

        private bool IsExcluded(string shortPrefabName)
        {
            if (config.ExcludedPrefabs == null || string.IsNullOrEmpty(shortPrefabName)) return false;
            foreach (var excluded in config.ExcludedPrefabs)
            {
                if (!string.IsNullOrEmpty(excluded) && excluded.Equals(shortPrefabName, StringComparison.OrdinalIgnoreCase))
                    return true;
            }
            return false;
        }

        private bool HasPermission(BasePlayer player)
        {
            if (player == null) return false;
            if (player.IsAdmin) return true;
            if (permission.UserHasPermission(player.UserIDString, PermUse)) return true;
            player.ChatMessage(Msg("NoPermission", player.UserIDString));
            return false;
        }
    }
}

Hooks used: OnServerInitialized

Config file

Created automatically on first load at oxide/config/AdminEntityRemover.json.

AdminEntityRemover.json
{
  "Maximum ray distance": 10,
  "Confirmation window (seconds)": 10,
  "Maximum entities per batch": 500,
  "Excluded prefabs": [
    "player",
    "santasleepingbag"
  ]
}

Commands and permissions

Chat and console commands
CommandWhat it doesPermission
/killRemoves the entity you are looking at (raycast up to the configured distance). Reports the prefab name and network ID.adminentityremover.use
/killall <shortprefabname|last>Reports how many entities match and starts a 10 second confirmation window.adminentityremover.use
/killall confirmRemoves every queued entity. Fails if the confirmation window has expired.adminentityremover.use
/killall cancelClears the pending batch without removing anything.adminentityremover.use
Permissions
PermissionGrants
adminentityremover.useGrants access to /kill and /killall.

How the code works

  1. Register the permission and messages

    The class registers adminentityremover.use in Init with permission.RegisterPermission, and LoadDefaultMessages fills a dictionary with keys such as NoPermission, NoTarget, Killed, BatchReady, ConfirmExpired and Cancelled. The Msg helper wraps lang.GetMessage and string.Format, so admins who set a language get translated replies. The Permission constant keeps the lowercase class name as the prefix, which is what Oxide expects for plugin-scoped permission strings.

  2. Raycast the entity under the crosshair

    /kill checks the permission, builds a ray from player.eyes.position along player.eyes.HeadForward(), and calls Physics.Raycast with the configured MaxRayDistance and a layer mask that excludes players and triggers. The RaycastHit gives you the collider; walking up with hit.collider.GetComponentInParent<BaseEntity>() resolves the actual BaseEntity (a wall's collider may be a child object). If nothing is hit or the collider belongs to the admin's own player entity, the command replies with NoTarget. Otherwise it calls Kill() and prints ShortPrefabName and net.ID.Value.

  3. Match entities by prefab name

    BaseNetworkable.serverEntities enumerates everything currently spawned. /killall iterates it with as BaseNetworkable, skips anything destroyed, and compares ShortPrefabName with StringComparison.OrdinalIgnoreCase so adminentityremover also matches AdminEntityRemover. The ExcludedPrefabs config list protects things you never want wiped, for example player or santasleepingbag. The special argument last reuses the entity from your previous /kill instead of scanning, which is handy for a single awkward target with an awkward prefab string.

  4. Confirmation window with a timer

    The batch is stored in a List<BaseNetworkable> field per admin, next to an expiry timestamp from Time.realtimeSinceStartup. A timer.Once of ConfirmSeconds clears the entry and tells the player the window closed. /killall confirm compares the current time against the expiry before touching anything and replies with ConfirmExpired when late. /killall cancel removes the entry immediately. Both paths call ClearPending, which also destroys the stored Timer, so no callback survives into Unload.

  5. Limits and batching

    MaxBatchSize caps how many entities one confirmation can remove; anything above that is trimmed and the reply states the trimmed count. Entities are killed one by one with a null and IsDestroyed check before each Kill(), because killing a parent (a car chassis, a storage box back panel) can already have taken children with it. A running counter is compared before the final RemovedCount message so the reply reflects what actually died, not the queued total.

  6. Tuning through config

    PluginConfig holds MaxRayDistance (default 10), ConfirmSeconds (default 10), MaxBatchSize (default 500) and the ExcludedPrefabs list. LoadConfig catches a broken file, warns and falls back to LoadDefaultConfig, and SaveConfig writes it back through Config.WriteObject. Change MaxRayDistance if your staff work from flying mounts and need to reach further than a few metres.

  7. Clean shutdown

    Everything pending lives in a Dictionary<ulong, PendingBatch> with an inner Timer. Unload loops the dictionary, destroys each Timer and clears the map so no callback fires after a plugin reload. There is no data file: batches are deliberately short-lived, and a server restart should not wipe entities later.

Install it on your server

  1. Save AdminEntityRemover.cs

    Keep the file name exactly as the class name. Oxide matches them.
  2. 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.
  3. Edit the config if you want

    On first load the default config is written to oxide/config/AdminEntityRemover.json (Carbon: carbon/configs/AdminEntityRemover.json). After editing, reload it:oxide.reload AdminEntityRemover c.reload AdminEntityRemover # Carbon
  4. Grant the permission

    Give it to every player, or swap default for a group such as vip:oxide.grant group default adminentityremover.use c.grant group default adminentityremover.use # Carbon

Variations

Each one opens the generator with a ready prompt you can tweak.

  • Log every removal

    Writes each killed prefab, position and admin name to oxide/logs so staff actions are auditable.

    Generate this version
  • Radius removal

    Adds /killradius <metres> that removes every entity of a chosen prefab inside a sphere.

    Generate this version
  • Owner check

    Refuses to remove entities placed by a specific group or owned by a real player until forced.

    Generate this version

FAQ

How do I install this on Oxide or Carbon?

Put AdminEntityRemover.cs in oxide/plugins, then oxide.reload AdminEntityRemover from the server console. Oxide and Carbon load the same file. Grant adminentityremover.use to your staff group with oxide.grant group admin adminentityremover.use. The config is created at oxide/config/AdminEntityRemover.json on first load.

Why does /kill say there is no target when I am clearly looking at a crate?

The raycast runs from the player's eye position along the head forward vector, and it ignores the player layer and trigger colliders. Very small objects, or objects behind a wall's mesh, will not be hit. Move closer and aim at the body of the entity, or raise MaxRayDistance in the config if you are killing from a distance.

Can /killall remove players or sleeping bags?

Yes, if you type their short prefab name and nothing is excluded. player is a valid short prefab name. Keep player, santasleepingbag and any other important entries in ExcludedPrefabs; the plugin checks that list before queuing anything, so an accidental /killall player reports zero matches instead.

What happens if the admin disconnects between /killall and confirm?

The timer still runs and clears the pending batch after ConfirmSeconds. The stored BasePlayer is only used for chat replies, and the code checks it against IsConnected before sending, so nothing throws when the admin is gone.

Does this delete entity save files?

No. Kill() removes the live entity and marks it dead for the next save, which is the same thing damage does at zero health. There is no direct manipulation of oxide/data or .sav files, and the removal is effective immediately on the running server.

Customize it for your server

Change the reward, add a cooldown, restrict it to VIPs. Start from this prompt and edit anything.

Prompt: Create an Oxide Rust plugin named AdminEntityRemover with /kill raycast entity removal and /killall <shortprefabname> removal confirmed within 10 seconds, permission adminentityremover.use, config for ray distance and excluded prefabs.
Customize for my server

Edit the prompt on the next screen. 2 free plugins a month, no card.

Last updated Oct 26, 2026.

Customize admin entity remover

Customize