BlueByte
GH001Fixed

GitHub: push rejected — GH001 Large files detected (over 100 MB)

By Haneul SeoUpdated September 5, 20265 min

Hi, it's BlueByte. You push, and GitHub rejects it: remote: error: GH001: Large files detected. Your history is fine locally, but somewhere in it is a file bigger than GitHub allows, and the server refuses the whole push. The symptom is a push that ends in (pre-receive hook declined) and names a file over GitHub's size limit. We'll walk through the limits, why deleting the file isn't enough, how to find the exact object, fix it two ways depending on where it lives, and keep it out for good.

What "GH001: Large files detected" means

GitHub enforces file-size limits on the server, so the push is rejected before anything is stored:

remote: error: GH001: Large files detected. You may want to try Git Large File Storage - https://git-lfs.github.com.
remote: error: Trace: 9f8c1a2b3c4d5e6f
remote: error: See https://gh.io/lfs for more information.
remote: error: File models/checkpoint.bin is 210.44 MB; this exceeds GitHub's file size limit of 100.00 MB
To github.com:acme/ml.git
 ! [remote rejected] main -> main (pre-receive hook declined)
error: failed to push some refs to 'github.com:acme/ml.git'

Two thresholds matter. GitHub warns on any file over 50 MiB and hard-blocks anything over 100 MiB — that block is what you're hitting. (The web upload UI caps at 25 MiB, a separate limit.) GitHub also recommends repositories stay under 1 GB, and strongly under 5 GB. The fix is to get the file under 100 MiB in history, or move it to Git LFS.

Why deleting the file and committing again doesn't help

The instinct is to git rm the file, commit, and push again. It won't work. Git stores every version of every file across the whole history, and a push sends all the commits the server doesn't have yet. The oversized blob is still in the earlier commit, so the pre-receive hook still sees it and still rejects the push. To fix this you have to remove the file from the commit that introduced it — not just from the tip.

Find which commit and which file is oversized

The error names the file, but if you have several, list the biggest blobs in history:

git rev-list --objects --all \
  | git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' \
  | awk '/^blob/ {print $3, $4}' | sort -n | tail -5
54182391 src/data/sample.csv
220612334 models/checkpoint.bin

The size is in bytes, so 220612334 is about 210 MB — that's the culprit. Note the path; you'll target it next.

If it's only in your latest commit: drop it and re-commit

If you committed the file just now and haven't shared the branch, the fix is one commit:

git rm --cached models/checkpoint.bin
echo "models/checkpoint.bin" >> .gitignore
git commit --amend -C HEAD
git push

--cached removes the file from the commit but keeps it on disk; the amend rewrites your single tip commit without the blob. If the file was added across the last few commits, the migrate approach below is safer than amending each one.

If it's buried in history: rewrite with git lfs migrate

When the blob is several commits back, rewrite history to move it into Git LFS in one step:

git lfs migrate import --include="*.bin" --everything
git push --force-with-lease

git lfs migrate import replaces the matching files across history with small LFS pointer files and records the pattern in .gitattributes, so the real bytes live in LFS instead of the Git objects. --everything covers all branches and tags. This rewrites history — every commit after the change gets a new SHA — so it is destructive: you must force-push, and anyone else with a clone has to re-clone or hard-reset. Coordinate before running it on a shared branch. If you'd rather delete the file entirely than keep it, git filter-repo --path models/checkpoint.bin --invert-paths strips it instead.

A real case: a 210 MB checkpoint committed last week

A teammate committed models/checkpoint.bin (210 MB) five commits ago and pushed a feature branch that failed with GH001. You confirm the branch isn't shared yet, install Git LFS with git lfs install, then run git lfs migrate import --include="*.bin" --everything. The command rewrites those five commits, adds *.bin filter=lfs diff=lfs merge=lfs -text to .gitattributes, and leaves a pointer in place of the blob. git push --force-with-lease now succeeds — GitHub accepts the pointer (a few hundred bytes) and stores the real file in LFS.

Confirm the push succeeds and the file is tracked

Check that Git now treats the file as an LFS object:

git lfs ls-files
a1b2c3d4e5 * models/checkpoint.bin

A line here means the file is a pointer backed by LFS, not a raw blob. git cat-file -s HEAD:models/checkpoint.bin returning a tiny size — the pointer is around 130 bytes — confirms the history no longer carries the full file.

Keep large files out of Git

Decide up front what belongs in LFS and track it before the first commit: git lfs track "*.bin" writes the .gitattributes rule so new large files never enter Git history as raw blobs. Add build artifacts, datasets, and model files to .gitignore so they aren't committed by accident. For release binaries you don't need versioned, attach them to a GitHub Release instead of committing them. A pre-commit hook that rejects files over about 50 MB catches the mistake locally, before it becomes a rejected push.

