Quick answer: A sub-resource without resource_local_to_scene set is referenced by path. If that path points to a cached external resource — or to nowhere at all — your in-scene edits get replaced by the cached version on save. Enable Local to Scene in the inspector for per-scene resources, save shared ones as standalone .tres files, call take_over_path on runtime duplicates, and use ResourceSaver.save with FLAG_BUNDLE_RESOURCES when you need to embed everything.

Here is how to fix Godot sub-resources that revert to a previous state after you save a PackedScene or share it via version control. You tweak a material on a MeshInstance3D in the editor, save the scene, close Godot, reopen, and the material is back to its original colors. Or your teammate pulls your scene from git and reports that all your tweaked noise textures are gone — the scene loads with the asset library defaults. The cause is almost always Godot deciding that your sub-resource is “the same” as one in its cache and substituting the cached version on load.

The Symptom

You edit a sub-resource attached to a node in your scene — a Material, a Curve, a NoiseTexture, a custom Resource subclass — and one of:

Edits revert when you reload the scene. Save, close, reopen the scene in the editor. The sub-resource is back to a previous state. The .tscn file on disk shows your edits in the text, but the in-memory scene loads with the cached version anyway.

Edits travel only on your machine. Commit the .tscn, your teammate pulls, opens the scene, and sees the un-tweaked resource. Your editor has the resource cached locally; theirs loads from disk and gets a different version.

Two scenes referencing the same resource step on each other. You instance the same custom Resource into two scenes, edit one, and the other changes too. Saving either scene saves the latest edits over the other’s state.

Runtime duplicates do not stick. You call resource.duplicate() at runtime, modify the duplicate, and reload — ResourceLoader.load gives you the original from cache, not your modified duplicate.

What Causes This

Sub-resources are referenced by path. When a scene file embeds a sub-resource, it stores it either as inline data ([sub_resource type="..." id="..."]) or as an external reference ([ext_resource path="res://..."]). Inline sub-resources are saved into the .tscn; external ones are loaded from the referenced path on scene load. If the path resolves to a cached version different from your edits, the cache wins.

resource_local_to_scene defaults to false. A resource whose resource_local_to_scene is false is shared across every scene that references it. Editing it in one scene mutates the cached version that other scenes see. The saver tries to deduplicate — if your inline edit looks “close enough” to a cached external resource, it replaces your inline copy with the external reference on save.

ResourceLoader caches by path. Once a resource is loaded from a path, ResourceLoader remembers it. Subsequent loads of the same path return the cached object, not a fresh load from disk. This is the source of most “edits revert” bugs — the disk version is correct but the cache hands out the old object.

Empty resource_path on duplicates. When you call duplicate(), the result has an empty resource_path. Saving the scene that contains the duplicate either embeds it inline (good) or, if a deduplicator decides it matches a cached resource, replaces your duplicate with a reference to that cached resource (bad).

take_over_path not called on runtime duplicates. After duplicating, the cache still serves the original on subsequent ResourceLoader.load calls. Without take_over_path, your duplicate exists alongside the original but anyone loading the path gets the original.

The Fix

Step 1: Enable Local to Scene. In the inspector, find the sub-resource (e.g., the Material on your MeshInstance3D), expand its properties, and toggle Local to Scene to On. This is a per-resource property, not per-node. Once enabled, the resource is embedded inside the .tscn, and every instance of the scene gets its own copy.

# Mark a resource as Local to Scene from code
extends Node3D

func _ready() -> void:
    var mesh: MeshInstance3D = $MeshInstance3D
    var mat: StandardMaterial3D = mesh.get_active_material(0)

    if mat == null:
        return

    if not mat.resource_local_to_scene:
        mat.resource_local_to_scene = true
        # Setup local-to-scene gives this instance a unique copy
        mat.setup_local_to_scene()
        mesh.set_surface_override_material(0, mat)

setup_local_to_scene is the runtime equivalent of toggling the inspector checkbox. After the call, this material instance is owned by this scene instance and will not be shared with anyone else.

Step 2: Save shared resources as standalone .tres files. If a resource is meant to be shared (a default material applied to every wall in a level), give it a real path on disk and reference it explicitly. The shared version becomes the source of truth and per-scene overrides are obviously local edits.

func save_default_material() -> void:
    var mat := StandardMaterial3D.new()
    mat.albedo_color = Color(0.4, 0.45, 0.5)
    mat.metallic = 0.1

    var path := "res://materials/wall_default.tres"
    var err := ResourceSaver.save(mat, path)
    if err != OK:
        push_error("Save failed: %s" % err)
        return

    # Tell the cache: this object now owns that path
    mat.take_over_path(path)

Once saved, scenes that reference res://materials/wall_default.tres get the shared resource. Per-scene tweaks should duplicate the resource and mark the duplicate Local to Scene; only the unmodified shared version stays externally referenced.

take_over_path on Runtime Duplicates

If your code duplicates a resource at runtime and expects subsequent loads to return the duplicate, take_over_path is mandatory:

func customize_for_player(player_id: int) -> Resource:
    var base := ResourceLoader.load(
        "res://characters/base_outfit.tres")
    var custom: Resource = base.duplicate(true)

    # Modify the duplicate
    custom.tint = player_color(player_id)

    # Replace the cached version so future loads get our copy
    var path := "res://characters/p%d_outfit.tres" % player_id
    custom.take_over_path(path)

    # Optional: persist to disk for next session
    ResourceSaver.save(custom, path)
    return custom

duplicate(true) recursively duplicates sub-resources too, which is usually what you want for a fully-detached copy. take_over_path claims the path in the cache so any other code calling ResourceLoader.load on it gets the customized version, not the base.

FLAG_BUNDLE_RESOURCES for Self-Contained Saves

If you need to save a scene or resource that pulls in everything as inline data — for example, exporting a user-created level to a single file — use FLAG_BUNDLE_RESOURCES on ResourceSaver.save:

func export_user_level(scene: PackedScene, out: String) -> void:
    var err := ResourceSaver.save(scene, out,
        ResourceSaver.FLAG_BUNDLE_RESOURCES)
    if err != OK:
        push_error("Export failed")

The bundle flag inlines every external reference into the output file. The result is portable — no broken paths when the file moves to another machine — at the cost of duplicating shared resources for every save.

“Sub-resources are paths in disguise. If you want your edits to stick, either embed them in the scene with Local to Scene or give them a real path of their own.”

Related Issues

If your scene saves correctly but loads with missing references, see Broken Resource References on Load. For .tres merge conflicts in version control, check Resolving .tres Merge Conflicts.

Local to Scene checkbox — one click that prevents a week of “why did my edits disappear” debugging.