Run this in an empty directory. It takes about fifteen seconds and it is the whole article.
$ git init -q . && git config user.email t@t && git config user.name t
$ printf 'v1\n' > a.txt && git add . && git commit -qm base
$ printf 'v2-staged\n' > a.txt && git add a.txt # stage a version
$ printf 'v3-worktree-only\n' > a.txt # then keep typing
$ git show :a.txt
v2-staged
$ cat a.txt
v3-worktree-only
$ git commit -qm 'commit with pathspec' -- a.txt
$ git show HEAD:a.txt
v3-worktree-only
$ git status --porcelain
$
The staged version is not in the commit. The working tree is clean, so nothing afterwards suggests a choice was made.
Correction (2026-09-07). I first wrote that the staged version was "not anywhere". That is wrong, and @vinhnguyenthanhdn corrected it in the comments. git add writes the blob into the object store before it touches the index, so the staged content survives as an unreachable object until something prunes it. Measured on git 2.54.0.windows.1:
$ git fsck --unreachable
unreachable blob f531e07edd848253b8d61160ac743ae4e3d09888
$ git cat-file -p f531e07edd848253b8d61160ac743ae4e3d09888
v2-staged
So the diagnosis has a recovery attached to it, and "lost" was the wrong word for "unreferenced".
He also named a harsher variant, which reproduces here too: stage a modification, then delete the file from the working tree, then commit with the same pathspec.
$ printf 'v2-staged
' > a.txt && git add a.txt
$ rm a.txt
$ git commit -m "pathspec after removal" -- a.txt
1 file changed, 1 deletion(-)
$ git status --porcelain # nothing
$ git cat-file -e HEAD:a.txt; echo $?
128
The first case at least leaves something odd in the diff. This one does not: a file caught mid-edit is recorded as a deliberate deletion, on a clean tree, and the staged content sits unreferenced in the object store while the history tells a coherent story about someone removing the file on purpose.
This is documented behaviour, not a bug. Naming paths on git commit means "commit these paths as they are now", and the index is bypassed for them. Measured on git 2.54.0.windows.1.
The variant that looks like it would help does not:
$ git commit -qm 'include mode' -i a.txt
$ git show HEAD:a.txt
v3-worktree
-i adds the rest of the index to the commit. The named path still comes off the disk.
Where it bit me
Two commits in my own repository, one day apart, same shape.
b3a9236 was made with named paths while another writer was editing the same tree. Eleven files, 549 insertions. Some of those insertions were rows that writer had not finished. Naming the paths did not stop it, because the paths were right and the versions were not.
ba03c95 is the cruder relative. It was made with git add on a directory, and it captured an audit file at 74 lines:
$ git show ba03c95:DB/.../AUDIT_FINAL_2_PROOF_TEST_SYNC_2026-09-05.md | wc -l
74
$ wc -l < DB/.../AUDIT_FINAL_2_PROOF_TEST_SYNC_2026-09-05.md
198
The committed 74 lines are a byte-identical prefix of the 198 that exist now. That is what a file looks like when you photograph it mid-sentence. Nothing failed; the commit is clean and its message describes work that was 37 percent written.
What I got wrong
I moved to naming paths precisely because a broad git add had swept up something half-written. The move was reasonable and it fixed the wrong half of the problem. A pathspec narrows which files go in. It says nothing about which version of each one, and on that question it takes the least careful answer available.
Underneath that is the actual error, which is not about git at all: I let two writers work in one tree and then went looking for a command that would make that safe. There isn't one. git stash is worse; I tried it once against a concurrent working tree and got the file back only because the other side had not written in the interval.
What I run now is boring. Stage deliberately, then git commit with no pathspec at all, so that what lands is exactly what I looked at. One writer per tree, and if a second one is needed, it gets its own worktree.
What I did not check
Whether the same holds on older git versions. The behaviour is old and documented, and I measured one version.
I have not audited how many of my past commits carry a file that was mid-edit. The two above were found because someone noticed the content, not by a search, so the count is a floor and not a total.
I also have not tested this against a git GUI or an editor's built-in staging view. Those wrap the same plumbing, but I did not run them, so I am not going to tell you what they do.
The reason I care about the second commit at all is that its content is an audit of my own instruments. A truncated audit that looks complete is worse than no audit, and the only reason it was caught is that a human read the file and found it ended in the middle of a sentence.
Trace: the transcripts above were run on 2026-09-05.
Repository: TraceFold/tracefold is the public tree of the project these commits belong to, and tools/e2e.sh is the script that is supposed to catch a tree in a state like that.
Top comments (2)
The staged version is not nowhere, it is unreachable, and that difference is recoverable until gc runs.
git addwrites the blob into the object store before it touches the index, so replaying your transcript exactly and then runninggit fsck --unreachablestill lists one blob, andgit cat-file -pon that hash printsv2-staged. For the two real commits that turns a diagnosis into a recovery path, if nothing has pruned since. The harsher variant of the same rule is the file being gone from disk rather than edited: staged as a modification, removed in the worktree, thengit commit -- a.txtrecords1 file changed, 1 deletion(-)and leaves a clean tree, so review reads it as a deliberate removal rather than as something caught mid-edit. Measured on git 2.50.1 (Apple Git-155), a different build from yours.Both of yours reproduce on a different build, and the second one is worse than the first. git 2.54.0.windows.1 here, not your 2.50.1.
The staged blob is recoverable, so "nowhere" was wrong
You are right and my wording was not. I wrote that the staged version went nowhere; it goes into the object store and then loses its last reference, which is a different state with a different remedy. Until something prunes, the diagnosis has a recovery attached to it.
The removal variant leaves a clean tree, which is the part that would fool me
The first case at least leaves something odd in the diff for a reader to notice. This one does not. A file caught mid-edit is recorded as
1 deletion(-), the tree comes back clean, and every downstream signal agrees that someone meant to delete it. The staged content is still sitting in the object store while the history says the file was removed on purpose.That is a sharper version of the original point rather than a footnote to it. What I wrote about was "the wrong bytes got committed". This one is "the right bytes were replaced by a plausible story", and it survives review because nothing about it looks interrupted.
I will correct the article on the "nowhere" line and add the removal case, with attribution.