How this differs from "pack exceeds maximum allowed size"

GH001 is about a single file over 100 MiB. A different rejection — remote: fatal: pack exceeds maximum allowed size — is about the total size of the push, not one file. If the message names a file and a per-file limit, it's GH001 and LFS or a history rewrite is the fix; if it names the pack size, push fewer commits at a time or split the push into smaller chunks.

Related questions

I deleted the file and committed, but the push is still rejected.

The blob is still in the earlier commit that added it. A push sends the whole history, so the oversized object is still there. Remove it from the commit that introduced it — amend if it's the tip, or git lfs migrate / git filter-repo if it's deeper.

What's the difference between git lfs track and git lfs migrate?

git lfs track only affects files committed from now on — it writes the .gitattributes rule. git lfs migrate rewrites existing history to convert files already committed. You need migrate to fix a file that's already in a commit.

Do I have to force-push after git lfs migrate?

Yes. migrate rewrites history and changes every affected commit's SHA, so a normal push is rejected as non-fast-forward. Use git push --force-with-lease, and warn collaborators first.

Can I raise the 100 MiB limit?

No — it's a hard server-side limit for regular Git objects and can't be increased. Version large files through Git LFS instead, or attach release binaries to a GitHub Release.

Will my teammates' clones break after I migrate and force-push?

Their existing clones diverge from the rewritten history. After you force-push, they should re-clone or run git fetch then git reset --hard origin/<branch>. Coordinate the timing so no one loses in-flight work.

References

Haneul Seo

Infrastructure engineer · 10+ years running Linux fleets

More in this category

detected dubious ownershipFixed

Git: fatal: detected dubious ownership in repository

Since the CVE-2022-24765 fix in Git 2.35.2, Git refuses to read a repository whose working tree or .git directory is owned by a different user than the one running the command. It shows up in containers, CI jobs, sudo sessions and shared drives. Fix the ownership if the repo should be yours, or add the exact path to safe.directory in your global config — never in the repo's own config, which Git ignores for this.

Git
failed calling webhookFixed

Kubernetes: Internal error occurred: failed calling webhook

An admission webhook sits in front of your write, the API server could not get an answer out of it, and failurePolicy: Fail turned that silence into a rejection. The tail of the message is the whole diagnosis: context deadline exceeded means the call went nowhere, no endpoints available means nothing is running, and an x509 line means the API server does not trust the webhook's certificate. Each has a different fix, and none of them is your manifest.

Kubernetes
1205Fixed

MySQL: ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

A statement waited the full innodb_lock_wait_timeout for a row lock another transaction is still holding, and gave up. sys.innodb_lock_waits names the blocking session and hands you the KILL statement, and a blocking_query of NULL means the blocker is idle on an open transaction. The detail most retry loops get wrong: by default only the timed-out statement is rolled back, so your transaction is still open and still holds every lock it took earlier.

MySQL
MISCONFFixed

Redis: MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk

Reads keep working and every write is rejected, because the last background save failed and stop-writes-on-bgsave-error defaults to yes. The log names the real cause — no space, a dir the redis user can't write, a read-only mount at rename time, or fork failing with Cannot allocate memory. Fix the cause, run one BGSAVE, and writes come back on their own with no restart: rdb_last_bgsave_status flips from err to ok. Setting stop-writes-on-bgsave-error no restores writes instantly but leaves the snapshot broken, so treat it as a deliberate trade, not the fix.

Redis
FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memoryFixed

Node.js: FATAL ERROR: Reached heap limit — JavaScript heap out of memory (exit 134)

The V8 heap has its own ceiling, derived from system memory and the Node release, and it is often far below the RAM you have; when a build or server reaches it, V8 aborts with FATAL ERROR: Reached heap limit and exit code 134. Read the real limit with v8.getHeapStatistics().heap_size_limit, then raise it with --max-old-space-size (in MiB) or NODE_OPTIONS for a large workload, size it below the cgroup limit inside containers, and use --heapsnapshot-near-heap-limit to catch a leak in a long-running process. Exit 137 with no FATAL ERROR line is a container kill, not this.

Node.js
exec /docker-entrypoint.sh: exec format errorFixed

Docker: "exec format error" when the container starts — wrong-platform image, no emulator, or a script with no shebang

The container exits on its first instruction with exec format error — the kernel's ENOEXEC, meaning the file exists but cannot be executed here. In practice that is an image built on one CPU architecture (an Apple-silicon Mac produces linux/arm64) and run on another (an x86_64 server) with no QEMU handler registered in binfmt_misc, or an entrypoint script whose first line is not a shebang. uname -m, docker image inspect and ls /proc/sys/fs/binfmt_misc tell the causes apart; the fix is an explicit docker buildx build --platform (or a manifest list for both), QEMU registration or --platform when you mean to emulate, and a #!/bin/sh line for the script.

Docker