AmanERPAmanERP
Blog पर वापस
AI-NATIVE VERIFICATION

git commit -- file.go Doesn't Limit Scope. It Does the Opposite.

The one git semantic that turns clean parallel-agent execution into ghost commits, and the pre-commit check that stops it.

Niraj Kumar2026-06-026 min read
A minimal terminal card showing a pathspec git commit stopped by a pre-commit check reporting unstaged lines.

The wave was nearly closed when I checked the diff-tree. Commit 8dd07f38 had swallowed five Makefile target bodies a sibling agent had written but not yet committed. Commit 94034548 had swept up two other agents' changelog entries. Two commits now claimed work that wasn't theirs, and I had made each one with git commit -- <file>, the flag I reached for to narrow the blast radius.

Does git commit -- file.go limit a commit to that file?

No. It widens it. The pathspec form is shorthand for git commit --only, which re-stages each named file's full working-tree content before committing. Alone on a clean tree, the re-stage absorbs nothing and you never see it. With several agents sharing one working tree, it absorbs whatever a sibling left unstaged in those files, into your commit, under your name.

The footgun, stated plainly

Most engineers reason about git by analogy. git add -- file.go stages only that file. git diff -- file.go shows only that file's diff. So git commit -- file.go must commit only that file, right.

The pathspec doesn't filter the commit down to your staged changes in that file. It tells git to take the file's current working-tree state: every line in it, whoever wrote it, staged or not.

On a solo session that distinction never costs you anything; there's nothing unstaged to absorb. The footgun stays dormant until you run several writers against one working tree, which is exactly what a parallel agent wave is.

How it bit a wave

In one session I ran three waves of four coding agents against a single checkout: ten parallel agents touching shared files. Each agent had used git add -p to stage specific hunks of files like unreleased.md and the Makefile. Then each committed with git commit -- <its-files>.

Commit 94034548, closing one debt ticket, absorbed two other agents' unstaged changelog entries. Commit 8dd07f38 absorbed five of a sibling agent's unstaged Makefile target bodies. Each git commit -- <file> had re-staged the file's full working tree, including the hunks a sibling had written but not yet committed.

The functional impact was zero: every hunk landed somewhere at HEAD, nothing was lost. The damage was attribution. git blame now points at the wrong agent for those hunks, and the audit trail, the thing you most need when an AI wrote the code, lies about who did what.

The sibling variant: git mv leaks too

The same mechanism has a quieter cousin. During a wave, one agent ran git mv backlog/BUG-134.md resolved/BUG-134.md, staging both the delete and the add in the shared index, as part of closing its ticket. Before that agent committed, a different agent ran a plain git commit to close its own ticket.

The plain commit captured the index state, including the first agent's pre-staged deletion. Commit bc9e8e34 now looks like it deleted two files it never touched. Plain git commit captures exactly what you staged is wrong in a shared tree: it captures whatever is in the index, regardless of which agent put it there.

This is the cost when the index is shared. In the project's worst run, 6 of 14 ticket closures, 43%, missed the deletion-half of a git mv for related reasons. The list both halves in the commit mitigation only got that to 8 of 14. Nearly half a wave of closures, corrupted by a staging model nobody had read closely enough.

Why the analogy fails

git add and git diff are scope-limited by pathspec. git commit with a pathspec is the one command in that family that breaks the pattern: a genuine inconsistency in git's command surface, and humans and language models both extrapolate from the consistent cases into the inconsistent one.

It took five months to surface because the precondition is narrow: multiple agents on one tree, at least one using git add -p, at least one running git commit -- with a dirty working tree. Low-concurrency sessions never trip it. The day you scale to ten parallel agents, it fires.

The fix is a pre-commit check, not a rule you remember

You cannot rely on remembering the --only semantic at 11pm in a four-agent wave. Encode it. Before any pathspec commit, assert the files you're about to commit have zero unstaged lines, so the re-stage absorbs nothing:

for f in <files-you-intend-to-commit>; do
    unstaged=$(git diff --numstat -- "$f" | awk '{s+=$1+$2} END{print s+0}')
    if [[ "$unstaged" -gt 0 ]]; then
        echo "ABORT: $f has $unstaged unstaged lines - git commit -- $f would absorb sibling edits." >&2
        exit 1
    fi
done

Then pick the commit form by intent, not habit:

  • Commit exactly the hunks you git add -p-staged: git commit with no pathspec.
  • Commit these files' working-tree state as-is: git commit -- <files>.
  • Commit staged hunks plus these unstaged files: git commit -i <files>.

And verify reality after the fact, because intent is not outcome. Pre-commit git diff --cached --name-only checks what you meant to commit; post-commit git diff-tree --name-only HEAD checks what you actually committed. Run both:

git diff-tree --no-commit-id --name-only -r HEAD | sort > /tmp/committed.txt
diff /tmp/expected.txt /tmp/committed.txt   # non-empty? do not report done

For shared-writer files like a changelog, sidestep the race entirely: each agent writes its own fragment file, and a single consolidation step merges them at the end. Single writer per file, zero index contention.

Where this rule does not apply

The honest caveat: this is a shared-working-tree problem, not a git problem. If your agents each get their own checkout, separate clones or git worktrees, the index is never shared, sibling hunks can't leak, and the numstat-zero check buys you nothing. One-checkout-per-agent is the cleaner isolation, and if I were starting over I would reach for it first.

I run agents on a single tree anyway, for a specific reason: a shared checkout lets the orchestrator read every agent's in-flight diff in one place, and that visibility is worth more to me than the isolation. The pre-commit check is the price of that choice. So the rule is conditional: adopt it only if you have made the same trade. If you have not, give each agent its own tree and skip the rest of this piece.

Steal the numstat-zero check, and with it the rule I would tattoo on a parallel wave: a flag named like a scope limiter is not a contract. Read what it does to the index, not what its name implies.

Series linkage

Part 1 of 5: "AI-Native Verification Failure Classes".

  • git-footgun-parallel-agents: staging corruption under shared-index parallelism.
  • review-tunnel-vision-positive-space-negative-space: reviews that check what's present, never what's absent.
  • abstraction-collapse-ai-rewrite-trap: the whole missing layer a perfect-looking rewrite never built.
  • code-exists-not-wired: code that compiles, tests, and reviews clean but is never called.
  • mutation-gate-100-percent-zero-kills: a quality gate reporting 100% while catching nothing.

About AmanERP

AmanERP is an AI-native ERP for SMBs, built in public, down to a git history that has to tell the truth about who wrote what. Aman means peace: calm software, clean audit trails.