Skip to content
RustPluginGenerator

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

DefaultSkins.cs
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.

DefaultSkins.json
{
  "Skins": {}
}

Commands and permissions

Chat and console commands
CommandWhat it doesPermission
/skin <shortname> <skinid>Save a default skin ID for an item shortname (0 clears it).defaultskins.use
/skin listList the default skins you have saved.defaultskins.use
Permissions
PermissionGrants
defaultskins.useAllows the player to set, clear and list default item skins.

How the code works

  1. Storing per-player skin choices

    A nested StoredData class holds Dictionary<string, Dictionary<string, ulong>> Skins, keyed by Steam ID and then by shortname. It is read in OnServerInitialized and written back in Unload with Interface.Oxide.DataFileSystem.ReadObject and WriteObject under the name DefaultSkins. Every lookup is null-checked because a fresh server has no data file yet.

  2. The /skin command

    [ChatCommand("skin")] handles both forms. With one argument list it prints the player's saved entries through the lang message SkinList. With two arguments it validates the shortname with ItemManager.FindItemDefinition(skinname), parses the ID with ulong.TryParse, and stores or removes the entry. Setting a skin requires the defaultskins.use permission and is rejected when the player is not connected.

  3. Applying the skin on craft finish

    OnItemCraftFinished(ItemCraftTask task, Item item, ItemCrafter itemCrafter) fires after the crafted item exists. The plugin reads itemCrafter.owner to find the player, pulls the saved skin for item.info.shortname, and skips anything equal to the current item.skin. Otherwise it assigns item.skin and calls item.MarkDirty() so the client renders the new skin immediately.

  4. 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 SaveData from Unload. If the player is an NPC, a null owner, or has no saved skin for that shortname, the hook returns immediately.

  5. Messages and permissions

    All player-facing text lives in LoadDefaultMessages, with keys such as SkinSet, SkinCleared, SkinList, InvalidItem, InvalidSkin and NoPermission, formatted through lang.GetMessage. The single permission defaultskins.use is registered in Init and checked in the command with permission.UserHasPermission(player.UserIDString, PermUse).

  6. Unload and persistence

    Unload writes the dictionary back to oxide/data/DefaultSkins.json so 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

  1. Save DefaultSkins.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/DefaultSkins.json (Carbon: carbon/configs/DefaultSkins.json). After editing, reload it:oxide.reload DefaultSkins c.reload DefaultSkins # Carbon
  4. Grant the permission

    Give it to every player, or swap default for a group such as vip: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 version
  • Server-wide default skins

    Give admins a console command that sets a fallback skin for everyone.

    Generate this version
  • Skin 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.

Prompt: Create a Rust Oxide plugin called DefaultSkins: /skin <shortname> <skinid> saves a per-player default skin in oxide/data and OnItemCraftFinished applies it, permission defaultskins.use.
Customize for my server

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

Last updated Oct 26, 2026.

Customize default item skins

Customize