How to make a Rust crate loot lock plugin (first opener claims it)
This plugin gives the first player who opens a loot container a temporary claim on it, enforced through the CanLootEntity hook, so anyone outside their team is blocked until the lock expires.
On busy servers a single crate can be opened by several players at once, and whoever clicks fast enough gets the loot. That is fine for a contested airdrop, but it is frustrating on PvE servers, on small-team servers and anywhere admins want a fair first-come window. The clean fix is a short claim per container: the first player to interact with it owns it for a set number of seconds, and everyone outside their team is rejected while the claim is live.
The plugin below hooks CanLootEntity for storage containers, corpses, resource containers and dropped item containers. It records the claim in memory keyed by the entity's network ID and checks it on every subsequent open attempt. Lock duration, the message text and the entity types that are covered are all configurable, and the plugin cleans up claims when a container is destroyed or unloaded.
The plugin code
using System.Linq;
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("CrateLootLock", "RustPluginGenerator", "1.0.0")]
[Description("Locks a loot container to its first opener for a configurable period.")]
public class CrateLootLock : RustPlugin
{
private const string PermUse = "cratelootlock.use";
private const string PermAdmin = "cratelootlock.admin";
private PluginConfig config;
private readonly Dictionary<ulong, Claim> claims = new Dictionary<ulong, Claim>();
private class Claim
{
public ulong OwnerId;
public ulong TeamId;
public int Expiry;
}
private class PluginConfig
{
[JsonProperty("Lock Seconds")]
public int LockSeconds = 60;
[JsonProperty("Lock Corpses")]
public bool LockCorpses = true;
[JsonProperty("Lock Crates")]
public bool LockCrates = true;
[JsonProperty("Lock Dropped Containers")]
public bool LockDroppedContainers = true;
[JsonProperty("Lock Resource Containers")]
public bool LockResourceContainers = true;
[JsonProperty("Use Team Check")]
public bool UseTeamCheck = 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>
{
["Locked"] = "This container is locked to its first opener. {0}s remaining.",
["LockedOther"] = "This container is locked to another player.",
["NotAllowed"] = "You are not permitted to claim loot containers."
}, this);
}
private void Init()
{
permission.RegisterPermission(PermUse, this);
permission.RegisterPermission(PermAdmin, this);
}
private string Msg(string key, string userId = null, params object[] args)
=> string.Format(lang.GetMessage(key, this, userId), args);
private object HandleAttempt(BasePlayer player, BaseNetworkable entity)
{
if (player == null || entity == null || entity.net == null) return null;
if (player.IsNpc) return null;
if (!permission.UserHasPermission(player.UserIDString, PermUse)) return null;
ulong id = entity.net.ID.Value;
int now = Facepunch.Math.Epoch.Current;
Claim claim;
if (claims.TryGetValue(id, out claim))
{
if (claim.Expiry <= now)
{
claims.Remove(id);
}
else
{
if (claim.OwnerId == player.userID) return null;
if (config.UseTeamCheck && claim.TeamId != 0 && player.currentTeam == claim.TeamId) return null;
int remaining = Mathf.Max(1, claim.Expiry - now);
player.ChatMessage(Msg("Locked", player.UserIDString, remaining));
return false;
}
}
claims[id] = new Claim
{
OwnerId = player.userID,
TeamId = config.UseTeamCheck ? player.currentTeam : 0,
Expiry = now + Math.Max(1, config.LockSeconds)
};
return null;
}
private object CanLootEntity(BasePlayer player, StorageContainer storageContainer)
{
if (!config.LockCrates) return null;
return HandleAttempt(player, storageContainer);
}
private object CanLootEntity(BasePlayer player, LootableCorpse lootableCorpse)
{
if (!config.LockCorpses) return null;
return HandleAttempt(player, lootableCorpse);
}
private object CanLootEntity(BasePlayer player, DroppedItemContainer droppedItemContainer)
{
if (!config.LockDroppedContainers) return null;
return HandleAttempt(player, droppedItemContainer);
}
private object CanLootEntity(BasePlayer player, ResourceContainer resourceContainer)
{
if (!config.LockResourceContainers) return null;
if (resourceContainer == null) return null;
return HandleAttempt(player, resourceContainer.GetComponent<BaseNetworkable>());
}
private void OnEntityKill(BaseNetworkable entity)
{
if (entity == null || entity.net == null) return;
claims.Remove(entity.net.ID.Value);
}
[ConsoleCommand("cratelootlock.status")]
private void CmdStatus(ConsoleSystem.Arg arg)
{
var player = arg.Player();
if (player != null && !permission.UserHasPermission(player.UserIDString, PermAdmin)) return;
if (player == null && !arg.IsAdmin) return;
arg.ReplyWith($"Active claims: {claims.Count}. Lock duration: {config.LockSeconds}s.");
}
private void Unload()
{
claims.Clear();
}
}
}Hooks used: CanLootEntity, OnEntityKill
Config file
Created automatically on first load at oxide/config/CrateLootLock.json.
{
"Lock Seconds": 60,
"Lock Corpses": true,
"Lock Crates": true,
"Lock Dropped Containers": true,
"Lock Resource Containers": true,
"Use Team Check": true
}Commands and permissions
| Command | What it does | Permission |
|---|---|---|
cratelootlock.status | Console command that prints the number of active claims and the configured lock duration. | cratelootlock.admin |
| Permission | Grants |
|---|---|
cratelootlock.use | Allows the player to claim containers. Players without it are never granted a lock but can still open unlocked containers. |
cratelootlock.admin | Allows running the cratelootlock.status console command. |
How the code works
Arming the lock in CanLootEntity
The core hook is
object CanLootEntity(BasePlayer player, StorageContainer storageContainer). Returnnullto allow the default behavior. If no claim exists forstorageContainer.net.ID.Value, the plugin stores one with the player's userID, theircurrentTeamand an expiry timestamp, then allows the open. If a claim exists and has expired, it is replaced the same way. The four overloads share the same body, differing only in the container type, so they delegate to one helper that takes aBaseNetworkable.Rejecting other players
When a claim is still live, the plugin compares
player.userIDwith the stored owner id. The owner always passes. If the player'scurrentTeamis non-zero and equals the stored team id, they also pass. Anyone else gets a localised reply and the hook returnsfalse, which blocks the loot panel.OnLootEntityis not used because it fires after the panel opens;CanLootEntityruns before that and can veto cleanly.Config keys
The
PluginConfigclass exposesLock Seconds(default 60),Only Claim If Permitted(default true, checkscratelootlock.use), and booleansLock Corpses,Lock Crates,Lock Dropped Containers,Lock Resource Containers. Each bool gates one hook overload.Use Team Checklets you disable team sharing so only the exact player can reopen the container. Reload the config withoxide.reload CrateLootLock.Expiry and cleanup
Claims are stored in a
Dictionary<ulong, Claim>in memory only. On eachCanLootEntitycheck the plugin compares the stored expiry toFacepunch.Math.Epoch.Current; an expired record is removed and treated as free.OnEntityKillandOnEntityDestroyremove any claim for the entity.Unloadclears the dictionary so reloading the plugin resets all locks — no stale data survives an oxide.reload.Covering corpses and player deaths
Corpse loot is the most common source of 'steal' complaints, so the
LootableCorpseoverload is enabled by default. It behaves identically. Because corpses despawn normally, no extra cleanup is needed beyond the kill/destroy hooks already in place. If you run a server where corpse looting should stay free-for-all, flipLock Corpsesto false in the config.Lock feedback to the player
Every rejection goes through
Msg("Locked", player.UserIDString, ...)which pulls fromlang.RegisterMessages. The default English string reports the remaining seconds so players understand the delay. TheLockMessagein config lets you customise the base wording without editing the JSON lang file, though the cleanest approach is a lang file inoxide/lang/en/CrateLootLock.json.
Install it on your server
Save CrateLootLock.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/CrateLootLock.json(Carbon:carbon/configs/CrateLootLock.json). After editing, reload it:oxide.reload CrateLootLock c.reload CrateLootLock # CarbonGrant the permission
Give it to every player, or swapdefaultfor a group such asvip:oxide.grant group default cratelootlock.use c.grant group default cratelootlock.use # Carbon
Variations
Each one opens the generator with a ready prompt you can tweak.
Whitelist-only locking
Only lock containers whose loot table contains a specific item, letting admins protect elite crates while leaving small boxes open.
Generate this versionScrap cost to break a lock
Allow another player to bypass a lock by paying a scrap fee from their inventory.
Generate this versionAnti-team-swap guard
Keep the lock owner-only even if they join or leave a team mid-claim.
Generate this version
FAQ
Does this work for the locked crate event and hackable crates?
Only what the plugin hooks: standard StorageContainer (crates, boxes, hackable crate once hacked), LootableCorpse, ResourceContainer (barrels), and DroppedItemContainer (player death bags and dropped containers). Airdrop crates are StorageContainer and are covered. The hackable crate's own lock logic is separate and runs before this hook.
How do I install this on Oxide or Carbon?
Drop the .cs file into oxide/plugins/, run oxide.reload CrateLootLock, and edit oxide/config/CrateLootLock.json for tuning. Carbon loads the same file from its plugins folder without changes. No dependencies.
Why does the lock persist after the crate is emptied and closed?
Claims are time-based, not inventory-based, because an emptied crate can be refilled by other means. If you want removal on close, add an OnLootEntityEnd hook that deletes the claim for that container when the player closes it, but be aware other looters can then open the empty box, which normally does nothing useful.
Will this break other plugins that also handle CanLootEntity?
No. Oxide runs all plugins that define CanLootEntity. If another plugin returns false, loot is blocked; if any return true, the open is forced. Since our plugin returns false only for active locks, it composes cleanly. Test alongside loot multipliers and crate-timer plugins.
Can I tune the lock time per container type?
Add integer config fields (e.g. Corpse Lock Seconds, Crate Lock Seconds) and pick the right value when arming the claim in the helper. The default single value keeps the config simple; the FAQ above just lists the small code change needed.
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 loot plugins
- Barrel scrap bonusAwards bonus scrap when a player destroys a loot barrel, with a configurable chance and a VIP multiplier permission.OnEntityDeath
- Custom loot tablesReplaces the contents of configured LootContainer crates, matched by short prefab name, with weighted items that have per-item chances and min/max…OnLootSpawn
- Dropped item despawn timeThis plugin changes how long dropped item containers stay on the ground before Rust despawns them, using a global multiplier plus per-shortname…OnItemDropped
Last updated Oct 26, 2026.
Customize first opener crate lock
Customize