How to make a Rust default item skins plugin (skin per crafted item)
Lets each player store a default skin ID per item shortname with /skin and applies it to every item the player crafts through the OnItemCraftFinished hook. Skins are saved per Steam ID in oxide/data.
Skin selection in Rust is per-item: players open the skin picker in their inventory and change skins on items they already own. That is tedious for admins running custom skin shops, because every new crafted item arrives with the default skin and must be re-skinned by hand. This plugin remembers a preferred skin per item shortname for each player and stamps it onto crafted output automatically.
Players run /skin <shortname> <skinid> to set a default (and /skin <shortname> 0 to clear it), then every item of that shortname produced by crafting comes out with the saved skin. A player can also preview the saved skins with /skin list. Data lives in oxide/data/DefaultSkins.json, one dictionary per Steam ID, so it survives restarts and wipes only if you delete the data file. Only the skin field is changed; the item itself is the one the craft produced, so stacks, conditions and crafting costs are untouched.
The plugin code
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("DefaultSkins", "RustPluginGenerator", "1.0.0")]
[Description("Lets players save a default skin per item shortname and applies it to crafted items.")]
public class DefaultSkins : RustPlugin
{
private const string PermUse = "defaultskins.use";
private class StoredData
{
[JsonProperty("Skins")]
public Dictionary<string, Dictionary<string, ulong>> Skins = new Dictionary<string, Dictionary<string, ulong>>();
}
private StoredData data;
private void Init()
{
permission.RegisterPermission(PermUse, this);
}
private void OnServerInitialized()
{
LoadData();
}
private void Unload()
{
SaveData();
}
private void LoadData()
{
try
{
data = Interface.Oxide.DataFileSystem.ReadObject<StoredData>("DefaultSkins");
}
catch
{
PrintWarning("DefaultSkins data file was unreadable, starting empty");
data = null;
}
if (data == null)
data = new StoredData();
if (data.Skins == null)
data.Skins = new Dictionary<string, Dictionary<string, ulong>>();
}
private void SaveData()
{
if (data != null)
Interface.Oxide.DataFileSystem.WriteObject("DefaultSkins", data);
}
[ChatCommand("skin")]
private void CmdSkin(BasePlayer player, string command, string[] args)
{
if (player == null)
return;
if (!permission.UserHasPermission(player.UserIDString, PermUse))
{
player.ChatMessage(Msg("NoPermission", player.UserIDString));
return;
}
if (args == null || args.Length == 0)
{
player.ChatMessage(Msg("Usage", player.UserIDString));
return;
}
if (args.Length == 1 && args[0].Equals("list", StringComparison.OrdinalIgnoreCase))
{
ListSkins(player);
return;
}
if (args.Length < 2)
{
player.ChatMessage(Msg("Usage", player.UserIDString));
return;
}
string shortname = args[0].ToLowerInvariant();
var definition = ItemManager.FindItemDefinition(shortname);
if (definition == null)
{
player.ChatMessage(Msg("InvalidItem", player.UserIDString, shortname));
return;
}
ulong skinId;
if (!ulong.TryParse(args[1], out skinId))
{
player.ChatMessage(Msg("InvalidSkin", player.UserIDString, args[1]));
return;
}
if (!data.Skins.TryGetValue(player.UserIDString, out var entries))
{
entries = new Dictionary<string, ulong>();
data.Skins[player.UserIDString] = entries;
}
if (skinId == 0)
{
if (entries.Remove(shortname))
{
player.ChatMessage(Msg("SkinCleared", player.UserIDString, definition.displayName.english));
SaveData();
}
else
{
player.ChatMessage(Msg("NoEntry", player.UserIDString, definition.displayName.english));
}
return;
}
entries[shortname] = skinId;
SaveData();
player.ChatMessage(Msg("SkinSet", player.UserIDString, definition.displayName.english, skinId.ToString()));
}
private void ListSkins(BasePlayer player)
{
if (!data.Skins.TryGetValue(player.UserIDString, out var entries) || entries.Count == 0)
{
player.ChatMessage(Msg("ListEmpty", player.UserIDString));
return;
}
var lines = new List<string>();
foreach (var kvp in entries)
{
var definition = ItemManager.FindItemDefinition(kvp.Key);
string name = definition != null ? definition.displayName.english : kvp.Key;
lines.Add(string.Format("{0} = {1}", name, kvp.Value));
}
player.ChatMessage(Msg("SkinList", player.UserIDString, string.Join(", ", lines)));
}
private void OnItemCraftFinished(ItemCraftTask task, Item item, ItemCrafter itemCrafter)
{
if (item == null || item.info == null || itemCrafter == null)
return;
BasePlayer owner = itemCrafter.owner;
if (owner == null || owner.IsNpc)
return;
if (!data.Skins.TryGetValue(owner.UserIDString, out var entries) || entries.Count == 0)
return;
if (!entries.TryGetValue(item.info.shortname, out ulong skinId) || skinId == 0)
return;
if (item.skin == skinId)
return;
item.skin = skinId;
item.MarkDirty();
}
private string Msg(string key, string userId = null, params object[] args)
{
return string.Format(lang.GetMessage(key, this, userId), args);
}
protected override void LoadDefaultMessages()
{
lang.RegisterMessages(new Dictionary<string, string>
{
["Usage"] = "Usage: /skin <shortname> <skinid> to set a default skin, /skin <shortname> 0 to clear it, /skin list to view saved skins.",
["NoPermission"] = "You do not have permission to use default skins.",
["InvalidItem"] = "Unknown item shortname: {0}",
["InvalidSkin"] = "Skin ID must be a whole number: {0}",
["SkinSet"] = "Default skin for {0} is now {1}. Newly crafted items will use it.",
["SkinCleared"] = "Default skin for {0} cleared.",
["NoEntry"] = "You had no default skin saved for {0}.",
["ListEmpty"] = "You have no default skins saved.",
["SkinList"] = "Your default skins: {0}"
}, this);
}
}
}Hooks used: OnServerInitialized, OnItemCraftFinished
Config file
Created automatically on first load at oxide/config/DefaultSkins.json.
{
"Skins": {}
}Commands and permissions
| Command | What it does | Permission |
|---|---|---|
/skin <shortname> <skinid> | Save a default skin ID for an item shortname (0 clears it). | defaultskins.use |
/skin list | List the default skins you have saved. | defaultskins.use |
| Permission | Grants |
|---|---|
defaultskins.use | Allows the player to set, clear and list default item skins. |
How the code works
Storing per-player skin choices
A nested
StoredDataclass holdsDictionary<string, Dictionary<string, ulong>> Skins, keyed by Steam ID and then by shortname. It is read inOnServerInitializedand written back inUnloadwithInterface.Oxide.DataFileSystem.ReadObjectandWriteObjectunder the nameDefaultSkins. Every lookup is null-checked because a fresh server has no data file yet.The /skin command
[ChatCommand("skin")]handles both forms. With one argumentlistit prints the player's saved entries through the lang messageSkinList. With two arguments it validates the shortname withItemManager.FindItemDefinition(skinname), parses the ID withulong.TryParse, and stores or removes the entry. Setting a skin requires thedefaultskins.usepermission and is rejected when the player is not connected.Applying the skin on craft finish
OnItemCraftFinished(ItemCraftTask task, Item item, ItemCrafter itemCrafter)fires after the crafted item exists. The plugin readsitemCrafter.ownerto find the player, pulls the saved skin foritem.info.shortname, and skips anything equal to the currentitem.skin. Otherwise it assignsitem.skinand callsitem.MarkDirty()so the client renders the new skin immediately.Keeping the hook cheap
OnItemCraftFinished runs for every crafted item on the server, so it must not print warnings or touch disk. The plugin keeps all data in a single in-memory dictionary loaded once at startup and only calls
SaveDatafromUnload. If the player is an NPC, a null owner, or has no saved skin for that shortname, the hook returns immediately.Messages and permissions
All player-facing text lives in
LoadDefaultMessages, with keys such asSkinSet,SkinCleared,SkinList,InvalidItem,InvalidSkinandNoPermission, formatted throughlang.GetMessage. The single permissiondefaultskins.useis registered inInitand checked in the command withpermission.UserHasPermission(player.UserIDString, PermUse).Unload and persistence
Unloadwrites the dictionary back tooxide/data/DefaultSkins.jsonso a reload keeps whatever players set since the last save. Because the file is plain JSON keyed by Steam ID, admins can pre-seed skins for a group or copy entries between servers without touching the plugin code.
Install it on your server
Save DefaultSkins.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/DefaultSkins.json(Carbon:carbon/configs/DefaultSkins.json). After editing, reload it:oxide.reload DefaultSkins c.reload DefaultSkins # CarbonGrant the permission
Give it to every player, or swapdefaultfor a group such asvip:oxide.grant group default defaultskins.use c.grant group default defaultskins.use # Carbon
Variations
Each one opens the generator with a ready prompt you can tweak.
Skin only for a whitelisted set of items
Restrict default skins to a config list so players cannot skin arbitrary items.
Generate this versionServer-wide default skins
Give admins a console command that sets a fallback skin for everyone.
Generate this versionSkin picker UI
Let players choose a saved skin through a small CUI panel instead of typing IDs.
Generate this version
FAQ
Does this change items the player already owns?
No. The hook only fires when a craft finishes, so existing items keep their current skin. Players can still change those manually with the inventory skin picker.
Where are the skins stored?
In oxide/data/DefaultSkins.json, keyed by Steam ID and then by item shortname. Delete the file to reset everyone, or edit it to pre-seed values before a restart.
What happens on a server wipe?
Blueprint wipes do not delete oxide/data, so saved skins survive unless you remove the file yourself. The OnItemCraftFinished hook keeps working across wipes because the shortnames do not change.
Can players use skin IDs that are not in the skin shop?
Yes. The plugin only checks that the ID parses as a number. If you need validation, add an allowlist in config and reject IDs that are not in it before saving.
How do I install it?
Drop DefaultSkins.cs into oxide/plugins on Oxide or Carbon and run oxide.grant user <steamid> defaultskins.use. The first load writes a default data file automatically.
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 quality of life plugins
- BackpacksAdds a /backpack command that opens a per-player StorageContainer sized by permission, with contents persisted to oxide/data and optionally droppedโฆOnPlayerLootEndOnPlayerDisconnectedOnPlayerDeath
- Crafting speedA plugin that speeds up crafting by scaling the duration of ItemCraftTask instances when a craft starts, using a global multiplier plus optionalโฆOnItemCraft
- Day and night controlSkips or shortens night by advancing the in-game clock to a configurable morning time, and adds /day and /night admin commands.OnServerInitialized
Last updated Oct 26, 2026.
Customize default item skins
Customize