Rust Plugin Config File: How Oxide Configs Work and How to Write One
A Rust plugin config file is a JSON file at oxide/config/<PluginName>.json (carbon/configs on Carbon) that Oxide creates from the plugin's LoadDefaultConfig defaults on first load. Plugin authors map it to a C# class with JsonProperty names, read it with Config.ReadObject, write it with SaveConfig, and add new options in updates by relying on field defaults plus a version field. Server owners edit the file and reload the plugin with oxide.reload.
7 min readUpdated Oct 26, 2026
A Rust plugin config file is a JSON file named after the plugin, stored at oxide/config/<PluginName>.json. Oxide creates it from the plugin's default settings the first time the plugin loads. The plugin reads it into a C# class with Config.ReadObject, and writes it back with Config.WriteObject, usually wrapped in a SaveConfig override. Server owners change the JSON by hand and run oxide.reload <PluginName> to apply it.
On Carbon, the same file lives at carbon/configs/<PluginName>.json. Carbon loads Oxide plugins unchanged, so everything below applies to both.
Where the file lives and when it is created
The file name equals the plugin class name, so StarterKit.cs uses oxide/config/StarterKit.json. Oxide handles the config as part of loading the plugin, before OnServerInitialized:
- Oxide looks for the config file.
- If it does not exist, it calls the plugin's LoadDefaultConfig, which should build a fresh settings object, and the plugin saves it.
- If it exists, the plugin's
LoadConfigreads it. - Then Init, Loaded and OnServerInitialized run in the order described in how to make a Rust plugin.
Config is for settings an admin should edit. Data that the plugin changes while running, such as player balances or cooldowns, belongs in a data file. See Rust plugin data files. Player-facing text belongs in lang files, covered in Rust plugin localization.
The standard config pattern
Define a nested class for the settings, give every field a default value and a readable [JsonProperty] name, and override three methods. Here is a complete plugin that does it:
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
namespace Oxide.Plugins
{
[Info("Config Showcase", "RustPluginGenerator", "1.1.0")]
[Description("Demonstrates a versioned config with nested classes, lists and dictionaries")]
public class ConfigShowcase : RustPlugin
{
private const string PermUse = "configshowcase.use";
private PluginConfig config;
private class PluginConfig
{
[JsonProperty("Config version")]
public string ConfigVersion = "1.1.0";
[JsonProperty("Welcome message enabled")]
public bool WelcomeEnabled = true;
[JsonProperty("Reward settings")]
public RewardSettings Reward = new RewardSettings();
[JsonProperty("Tips shown by the command", ObjectCreationHandling = ObjectCreationHandling.Replace)]
public List<string> Tips = new List<string> { "Use /help", "Read the rules" };
[JsonProperty("Multiplier per permission", ObjectCreationHandling = ObjectCreationHandling.Replace)]
public Dictionary<string, float> Multipliers = new Dictionary<string, float>
{
["configshowcase.vip"] = 1.5f,
["configshowcase.elite"] = 2f
};
}
private class RewardSettings
{
[JsonProperty("Scrap amount")]
public int Scrap = 10;
[JsonProperty("Cooldown (seconds)")]
public float Cooldown = 3600f;
}
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();
}
if (config.Reward == null) config.Reward = new RewardSettings();
if (config.Tips == null) config.Tips = new List<string>();
if (config.Multipliers == null) config.Multipliers = new Dictionary<string, float>();
if (config.Reward.Scrap < 0) config.Reward.Scrap = 0;
if (config.Reward.Cooldown < 0f) config.Reward.Cooldown = 0f;
if (config.ConfigVersion != Version.ToString())
{
PrintWarning("Config updated from version " + config.ConfigVersion + " to " + Version);
config.ConfigVersion = Version.ToString();
}
SaveConfig();
}
protected override void SaveConfig() => Config.WriteObject(config);
protected override void LoadDefaultMessages()
{
lang.RegisterMessages(new Dictionary<string, string>
{
["Welcome"] = "Welcome, {0}!",
["NoPermission"] = "You do not have permission to use this command.",
["Info"] = "Reward: {0} scrap, cooldown {1}s. Tips: {2}. Multiplier tiers: {3}."
}, 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 OnPlayerConnected(BasePlayer player)
{
if (player == null || !config.WelcomeEnabled) return;
player.ChatMessage(Msg("Welcome", player.UserIDString, player.displayName));
}
[ChatCommand("cfgshow")]
private void CmdShow(BasePlayer player, string command, string[] args)
{
if (player == null) return;
if (!permission.UserHasPermission(player.UserIDString, PermUse))
{
player.ChatMessage(Msg("NoPermission", player.UserIDString));
return;
}
player.ChatMessage(Msg("Info", player.UserIDString,
config.Reward.Scrap,
config.Reward.Cooldown,
string.Join(", ", config.Tips),
config.Multipliers.Count));
}
}
}The resulting oxide/config/ConfigShowcase.json looks like this:
{
"Config version": "1.1.0",
"Welcome message enabled": true,
"Reward settings": {
"Scrap amount": 10,
"Cooldown (seconds)": 3600.0
},
"Tips shown by the command": ["Use /help", "Read the rules"],
"Multiplier per permission": {
"configshowcase.vip": 1.5,
"configshowcase.elite": 2.0
}
}What each override does
| Method | Purpose |
|---|---|
LoadDefaultConfig | Builds a new settings object with all default values. Oxide uses it when no file exists, and the plugin reuses it as a fallback. |
LoadConfig | Reads the file into the settings class, handles bad files, validates values and saves. |
SaveConfig | Writes the current in-memory object to the JSON file. |
Calling SaveConfig() at the end of LoadConfig matters. It writes any options that the file was missing, so admins see new settings in the file after an update.
JsonProperty names
[JsonProperty("Welcome message enabled")] sets the key in the JSON file. The C# field name does not matter to the file, so you can use readable keys with spaces and units, such as "Cooldown (seconds)". Rules worth following:
- Treat key names as a public interface. Renaming one makes the old value in users' files invisible, and the field falls back to its default.
- Put units in the key (seconds, percent, multiplier) so admins do not guess.
- Use real types. Booleans, numbers and strings in the JSON should map to
bool,int/floatandstringfields. A string in a number field is a parse error. - Matching by key is case-insensitive in Json.NET, but keep the exact casing anyway so the saved file stays tidy.
If the compiler reports that JsonProperty is unknown, the file is missing using Newtonsoft.Json;. See CS0246 JsonProperty not found.
Defaults make updates safe
When Json.NET deserializes a file, it creates the object with its constructor, so every field first holds its default from the code. It then overwrites only the fields whose keys are present in the file. This gives you the core update rule:
- A new option you add in version 1.1.0 is absent from a 1.0.0 file, so it keeps its default value.
- The user's existing values stay untouched.
- Because
LoadConfigends withSaveConfig(), the new key appears in the file right after the plugin reloads.
You do not need a merge routine for added options. The default values are the merge.
What the version field is for
The Config version value does not drive the merge. Its purpose is to let you run one-time migrations when the meaning of a setting changes. Examples:
- You renamed a key and need to copy the old value across.
- You changed a unit, such as minutes to seconds, and need to convert the stored number.
- You want to log a notice so admins know to review new options.
A migration sketch inside LoadConfig, before the save:
if (config.ConfigVersion == "1.0.0")
{
// old key stored minutes, new key stores seconds
var old = Config["Cooldown (minutes)"];
if (old != null)
config.Reward.Cooldown = Convert.ToSingle(old) * 60f;
}Config["Key"] reads a raw top-level value from the loaded file, which lets you reach keys that no longer exist in the class. Keep migrations short and delete them after a few releases.
Handling a corrupt config
Admins edit JSON by hand, and a missing comma or quote is the usual breakage. The pattern above wraps ReadObject in try/catch, treats a null result as failure, logs one warning and falls back to defaults. Two consequences to know:
- The plugin keeps working with default settings instead of failing to load.
SaveConfig()then overwrites the broken file with defaults, so the admin's other values are lost. Tell server owners to copy the file before editing.
For the error messages Oxide prints in this situation, see failed to load config file (corrupt). If saving fails because the settings class holds objects Json.NET cannot serialize, see failed to save config: illegal objects. Keep config classes to plain values: numbers, strings, booleans, lists, dictionaries and nested plain classes. Never store BasePlayer, Item, Vector3 or entity references.
After parsing, validate the numbers. A negative cooldown or a zero multiplier typed by an admin can break logic later, so clamp or replace such values in LoadConfig, and optionally warn once at load time.
Nested classes, lists and dictionaries
- Nested classes group related options (
Reward settings). Initialize each one withnewso it is never null for a fresh config, and re-check for null after reading, since an admin can writenullin the file. - Lists suit ordered or repeated values such as announcements, shortnames or blocked words.
List<string>andList<int>are the safest. - Dictionaries suit lookups such as permission-to-multiplier or shortname-to-amount. Prefer
stringkeys. Json.NET writes dictionary keys as JSON strings anyway.
The duplicate default entries trap
When a field already holds a list or dictionary, Json.NET by default adds the file's entries into the existing collection. A list with two defaults can therefore grow to four entries after one reload and keep growing with each save. Setting ObjectCreationHandling = ObjectCreationHandling.Replace on the property makes the file's collection replace the default completely. Use it on every list and dictionary with default content, as in the example.
The flip side: with Replace, an admin who deletes all entries from a list gets an empty list, not the defaults. That is usually the intended behavior.
Editing the config as a server owner
- Copy the current file to a backup.
- Edit the JSON in a plain text editor. Keep quotes, commas between entries and no trailing comma after the last one.
- Validate the JSON with an editor that highlights syntax errors.
- Run
oxide.reload ConfigShowcasein the server console or RCON. See Oxide console commands for the full list. - Check the console for the "Loaded plugin" line and for any warning about an invalid configuration.
The plugin does not notice file edits on its own. Saving the .cs file in oxide/plugins also reloads it, which re-reads the config, but changing only the JSON does not trigger anything.
To reset a plugin to defaults, unload it, delete its config file and load it again. The file is recreated from LoadDefaultConfig.
Config values that name permissions, like the multiplier dictionary above, still require the permission to be registered in code with permission.RegisterPermission. Granting works as described in Oxide permissions and groups. Permission names should start with the lowercase plugin name.
Common mistakes
- Reading config in a hook on every call. Read it once in
LoadConfigand keep the object. Never hit the file from frequent hooks such as OnEntityTakeDamage or OnDispenserGather. - Using config for runtime state. If your code calls
SaveConfigafter every player action, you are overwriting what the admin set and doing needless disk writes. Use a data file for that. - Changing key names between releases without a migration.
- Hard-coding chat text in config-like classes instead of lang files. Admins who want to translate should edit lang files.
- Skipping null checks after reading. A hand-edited
nullfor a nested class or list will cause aNullReferenceExceptionlater in a hook. See failed to call hook: NullReferenceException.
Seeing config patterns in real recipes
These how-to recipes use configurable settings and are good starting points: Gather rate multiplier, Kits with cooldowns, Automatic announcements and Scheduled restarts. You can also describe the settings you want in the plugin generator, and it produces a plugin with a config class that follows this pattern.
FAQ
Why did my config file get reset to defaults?
The plugin's LoadConfig most likely failed to parse the JSON (a missing comma or quote, for example) and fell back to defaults, then saved them over your file. Keep a backup before editing, and validate the JSON in an editor first. See the corrupt config error page.
Why do my default list entries appear twice after a reload?
Json.NET adds file entries to a list that already holds default values instead of replacing it. Set ObjectCreationHandling = ObjectCreationHandling.Replace in the [JsonProperty] attribute of every list and dictionary field with defaults, so the file contents win completely.
Do config values change live without a reload?
No. The plugin reads the file once while loading and keeps the values in memory. Editing the JSON does nothing until you run oxide.reload <PluginName>, or save the .cs file so Oxide reloads it. Saving the plugin from its own code only writes the in-memory values.
Can I rename a config key without losing the user's value?
Not automatically. Json.NET matches by the exact JsonProperty name, so a renamed key reads as missing and takes the default. To migrate, read the old key from Config["Old key"] in LoadConfig when the stored version is older, copy the value into the new field, and then save.
Where is the config on a Carbon server?
Carbon stores configs in carbon/configs/<PluginName>.json instead of oxide/config. The plugin code is the same, because Carbon loads Oxide plugins and provides the same Config API. Carbon also accepts the oxide.reload console command, so the editing workflow is identical.
More guides
- CovalencePlugin vs RustPlugin: Differences and Which to Use4 min read
- How to Debug a Rust Oxide Plugin: Compile Errors, Exceptions and Slow Hooks10 min read
- How to Give Items to Players in a Rust Plugin (Oxide/Carbon)5 min read
- How to Install Oxide on a Rust Dedicated Server (Windows and Linux)7 min read
Skip the C#: describe your plugin
Open generator