NodeSync is a Blender addon that brings Git-based version control to your node trees. It tracks Geometry Nodes, Shader Nodes (materials, worlds, and lights), and the image textures they reference — letting you commit, branch, push, pull, and restore your setups just like source code, with support for any Git remote (GitHub, GitLab, Codeberg, Gitea, Bitbucket, self-hosted, ...) and a live diff overlay.
Each node tree is serialized to a JSON file and tracked individually in Git, giving you a precise history of every change. Branch for experiments, collaborate through any Git host you already use, and restore any version in seconds.
- Commit & restore any version of your Geometry and Shader node trees
- Branch to experiment without breaking your main setup
- Push/pull to/from any Git remote for backup and collaboration
- Auto-push on commit — optionally publish every commit to your remote without a separate click
- Visualize diffs with a color overlay (added / modified / deleted nodes), with an adjustable diff base
- Multi-lane history coloring — each commit is colored by the most-specific branch that reaches it, so the default branch owns shared ancestors
- Resolve merge conflicts when two people edit the same group
- Filter history by the currently viewed node tree, or by Geometry vs Shader
- Selective pull — pick which incoming groups to apply and which to keep local
- Track image textures referenced by shader nodes (opt-in)
- Scene assignments — material slots and Geometry Nodes modifier links travel with the repo, so scene wiring survives across clones, machines, and reverts
- Download or build
nodesync.zip - In Blender: Edit → Preferences → Add-ons → Install → select
nodesync.zip - Enable the addon
The NodeSync panel appears in both the Geometry Node Editor and Shader Editor N-panel under the NodeSync tab.
- Open the NodeSync panel in the Geometry or Shader Node editor
- Set your Project Root folder (defaults to the
.blendfile directory) - Click Init New Project — this creates a
nodes/folder, a.nodesyncconfig file, and runsgit init
- Type a message in the Commit message field
- Click Commit — all Geometry and Shader node trees are serialized to JSON and committed
- Create an empty repo on your Git host of choice (GitHub, GitLab, Codeberg, Gitea, Bitbucket, self-hosted Forgejo, ...).
- Paste the repo URL into the Remote URL field and click Set Remote.
- Add your credentials in Edit → Preferences → Add-ons → NodeSync:
- Personal Access Token — generated from your Git host's user settings (scope: write to your repo). Required for pushing; reading public repos works without one.
- Remote Username — host-specific. Leave blank for GitHub or Azure DevOps; use
oauth2for GitLab;x-token-authfor Bitbucket; your account name for Codeberg / Gitea / Forgejo.
- Click Push ↑ to upload.
SSH URLs (
git@host:user/repo.git) work too — they bypass the token field entirely and use your system SSH agent.
- Paste the Git repository URL and choose a local folder.
- Click Clone from Git Remote — all node trees (geometry and shader) are imported automatically.
| Action | Description |
|---|---|
| Commit | Saves all node trees to JSON and creates a Git commit |
| Checkout | Restores your node trees to any previous commit |
| History | Browse up to 300 commits with author, date, and branch info; the bookmark icon tracks whichever commit is currently loaded |
| History Filter | Show only commits that touched the currently open node tree, or filter by Geometry vs Shader |
| View Diff | Overlays the node graph with colors showing what changed vs the chosen diff base |
| Diff Base | Choose any prior commit (not just HEAD) to diff the current state against |
Diff colors:
- Green — node was added since last commit
- Orange — node was modified
- Red ghost — node was deleted (shown as a placeholder)
NodeSync automatically exports every shader tree when you commit or save your .blend file. The on-disk layout is:
nodes/
_scene_assignments.json ← which objects use which materials / GN groups
MyGeometryGroup.json ← standalone geometry node groups
shader/
MyShaderGroup.json ← standalone shader node groups
materials/
Stone.json ← Material "Stone" node tree
Metal.json
worlds/
HDRI_Sky.json ← World lighting node tree
lights/
AreaLight.json ← Light node tree
textures/
rock_diffuse.png ← copied by "Track Shader Textures"
Alongside the per-tree JSONs, NodeSync writes a single nodes/_scene_assignments.json on every commit and .blend save. It records two things:
- material_slots — for each object, which material name occupies each of its material slots
- modifier_links — for each object, which Geometry Nodes node group each
NODES-type modifier points at
{
"version": 1,
"material_slots": {
"Cube": ["Stone", null, "Metal"],
"Sphere": ["Stone"]
},
"modifier_links": {
"Plane": { "GeometryNodes": "MyGeometryGroup" }
}
}This file is read on every revert, branch switch, pull, and clone. After NodeSync re-imports the node trees, it walks this map and re-attaches materials and GN modifier groups to any object slot that's currently empty. It never overwrites an existing assignment, so reassigning a slot manually after revert is safe — the next pull won't undo it.
Because this lives in git and not in memory, slot assignments survive across Blender sessions, machines, and clones — anyone who checks out the commit gets the same scene wiring.
When Track Shader Textures is enabled (Addon Preferences → Commit Behaviour):
- On each commit, NodeSync walks every Shader Image Texture node in all tracked shader trees
- Each referenced image is copied into
textures/<name>and staged for the commit - Packed images and generated images are written via Blender's render pipeline
- External images are copied verbatim from their source path
This means a fresh git clone + Clone from Git Remote gives a fully reproducible shader setup.
Image names are sanitized before they hit disk (see Filenames on disk). The image's real name is stored inside the JSON, so the node reconnects to the right image on import no matter what the file ended up being called.
Blender allows characters in a data-block name that a filesystem does not — /, :, trailing spaces, trailing dots. Written out verbatim, some of those produce files the OS mangles: on Windows a name ending in a space or a dot cannot be opened, renamed or deleted through Explorer or del, because Win32 silently resolves the path to a different name.
NodeSync sanitizes every name before using it as a filename:
- path separators,
<>:"|?*and control characters become_ - leading and trailing whitespace and trailing dots are stripped
- Windows device names (
CON,NUL,COM1…) are prefixed with_ - a name that had to be changed gets a short hash of the original appended, so two different node groups can never collide on one file
| Data-block name | File written |
|---|---|
Rock Generator |
Rock Generator.json |
Wood/Bar |
Wood_Bar~d1aee731.json |
Wood:Bar |
Wood_Bar~7298d2b5.json |
Metal (trailing space) |
Metal~4acf34d4.json |
Names that need no cleaning are left exactly as they are, so ordinary projects still have readable filenames. The hash only appears where it prevents a real collision or an unusable file.
Projects created by an earlier version still contain files written under the old scheme. When NodeSync detects them it shows a Migrate Project Files button in the Project panel, with a count of what needs changing.
The button opens a dialog listing every rename before anything happens. Applying it renames files to the current scheme, repairs names the OS cannot delete, and stages the result so your next commit records clean renames rather than a pile of deletions. Each file's correct name is recovered from the JSON contents, not from its current filename, so it works even on files whose names were mangled beyond recognition.
Two things worth knowing:
- Commit or stash first. The migration edits files in your project folder.
- Everyone on a shared repo needs 1.3.4 before you push a migration. An older version will re-create the old filenames and undo it.
One case cannot be repaired: if two node groups whose names differed only in an illegal character (A/B and A:B) both existed, the older version already overwrote one with the other on disk. Migration fixes the naming going forward but cannot recover data that was lost before it ran.
- Create Branch from the Branches panel
- Switch Branch — reimports all node trees from the target branch
- Each branch gets a unique color swatch shown in the history list
- History coloring uses branch lanes: shared ancestors are colored by the default branch, and feature branches keep their own color only on commits that aren't reachable from
main
- Push ↑ — sends your commits to the configured Git remote
- Auto-Push on Commit — turn on in addon preferences to push automatically after every commit
- Pull ↓ — fetches and shows a per-group selection dialog so you can choose which incoming changes to apply; reimports changed trees automatically and advances the history bookmark to the new HEAD
- On merge conflicts, a Conflicts panel appears with per-file options:
- Keep Mine — use your local version
- Use Remote — use the incoming version
- Complete Merge / Abort Merge when done
- Per-file import failures during pull surface in the Blender info bar instead of being silently swallowed
| Preference | Description |
|---|---|
| Remote Username | Host-specific username for HTTPS auth. Blank for GitHub / Azure DevOps; oauth2 for GitLab; x-token-auth for Bitbucket; account name for Codeberg / Gitea / Forgejo |
| Personal Access Token | Token used as the password for HTTPS push/pull. Generated in your Git host's user settings; needs write-repository scope |
| Auto-Push on Commit | Automatically push after every commit when a remote is configured |
| Screenshot Node Editor on Commit | Capture a screenshot of the node editor and attach it to the commit |
| Track Shader Textures | Copy image textures from Shader Image Texture nodes into textures/ on commit |
Each node tree is serialized to a JSON file containing:
- The full node interface (inputs/outputs with types, defaults, min/max)
- Every node with its type, position, properties, and socket defaults
- All links between nodes
- For Geometry Nodes trees: the
is_modifierandis_toolflags, so reconstructed groups appear in the modifier dropdown and Tool panel exactly as they did on the source machine - For embedded material/world/light trees:
owner_typeandowner_nametags so the deserializer knows which Blender data-block to attach to
In addition, nodes/_scene_assignments.json records which objects use which materials (per slot) and which GN modifiers point at which node groups, so scene wiring is restored automatically on revert / branch-switch / pull / clone.
On checkout or pull, NodeSync reconstructs every node tree from JSON in dependency order (nested groups first), then applies the scene-assignments map to re-attach materials and modifier groups to any empty slot. Reconstructed groups also get a fake user so they stay visible and don't linger as orphan data-blocks. Socket matching uses Blender's internal socket identifiers for stability, keeping git history clean even when sockets are reordered.
Every write is atomic: content goes to a scratch file in the same directory and is renamed over the target only once it is complete. A write interrupted by a crash, a full disk or a permissions change leaves the previous version untouched instead of a truncated file that fails every later import. The same applies to textures — both Blender's image writer and a plain file copy create the destination before filling it, so a failure partway used to leave a zero-byte file behind and destroy the previous commit's copy.
Failures during export are reported in the Blender info bar and named individually in the system console. Earlier versions printed them to the console only, which meant a commit where every export failed reported "No tracked node groups found in file" — the wrong diagnosis entirely.
Git operations run via subprocess. No external Python dependencies required — only the standard library and Blender's bpy.
- Blender 4.x
- Git installed and on your PATH
- A Personal Access Token from your Git host (GitHub, GitLab, Codeberg, Gitea, Bitbucket, ...) — only needed for pushing or for pulling private repos. Public-repo reads work without one, and SSH URLs use your system SSH agent instead
