Managing Game Assets with Git LFS
Game projects are full of large binary files: sprites, textures, audio, and models. Git was built for text, and handles them badly. This guide explains how Git Large File Storage (LFS) fixes that in our Unity projects, how to set it up correctly from day one, and how to get unstuck when assets don't sync.
Step 1: Understand Why We Use LFS
Every clone of a Git repository contains every version of every file ever committed. For code that's fine: text compresses well and changes are small. A binary file can't be stored as small differences, so each edit stores a complete new copy. Re-export a 40 MB sprite sheet 25 times and every developer downloads a gigabyte of history for that one file, forever.
Git LFS replaces each large file in the repository with a tiny text pointer. The real file lives in separate LFS storage on GitHub, and Git downloads only the versions your current commit needs. A pointer looks like this:
version https://git-lfs.github.com/spec/v1
oid sha256:4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393
size 41943040
- Smaller, faster clones: you download today's art, not every draft of it.
- GitHub limits: GitHub warns on files over 50 MB and rejects files over 100 MB in normal Git. LFS stores them properly.
- File locking: binary files can't be merged, so LFS lets an artist lock a file while editing it (Step 6).
- The trade-off: LFS storage and download bandwidth count against the organization's GitHub quota, so only binary files belong in it.
Step 2: Install Git LFS
Install the LFS extension, then run git lfs install once. It registers LFS with your global Git configuration so every repository on your machine handles pointers automatically.
macOS
brew install git-lfs
Linux
sudo apt install git-lfs
Windows
git lfs version
Git for Windows already includes LFS. If this prints a version, you're set; otherwise reinstall Git with winget install --id Git.Git -e --source winget.
git lfs install
You should see "Git LFS initialized." Run this once per machine, not once per repository.
Step 3: Configure Unity for Version Control
Before any assets are committed, set two project settings in Unity under Edit > Project Settings > Editor:
- Version Control > Mode: Visible Meta Files. Every asset gets a
.metafile holding its unique ID (GUID) and import settings. Scenes and prefabs reference assets by that GUID, so a missing or regenerated.metafile breaks references with "Missing" errors. Always commit an asset and its.metafile together, and move or rename assets inside the Unity editor, not in your file browser. - Asset Serialization > Mode: Force Text. Scenes, prefabs, materials, and ScriptableObjects are saved as readable YAML text, so Git can diff and merge them. These text files stay in regular Git; do not put them in LFS.
The repository's .gitignore must exclude Unity's generated folders. Unity rebuilds them on demand, and committing them causes constant conflicts and enormous repositories. Start from GitHub's Unity.gitignore (opens in a new tab); at minimum it ignores:
/[Ll]ibrary/
/[Tt]emp/
/[Oo]bj/
/[Bb]uild/
/[Bb]uilds/
/[Ll]ogs/
/[Uu]ser[Ss]ettings/
/[Mm]emoryCaptures/
*.csproj
*.sln
*.pidb
*.userprefs
See Repository Standards for the rest of the required repository files.
Step 4: Track Binary File Types
LFS only handles file types you tell it to track. git lfs track writes a rule into .gitattributes at the root of the repository. That file must be committed before the matching assets, because a file committed before its rule exists goes into normal Git history.
git lfs track "*.psd"
git lfs track "*.aseprite"
git add .gitattributes
git commit -m "build(lfs): track Photoshop and Aseprite files"
For a new Unity project, skip the one-at-a-time commands and commit this studio baseline as .gitattributes instead. It covers the binary formats we use for 2D and 3D work:
# Normalize line endings for text files
* text=auto
# Unity YAML (.unity, .prefab, .asset, .meta) is deliberately not listed:
# it stays in regular Git as text so it can be diffed and merged.
# Images and sprites
*.png filter=lfs diff=lfs merge=lfs -text
*.jpg filter=lfs diff=lfs merge=lfs -text
*.jpeg filter=lfs diff=lfs merge=lfs -text
*.gif filter=lfs diff=lfs merge=lfs -text
*.tga filter=lfs diff=lfs merge=lfs -text
*.tif filter=lfs diff=lfs merge=lfs -text
*.tiff filter=lfs diff=lfs merge=lfs -text
*.exr filter=lfs diff=lfs merge=lfs -text
*.hdr filter=lfs diff=lfs merge=lfs -text
*.psd filter=lfs diff=lfs merge=lfs -text lockable
*.ase filter=lfs diff=lfs merge=lfs -text lockable
*.aseprite filter=lfs diff=lfs merge=lfs -text lockable
*.kra filter=lfs diff=lfs merge=lfs -text lockable
# Audio
*.wav filter=lfs diff=lfs merge=lfs -text
*.mp3 filter=lfs diff=lfs merge=lfs -text
*.ogg filter=lfs diff=lfs merge=lfs -text
*.aif filter=lfs diff=lfs merge=lfs -text
*.aiff filter=lfs diff=lfs merge=lfs -text
*.flac filter=lfs diff=lfs merge=lfs -text
# Video
*.mp4 filter=lfs diff=lfs merge=lfs -text
*.mov filter=lfs diff=lfs merge=lfs -text
*.webm filter=lfs diff=lfs merge=lfs -text
# 3D models and source files
*.fbx filter=lfs diff=lfs merge=lfs -text
*.obj filter=lfs diff=lfs merge=lfs -text
*.blend filter=lfs diff=lfs merge=lfs -text lockable
# Fonts, packages, and compiled libraries
*.ttf filter=lfs diff=lfs merge=lfs -text
*.otf filter=lfs diff=lfs merge=lfs -text
*.unitypackage filter=lfs diff=lfs merge=lfs -text
*.dll filter=lfs diff=lfs merge=lfs -text
*.so filter=lfs diff=lfs merge=lfs -text
*.dylib filter=lfs diff=lfs merge=lfs -text
*.a filter=lfs diff=lfs merge=lfs -text
*.zip filter=lfs diff=lfs merge=lfs -text
Verify that a file is going to LFS before you commit it. git check-attr should report filter: lfs, and after committing, the file should appear in git lfs ls-files:
git check-attr filter -- Assets/Art/Characters/Hero.png
git lfs ls-files
Small pixel-art sprites are still tracked: they are re-exported often, and many small binaries add up just like one large one.
Step 5: Work with LFS Day to Day
Once LFS is installed and .gitattributes is committed, you use the normal commands from the Daily Git Terminal Workflow: clone, pull, add, commit, and push handle LFS files automatically. A few extra commands help you see what's going on:
| Command | What it tells you |
|---|---|
git lfs status | Which staged files will be stored as LFS objects, and which as normal Git files. |
git lfs ls-files | Every LFS file in the current commit. * means downloaded; - means only the pointer is present. |
git lfs track | The tracking rules currently in effect. |
git lfs pull | Downloads any LFS content missing from your current checkout. |
git lfs prune | Frees disk space by deleting old local LFS versions that are safely on GitHub. |
Adding a new file type later: track it and commit .gitattributes first. Files of that type that were already committed stay in regular Git history; run git add --renormalize . and commit to convert the current versions to LFS going forward. Moving old history into LFS requires git lfs migrate, which rewrites every commit and forces every team member to re-clone, so it is a lead-approved operation only.
Step 6: Lock Files Before Editing Binaries
Two people can edit the same C# file and Git will merge their work. Two people editing the same .psd means one person's work is thrown away: there is no way to merge two versions of an image. File locking prevents that. In the baseline above, source art files are marked lockable, which makes them read-only on disk until you lock them.
git lfs locks # who has what locked
git lfs lock Assets/Art/Source/Hero.aseprite # claim the file before editing
git lfs unlock Assets/Art/Source/Hero.aseprite # release it after your changes are pushed
- Pull first, so you're editing the latest version.
- Lock the file. If someone else holds the lock, talk to them; don't work around it.
- Edit, commit, and push.
- Unlock. Pushing doesn't release locks automatically.
A lead can release an abandoned lock with git lfs unlock --force <path>. Check with the lock owner first.
Step 7: Merge Scenes and Prefabs with Smart Merge
Scene and prefab conflicts are hard to resolve by hand because the YAML is long and full of IDs. Unity ships a merge tool, UnityYAMLMerge (Smart Merge), that understands the format. Register it once in your global Git config. Open the config file in your editor:
git config --global --edit
Paste this block at the end of the file, replace the path with your editor's UnityYAMLMerge location from the tabs below, then save and close:
[merge]
tool = unityyamlmerge
[mergetool "unityyamlmerge"]
trustExitCode = false
cmd = '<path to UnityYAMLMerge>' merge -p "$BASE" "$REMOTE" "$LOCAL" "$MERGED"
macOS
/Applications/Unity/Hub/Editor/<version>/Unity.app/Contents/Tools/UnityYAMLMerge
Linux
/home/<you>/Unity/Hub/Editor/<version>/Editor/Data/Tools/UnityYAMLMerge
Windows
C:/Program Files/Unity/Hub/Editor/<version>/Editor/Data/Tools/UnityYAMLMerge.exe
Use forward slashes in the Git config file, even on Windows.
When a rebase or merge stops on a .unity or .prefab conflict, run git mergetool. Smart Merge resolves what it can, then you continue as in the daily workflow guide. Open the scene in Unity and check it before you commit. The best prevention is coordination: agree who owns a scene for the day, and split big scenes into prefabs so people rarely touch the same file.
Step 8: Troubleshoot LFS Sync Issues
Most LFS problems come from one of three causes: LFS isn't installed on the machine, a file was committed before it was tracked, or authentication or quota problems are blocking the download. Find your symptom below.
Sprites are pink or missing, or files are tiny text files
You have the pointers but not the content, usually because the repository was cloned before git lfs install was run. Open the file in a text editor: if it starts with version https://git-lfs.github.com/spec/v1, that's a pointer. Fix it with:
git lfs install && git lfs pull
Push rejected: "exceeds GitHub's file size limit of 100.00 MB"
A large file was committed to regular Git because it wasn't tracked yet. If you haven't pushed the commit, undo it (your changes are kept), track the type, and commit again:
git reset --soft HEAD~1
git lfs track "*.wav"
git add .gitattributes
git add --renormalize .
git commit
If the file is buried in older commits, ask a lead. Removing it means rewriting history with git lfs migrate import.
"Encountered N files that should have been pointers, but weren't"
Files that match an LFS rule were committed as regular Git files, typically by someone without LFS installed. Convert them and commit the result:
git add --renormalize .
git commit -m "build(lfs): move tracked binaries into LFS"
"This repository is over its data quota" or bandwidth errors
The organization has used up its GitHub LFS storage or bandwidth. This isn't something to fix locally. Don't delete files or force-push; tell your lead, who can review usage and buy more data packs.
Authentication errors or "smudge filter lfs failed"
LFS downloads use the same credentials as Git. Check that you can reach the repository at all with git fetch and, if you use the GitHub CLI, gh auth status. For details, run the failing command with tracing on and read the most recent LFS log:
macOS
GIT_TRACE=1 git lfs pull
git lfs logs last
Linux
GIT_TRACE=1 git lfs pull
git lfs logs last
Windows
$env:GIT_TRACE=1; git lfs pull; Remove-Item Env:GIT_TRACE
git lfs logs last
Cloning takes too long or fills the disk
Clone with only the pointers, then download just the folders you need:
macOS
GIT_LFS_SKIP_SMUDGE=1 git clone git@github.com:Shattered-Vales-Studio/<repo>.git
cd <repo>
git lfs pull --include="Assets/Art/Characters/**"
Linux
GIT_LFS_SKIP_SMUDGE=1 git clone git@github.com:Shattered-Vales-Studio/<repo>.git
cd <repo>
git lfs pull --include="Assets/Art/Characters/**"
Windows
$env:GIT_LFS_SKIP_SMUDGE=1; git clone git@github.com:Shattered-Vales-Studio/<repo>.git; Remove-Item Env:GIT_LFS_SKIP_SMUDGE
cd <repo>
git lfs pull --include="Assets/Art/Characters/**"
Run git lfs prune occasionally to remove old local copies of assets you no longer need.