You cloned the repo, opened assets/hero-image.psd in Photoshop, and it crashed instantly. Run file assets/hero-image.psd and the truth shows up: it is not a PSD at all, it is an ASCII text file whose first line reads version https://git-lfs.github.com/spec/v1. That three-line text blob is a Git LFS pointer. The real binary lives on the LFS server and the smudge filter that should have swapped the pointer for the binary on checkout never ran.
Fastest fix (works for the most common cause):
git lfs install # register the smudge/clean filters
git lfs pull # download every LFS object for the current commit
If that returns the file, you are done. If git lfs pull errors or still leaves pointer text, the smudge filter is being skipped or the LFS server is refusing you — work through the diagnosis table below to find which bucket you are in. As of Git LFS 3.7.x (June 2026), the commands and flags here are current.
Which bucket are you in
Run these three checks first; the result tells you which cause section to read.
| Check | Output that points to the cause | Cause |
|---|---|---|
git lfs version | command not found or no output | LFS not installed (cause 1) |
git config filter.lfs.smudge | empty, or contains --skip | smudge filter not registered / skipped (causes 1, 2) |
git lfs pull 2>&1 | tail -20 | batch response: This repository is over its data quota. or 401/403 | server quota or auth (causes 3, 4) |
git lfs env shows wrong Endpoint= | URL points at the old host after a migration | wrong endpoint (cause 5) |
git lfs ls-files is empty for files you expect | the file is a plain blob, not LFS | .gitattributes rule added too late (cause 6) |
Common causes
Ordered by hit rate, highest first.
1. Git LFS is not installed or git lfs install was never run
The repo was cloned on a machine where the git-lfs binary is missing, or it is installed but git lfs install (which writes the global filter.lfs.* config) was never run. Git sees the filter=lfs attribute in .gitattributes, finds no filter registered, and checks out the raw pointer text.
How to spot it: git lfs version returns “command not found” or a version older than the repo needs. git config filter.lfs.smudge returns empty.
2. The smudge filter is set to skip (env var or persistent config)
Two flavors of the same problem. The transient one is GIT_LFS_SKIP_SMUDGE=1 in the shell or CI step that ran the clone. The persistent one is more confusing: someone ran git lfs install --skip-smudge, which writes filter.lfs.smudge = git-lfs smudge --skip -- %f and filter.lfs.process = git-lfs filter-process --skip into your global config, so every clone forever lands on pointer text until you undo it.
How to spot it: echo $GIT_LFS_SKIP_SMUDGE prints 1, or git config filter.lfs.smudge contains --skip. CI logs that show a fast clone with no LFS download are another tell.
3. LFS server quota exceeded, rate-limited, or returning 403
On GitHub, exceeding the LFS bandwidth quota disables LFS for the rest of the billing month. The batch API then returns an error and the smudge filter falls back to writing pointer text. As of June 2026, GitHub Free and Pro accounts each get 10 GiB of LFS storage and 10 GiB of bandwidth per month; Team and Enterprise Cloud get 250 GiB each. GitHub retired the old prepaid “data packs” and now uses metered billing (you pay per GiB over the included amount), so the old “buy a data pack” advice no longer applies.
How to spot it: git lfs pull prints batch response: This repository is over its data quota. Or run GIT_CURL_VERBOSE=1 git lfs pull 2>&1 | grep -E "403|429|quota|rate limit" and watch for any non-200 response from the LFS endpoint.
4. LFS authentication is missing or scoped wrong
The credential used for clone has no LFS read permission, the stored token expired, or a self-hosted/Enterprise server in private mode refuses the batch POST. git lfs pull then reports 401 Unauthorized or 403 Forbidden on /info/lfs/objects/batch.
How to spot it: git lfs pull 2>&1 | head -20 shows a 401/403. On GitHub a fine-grained or classic token needs contents: read (or repo scope) for LFS. For Enterprise private mode, switching the remote to SSH often resolves the batch 403.
5. LFS endpoint URL is wrong after a repo migration
The repo moved from GitHub to GitLab (or to a self-hosted LFS server), but .lfsconfig or remote.origin.lfsurl still points at the old host, so downloads quietly hit the wrong server.
How to spot it: git lfs env — check the Endpoint= line and confirm it matches the current host. git lfs push --dry-run will also surface a connection error to the stale host.
6. .gitattributes LFS track rule is missing or was added too late
The file was committed as a normal blob before the filter=lfs rule was added to .gitattributes. The current .gitattributes has the rule, but the historical blob was never migrated, so older revisions check out as plain (often truncated-looking) content while new ones are pointers — a confusing mix.
How to spot it: git lfs ls-files does not list the file you expect. git log --oneline -- .gitattributes | tail -5 shows the track rule was added after the file’s first commit. git cat-file -p HEAD:path/to/file | head -1 that is NOT a version https://git-lfs... line confirms it is a plain blob.
Shortest path to fix
Step 1: Install and initialize Git LFS
# macOS
brew install git-lfs
# Debian/Ubuntu
sudo apt-get install git-lfs
# or download from https://git-lfs.com
git lfs install # registers the smudge/clean filters in your global config
Step 2: Pull all LFS objects for the current checkout
git lfs pull
This fetches every LFS object referenced by the current commit and runs the smudge filter to replace pointer files with real content. To limit bandwidth, scope it:
git lfs pull --include="assets/hero-image.psd"
git lfs pull --include="assets/**"
Step 3: Undo a skipped smudge filter
If git config filter.lfs.smudge contains --skip, re-enable it and re-checkout:
git lfs install --force # rewrites filter.lfs.* without --skip
unset GIT_LFS_SKIP_SMUDGE # clear the env var for this shell
git lfs pull # download objects
git checkout -- . # re-run smudge on the working tree
Step 4: Fix authentication
# Refresh the cached credential, then retry
git credential reject <<EOF
protocol=https
host=github.com
EOF
git lfs pull
If a self-hosted/Enterprise server returns 403 on the batch endpoint over HTTPS, switch the remote to SSH:
git remote set-url origin git@github.example.com:org/repo.git
git lfs pull
Step 5: Fix the LFS endpoint URL after a migration
git lfs env # confirm the current Endpoint=
git config lfs.url https://gitlab.example.com/org/repo.git/info/lfs
git lfs pull
Or pin it in .lfsconfig at the repo root so it survives future migrations:
[lfs]
url = https://gitlab.example.com/org/repo.git/info/lfs
Step 6: Migrate historical plain blobs to LFS
For the cause-6 case where old revisions are plain blobs:
git lfs migrate import --include="*.psd" --everything
git push --force-with-lease --all
git push --force-with-lease --tags
This rewrites history, so notify every teammate to re-clone (or hard-reset) afterward.
Step 7: Confirm it is fixed
file assets/hero-image.psd # should say "Adobe Photoshop Image", not "ASCII text"
git lfs status # tracked files should NOT be listed as pointers
git lfs ls-files # each file should show a "*" (object is present locally)
If a file still looks wrong, capture a debug log of the smudge run:
GIT_TRACE=1 GIT_LFS_LOG_FILE=lfs-debug.log git checkout -- assets/hero-image.psd
# then inspect lfs-debug.log for the exact server response
Prevention
- Put
git lfs installas the first line of your repo’s setup instructions and onboarding scripts. Missing this step turns every LFS file into pointer text for that contributor. - In CI, the
actions/checkoutdefault is stilllfs: falseas of June 2026. Setlfs: true, or add an explicitgit lfs pullstep after checkout (and pre-installgit-lfsin custom images —ubuntu-latestships it, but pin a known version if you depend on a recent flag). - Use plain
git clonefor LFS repos. Parallel LFS download is built into core clone now;git lfs cloneis deprecated and prints a warning. For huge repos, clone withGIT_LFS_SKIP_SMUDGE=1and then run a targetedgit lfs pull --include=...to control bandwidth. - Commit a
.lfsconfigwith an explicitlfs.urlso the endpoint survives remote migrations. - Watch your LFS bandwidth before it hits the monthly cap (10 GiB on GitHub Free/Pro as of June 2026) — once disabled, LFS stays off until the next billing cycle.
- Run
git lfs trackto add the.gitattributesrule BEFORE committing any large binary, never after.
FAQ
Q: How do I tell whether a file is an LFS pointer or the real file without opening it?
A: git cat-file -p HEAD:assets/hero-image.psd | head -3. If the first line is version https://git-lfs.github.com/spec/v1 it is a pointer. You can also run file <path>: a pointer reports as ASCII text even though the file should be binary.
Q: git lfs pull says “Object does not exist on the server” but the file is clearly tracked. What happened?
A: The binary was never pushed — someone committed the pointer but skipped git lfs push origin --all from their machine. They need to push it, or you must recover the binary from another source. This is different from a quota error, which says This repository is over its data quota.
Q: We blew past GitHub’s LFS bandwidth quota. What are the options?
A: As of June 2026 GitHub uses metered billing, so the simplest fix is to add a payment method and pay the per-GiB overage (the old prepaid data packs are gone). To avoid GitHub bandwidth entirely, point lfs.url in .lfsconfig at a self-hosted LFS store (for example an S3/R2-backed lfs-s3 server or a Gitea LFS endpoint) and push the objects there.
Q: What is the difference between git lfs fetch and git lfs pull?
A: git lfs fetch only downloads objects into the local cache (.git/lfs/objects/); it does not touch your working tree. git lfs pull is git lfs fetch plus git lfs checkout, so it also writes the real content into your files. Use pull for everyday work.
Q: Does Git LFS work with shallow or partial clones?
A: Yes, with caveats. git clone --depth 1 followed by git lfs pull resolves the shallow commits; objects from deeper history stay unreachable until you unshallow. Partial clone (--filter=blob:none) interacts better since Git LFS 3.6+, but if pointers persist run an explicit git lfs pull after checkout.