Quick answer: Phantom overrides appear when serialized field defaults differ between asset and instance. Initialize fields at declaration, keep ISerializationCallbackReceiver idempotent, and avoid mutating serialized fields in OnEnable or constructors.

Here is how to fix Unity prefab instances that show modified overrides on every domain reload, even when you did not edit them. The Overrides dropdown shows fields highlighted, click Revert and they return — only to reappear next reload. The cause is non-deterministic serialized field defaults, often from custom serialization callbacks.

The Symptom

Open a scene. Inspect a prefab instance. Overrides badge shows changes you did not make. Reverting works briefly, but a script change or domain reload re-marks them. Diffs in version control show oscillating values.

What Causes This

Field defaults differ between asset and instance. If your script sets a value in OnAfterDeserialize that does not match the saved default, Unity treats the difference as an override.

Constructor logic. C# constructors do not run for serialized objects. Setting fields there gives you one value at edit time and a different one when the object is loaded fresh.

Reference type defaults. A serialized reference field that is null on the asset but auto-populated in OnEnable produces phantom override.

ISerializationCallbackReceiver mutation. OnBeforeSerialize that modifies state on every save creates oscillation.

The Fix

Step 1: Initialize at declaration.

public class Enemy : MonoBehaviour
{
    [SerializeField] private int hp = 100;     // good - explicit default
    [SerializeField] private float speed = 5f;
    [SerializeField] private List<string> tags = new List<string>();

    // Bad - constructor side-effects
    // public Enemy() { hp = 100; }   // do not do this
}

Step 2: Keep OnAfterDeserialize idempotent.

public class Recipe : MonoBehaviour, ISerializationCallbackReceiver
{
    [SerializeField] private List<Ingredient> ingredients;
    [System.NonSerialized] private Dictionary<string, Ingredient> lookup;

    public void OnBeforeSerialize() { }   // no-op, do not modify serialized state

    public void OnAfterDeserialize()
    {
        // only rebuild the non-serialized cache
        lookup = new Dictionary<string, Ingredient>();
        foreach (var i in ingredients) lookup[i.id] = i;
    }
}

Step 3: Avoid mutating fields in OnEnable.

// BAD: triggers fake override on every load
void OnEnable()
{
    if (icon == null) icon = defaultIcon;   // modifies serialized field
}

// GOOD: leave field as-is, fall back at use site
public Sprite GetIcon() => icon != null ? icon : defaultIcon;

Step 4: Detect spurious overrides with PrefabUtility.

[UnityEditor.MenuItem("Tools/Find Spurious Overrides")]
static void Find()
{
    foreach (var go in Selection.gameObjects)
    {
        var mods = PrefabUtility.GetPropertyModifications(go);
        if (mods != null)
            foreach (var m in mods)
                Debug.Log($"{go.name} : {m.propertyPath} = {m.value}", go);
    }
}

Helps identify which fields are being marked, so you can find the culprit.

Step 5: Reset to default at design time. If certain fields should always inherit asset defaults, right-click each in the Inspector and choose Revert. Then add [SerializeField] with explicit defaults so they cannot drift.

“Defaults at declaration. OnAfterDeserialize touches only cache. No constructor logic. Phantom overrides disappear.”

Related Issues

For prefab apply-all errors, see Prefab Overrides Applied to Asset. For Prefab Mode save, see Prefab Stage Save.

Initialize at declaration. Idempotent OnAfterDeserialize. No mutation in OnEnable. Phantom overrides go away.