Skip to content
RustPluginGenerator

OnItemRemovedFromContainer

OnItemRemovedFromContainer is called after an item has been removed from an ItemContainer, at the end of ItemContainer.Remove, so the removal has already happened and cannot be cancelled.

Signature
void OnItemRemovedFromContainer(ItemContainer itemContainer, Item item, BasePlayer sourcePlayer)

Parameters

Hook parameters
ParameterTypeDescription
itemContainerItemContainerThe ItemContainer the item was just taken out of. Still holds the remaining items; the removed item is no longer in itemList at this point.
itemItemThe Item instance being removed. Its parent may already be cleared, so read info.shortname, amount and skin from the item itself rather than from its container.
sourcePlayerBasePlayerThe BasePlayer responsible for the removal, or null when the removal is server-driven (recycler, vendor, loot despawn, plugin code without a player owner).

Return value

The return value is ignored. ItemContainer.Remove always returns true after the hook runs, so returning a value or null changes nothing.

How it works

The hook fires at the end of ItemContainer.Remove, right after the container's contents-changed callback and MarkDirty(). Because it sits after the item is gone, it is a notification hook: you cannot stop the removal, only observe it. That makes it the right place to record what left a container.

Use itemContainer to identify where the item came from (entityOwner for a box, furnace or turret, playerOwner for a player inventory), item for the shortname, amount, skin and condition of what was taken, and sourcePlayer to know who did it. sourcePlayer is null for server-side removals, so always null-check it before touching player members.

Narrowing does not help here: ItemContainer and Item are the only concrete types passed, and declaring a subclass would only skip calls. The hook fires for every container removal on the server, including despawns, recycler consumption, plugin code, and inventory operations, so keep the body cheap and avoid Puts or console output.

Since removal cannot be vetoed, use CanMoveItem or OnItemRemove when you need to prevent a move instead of record it.

Example plugin: Count items pulled out of locked containers

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

namespace Oxide.Plugins
{
    [Info("Container Withdraw Audit", "RustPluginGenerator", "1.0.0")]
    [Description("Summarises which items a player removed from entity-owned containers during a loot session")]
    public class ContainerWithdrawAudit : RustPlugin
    {
        private const string PermSee = "containerwithdrawaudit.see";

        private PluginConfig config;
        private readonly Dictionary<ulong, Dictionary<string, int>> sessions = new Dictionary<ulong, Dictionary<string, int>>();
        private readonly Dictionary<ulong, Timer> reportTimers = new Dictionary<ulong, Timer>();

        private class PluginConfig
        {
            [JsonProperty("Summary delay after last withdrawal (seconds)")]
            public float ReportDelay = 3f;

            [JsonProperty("Notify players with permission")]
            public bool NotifyAdmins = true;
        }

        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>
            {
                ["Summary"] = "You removed {0} item stacks from containers: {1}",
                ["AdminSummary"] = "{0} removed from containers: {1}",
                ["None"] = "You did not remove any tracked items this session."
            }, 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(PermSee, this);
        }

        private void OnItemRemovedFromContainer(ItemContainer itemContainer, Item item, BasePlayer sourcePlayer)
        {
            if (itemContainer == null || item == null || sourcePlayer == null || !sourcePlayer.IsConnected) return;
            if (itemContainer.entityOwner == null) return;
            if (!(itemContainer.entityOwner is StorageContainer)) return;
            if (item.info == null) return;

            ulong userId = sourcePlayer.userID.Get();
            Dictionary<string, int> totals;
            if (!sessions.TryGetValue(userId, out totals))
            {
                totals = new Dictionary<string, int>();
                sessions[userId] = totals;
            }

            int current;
            totals.TryGetValue(item.info.shortname, out current);
            totals[item.info.shortname] = current + item.amount;

            Timer existing;
            if (reportTimers.TryGetValue(userId, out existing) && existing != null) existing.Destroy();
            reportTimers[userId] = timer.Once(Mathf.Max(1f, config.ReportDelay), () => Report(sourcePlayer));
        }

        private void Report(BasePlayer player)
        {
            if (player == null) return;
            ulong userId = player.userID.Get();
            reportTimers.Remove(userId);

            Dictionary<string, int> totals;
            if (!sessions.TryGetValue(userId, out totals) || totals.Count == 0) return;
            sessions.Remove(userId);

            string body = string.Empty;
            foreach (KeyValuePair<string, int> pair in totals)
            {
                if (body.Length > 0) body += ", ";
                body += pair.Key + " x" + pair.Value;
            }

            player.ChatMessage(Msg("Summary", player.UserIDString, totals.Count, body));

            if (config.NotifyAdmins && permission.UserHasPermission(player.UserIDString, PermSee))
            {
                PrintToChat(Msg("AdminSummary", null, player.displayName, body));
            }
        }

        private void OnPlayerLootEnd(PlayerLoot inventory)
        {
            BasePlayer player = inventory != null && inventory.baseEntity != null ? inventory.baseEntity.ToPlayer() : null;
            if (player == null) return;

            Timer existing;
            ulong userId = player.userID.Get();
            if (reportTimers.TryGetValue(userId, out existing) && existing != null)
            {
                existing.Destroy();
                reportTimers.Remove(userId);
            }
            NextTick(() => Report(player));
        }

        private void Unload()
        {
            foreach (Timer timer in reportTimers.Values)
            {
                if (timer != null) timer.Destroy();
            }
            reportTimers.Clear();
            sessions.Clear();
        }
    }
}

The plugin watches OnItemRemovedFromContainer during a loot session. When the container has an entity owner that is a StorageContainer, a BoxStorage or similar lootable, and the source player is connected, it starts a per-player timer on the first removal. Every removal inside the tracking window adds the item amount to a running total, and when OnPlayerLootEnd fires the plugin reports the shortlist of withdrawn items to the player and to admins on the permission list. TrackSeconds and the permission name live in the config, so admins can tune the window without recompiling, and the dictionary is cleared on unload to avoid stale state.

Common use cases

  • Withdraw audit for base boxes

    Log which player pulled which items from entity-owned containers, then surface the totals in a chat summary or a periodic report to admins.

  • Daily quota tracking

    Accumulate resource counts per player per day and use the totals to gate vendor commands or rewards behind a configurable cap.

  • Recycler and vendor accounting

    Track server-side removals where sourcePlayer is null to separate machine consumption from player withdrawals in your statistics.

Pitfalls

  • The removal is already final

    Do not try to restore the item by moving it back in this hook; that creates a second removal and a second hook call. Prevent the move in CanMoveItem or OnItemRemove instead.

  • sourcePlayer is often null

    Recyclers, vendors, loot despawn and plugin-driven removals pass no player. Guard the parameter before using UserIDString or displayName.

  • It fires extremely often

    Every stack split, quick-move and container despawn runs this hook. Avoid chat output, permission lookups and LINQ allocations in the body; buffer into a dictionary and flush on a timer or loot end.

Build a plugin with OnItemRemovedFromContainer

Describe what should happen and get a compiled plugin that uses this hook.

Prompt: Withdraw audit for base boxes using the OnItemRemovedFromContainer hook, with a config file and a permission
Generate this plugin

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 OnItemRemovedFromContainer

Generate