Build1 publisher3 min readPublished
One blob in the index blocks git's unreleased --drop-filtered from reclaiming any disk
A 397 MiB repo filtered to 36 MiB at clone time grew back to 360 MiB after ten checkouts of old commits. The reclaim command merged for git 2.56 exits 128 when the index still lists a filtered blob.
The Engineer · Build desk

What happened
- A 397 MiB repository cloned with --filter=blob:limit=1m started at 36 MiB of .git and reached 360 MiB after ten checkouts of older commits, with no fetch and no pull issued.
- git repack --drop-filtered, the first housekeeping option that removes refetched promisor blobs, merged to git's master on 25 August 2026 and is expected in 2.56, which has not shipped.
- Run on main with the assets present, it printed a fatal error naming assets/asset1.bin as referenced by the current index, exited 128 and dropped nothing, and --dry-run aborted the same way.
- Sparse-checkout with skip-worktree set on all twelve asset entries hit the same refusal, and the sparse index expanded to a full index before failing.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- constraint The one command that gives the disk back is unavailable to partial-clone plus sparse-checkout monorepos unless their index lists no filtered blob at all, which is the configuration those teams run by default.
- cost Storage plans sized on clone-time footprint under-count by an order of magnitude on this repo shape, and the bill lands on developer laptops and CI cache volumes rather than on the server that served the filter.
- exposure Any mirroring script or repo-size dashboard that walks objects hydrates the clone it was only supposed to measure, so the hydration arrives from automation nobody was watching.
- contradiction The documented refusal reads as selective, so anyone planning a housekeeping job from the docs will size the work wrongly until they run it and get exit 128.
The refetched blobs survive housekeeping by design. They are promisor objects and they are reachable, and until this change git had no mechanism for telling them apart from objects you meant to keep [5]. What `repack --drop-filtered` adds, according to its documentation, is a rebuild of the promisor pack without those objects followed by deletion of the now-redundant old packs [13]. That explains the cost profile. On a branch with no large files, the dry run listed 120 blobs and the real repack finished in 0.036 seconds, leaving a 372K `.git` [12]. Checking `main` back out refetched the twelve assets the working tree needs and settled at 36.2 MiB, with a clean `fsck` and three assets compared byte for byte against the server's copies [14].
The guard is keyed on index entries, not on files on disk. With cone-mode sparse-checkout excluding `assets`, the working tree held zero of those files, the index held twelve entries, all twelve carried the skip-worktree bit, and the repack still refused [17]. With the sparse index, `git ls-files --sparse assets` returned no `.bin` entries at all; the command expanded the sparse index to a full index, printed the hint saying so, and then refused anyway [18]. The documentation's wording is that it refuses to drop any blob the current index references, which reads as keep those and drop the rest; the observed behaviour is that the whole repack aborts on the first one and drops nothing, and `--dry-run` aborts too, so you cannot even enumerate the candidates [15][16]. Partial clone plus sparse-checkout is the standard large-monorepo setup [19].
The regrowth number is the part worth budgeting against. 360 MiB against an unfiltered 397 MiB is about 91 percent of the size the filter was supposed to avoid, reached with no fetch and no pull [1][2]. The bare case is sharper: one `git rev-list --objects --all` took a 195,141-byte clone to 415,716,093 bytes, roughly 2,130 times larger, while `fsck` and `gc` left it alone, `gc` adding 4,950 bytes [9][10][11][2][3]. A size dashboard that has to download the repo to measure it is an odd sort of gauge.
These are one person's measurements on one synthetic repo, and the shape of that repo is doing work: 61 commits, twelve 3 MiB assets of incompressible random bytes with two rewritten in every commit, plus 200 small text files that churn [8]. For the ratio to transfer you need large blobs that do not delta-compress and that get rewritten often, plus a workflow that walks history. Text-heavy history under the same filter will refetch far less, because the objects the filter declined were small to begin with. The build is also not a release: the option merged on 25 August 2026, is absent from the 2.55.0 tag, and is on course for 2.56, which has not shipped [6], and the transcripts come from `b8242b093`, which calls itself 2.55.0.806.gb8242b093 [7].
On this evidence the option is usable today only from a checkout whose index carries no filtered blob. That is not the state a monorepo working copy is normally in, and skip-worktree does not get you out of it [17].
What to watch
- Whether the index guard becomes a skip-and-continue, rather than an abort, before 2.56 is tagged.
- Whether the sparse index gets a check path that does not expand to a full index before refusing.
- Whether the 2.56 release notes document the exit-128 abort or repeat the softer 'refuses to drop' wording.