Fix "Failed to call hook ... InvalidCastException: Specified cast is not valid." in Oxide plugins
An InvalidCastException in a hook means your code used an explicit cast like (Plugin)obj or (T)value on an object that is actually a different type at runtime. Replace the cast with an "as" check (or Convert) and null-check the result before using it.
Failed to call hook 'OnPlayerChat' on plugin 'ChatFilter v1.0.0' (InvalidCastException: Specified cast is not valid.) at Oxide.Plugins.ChatFilter.OnPlayerChat (BasePlayer player, System.String message, ConVar.Chat+ChatChannel channel) [0x00041] in <8f3c1a2b4d5e4f6a9b0c1d2e3f4a5b6c>:0
Why it happens
An explicit cast (T)x throws InvalidCastException when the runtime type of x is not T and no valid conversion exists. The compiler only checks that the conversion is possible, not that the value really is that type. In Oxide plugins the classic triggers are: casting the object returned by Plugin.Call("Hook", args) to a type you assumed the other plugin returns (it may return null when the other plugin is missing or returns false/a different type); casting a value pulled from a data file or config (e.g. a string cast to int); and adding a parameter to a hook whose real argument is something else. OnPlayerChat fires with the engine's actual arguments, so if a helper inside it does the bad cast, the whole hook reports the cast failure. The exception is caught by Oxide, the hook keeps being called, and the stack trace points at the line with the cast.
How to fix it
- Read the stack trace in the error and find the line with a
(Something)cast. 2. If it is aCall()return value, change it toas Typeand check for null. Example:var other = SomePlugin?.Call("GetBalance", id) as float?;then useother ?? 0f. For value types useas int?/as float?orConvert.ToDouble(value, CultureInfo.InvariantCulture). 3. For config/data values read them with the typed reader (Config.ReadObject<T>(),DataFileSystem.ReadObject<T>()) instead of casting untyped objects. 4. Add a null check before every use, and exit the hook early if it is null. 5. Save the file; Oxide reloads it automatically. Confirm by runningoxide.reload ChatFilter, then trigger a chat message and check that noFailed to call hook 'OnPlayerChat'line appears and no new entry lands inoxide/logs.
Before and after
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
using Oxide.Core.Plugins;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("ChatFilter", "RustPluginGenerator", "1.0.0")]
[Description("Rewards players for chat messages and shows a balance")]
public class ChatFilter : RustPlugin
{
[PluginReference] private Plugin Economics;
private PluginConfig config;
private class PluginConfig
{
[JsonProperty("Reward amount")]
public int Reward = 10;
}
protected override void LoadDefaultConfig() => config = new PluginConfig();
protected override void LoadConfig() { base.LoadConfig(); try { config = Config.ReadObject<PluginConfig>(); if (config == null) throw new System.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>
{
["Reward"] = "You received {0} coins. Balance: {1}"
}, this);
}
private string Msg(string key, string userId = null, params object[] args) => string.Format(lang.GetMessage(key, this, userId), args);
private void OnPlayerChat(BasePlayer player, string message, ConVar.Chat.ChatChannel channel)
{
if (player == null || string.IsNullOrEmpty(message)) return;
var result = Economics?.Call("Deposit", player.UserIDString, (double)config.Reward);
var balance = Economics?.Call("Balance", player.UserIDString);
// Both casts below are unchecked and throw at runtime.
int newBalance = (int)balance;
bool success = (bool)result;
if (success)
player.ChatMessage(Msg("Reward", player.UserIDString, config.Reward, newBalance));
}
}
}using System;
using System.Collections.Generic;
using System.Globalization;
using Newtonsoft.Json;
using Oxide.Core;
using Oxide.Core.Plugins;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("ChatFilter", "RustPluginGenerator", "1.0.0")]
[Description("Rewards players for chat messages and shows a balance")]
public class ChatFilter : RustPlugin
{
[PluginReference] private Plugin Economics;
private PluginConfig config;
private class PluginConfig
{
[JsonProperty("Reward amount")]
public int Reward = 10;
}
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>
{
["Reward"] = "You received {0} coins. Balance: {1}"
}, this);
}
private string Msg(string key, string userId = null, params object[] args) => string.Format(lang.GetMessage(key, this, userId), args);
private void OnPlayerChat(BasePlayer player, string message, ConVar.Chat.ChatChannel channel)
{
if (player == null || string.IsNullOrEmpty(message)) return;
if (Economics == null) return;
var result = Economics.Call("Deposit", player.UserIDString, (double)config.Reward);
var balance = Economics.Call("Balance", player.UserIDString);
// "as" never throws; nullable types make value-type conversion safe.
var success = result as bool? ?? false;
var newBalance = balance as double? ?? 0d;
if (!success) return;
player.ChatMessage(Msg("Reward", player.UserIDString, config.Reward,
newBalance.ToString("0.##", CultureInfo.InvariantCulture)));
}
}
}FAQ
Why does the error repeat on every message?
Oxide catches the exception, keeps the plugin loaded, and calls the hook again on the next event. The line will keep printing until the cast is fixed and the plugin is reloaded with oxide.reload <Name>.
Is `as` always safe?
as returns null instead of throwing when the type does not match, so it is safe for reference types and nullable value types. You must still check for null. Plain value types (int, float, bool) cannot use as; use as int? or Convert.ToInt32.
Can a bad cast to a plugin type cause this?
Yes. (MyOtherPlugin)plugins.Find("MyOtherPlugin") throws if that plugin is not loaded. Use the [PluginReference] private Plugin MyOtherPlugin; field and null-check it, or plugins.Find(...) compared with null.
Does this happen in Carbon too?
Yes. Carbon loads the same Oxide plugin file and dispatches hooks in the same way, so an InvalidCastException in the hook prints a failed-hook error there as well. The fix is identical.
Different error?
Paste the full console output and get the cause and a fix in plain English.
Open the error explainerSkip the debugging
Generated plugins are compiled before you get them, and compiler errors are fixed automatically.
Generate a plugin2 free plugins a month, no card.
Last updated Oct 26, 2026.
Get plugins that compile first time
Generate