Why your custom git merge driver did not run
We put one logging merge driver through 10 merges in plain git. It ran in 6. One miss left a conflicted file with no markers, holding only our side.
You wrote merge=mydriver in .gitattributes, defined the driver, and git still
stopped with a conflict. Or it did not stop, and a side of the merge is gone.
The documentation explains how to define a driver. It says less about the ways git will skip it. So we built the smallest driver we could, one that writes a line to a log every time git calls it and then keeps the lines of both sides, and ran it through 10 merges in throwaway repositories with plain git 2.39.5.
Git called it in 6. Here are the 4 where it did not, and the 2 surprises in the 6 where it did.
The 10 merges
| # | Setup | Driver called | What git left |
|---|---|---|---|
| 1 | attribute committed, driver defined and on PATH | yes | clean, both lines |
| 2 | fresh clone of the same repository | no | conflict, with markers |
| 3 | driver defined, command not on PATH | no | conflict, no markers, only our side |
| 4a | branch cut before the attribute; merge main into it | no | conflict, with markers |
| 4b | same repository, merge that branch into main | yes | clean |
| 4c | same branch, main's .gitattributes committed onto it first | yes | clean |
| 5 | only one side changed the file | no | clean, the changed side |
| 6 | both sides changed the file, far apart | yes | clean, and wrong |
| 7a | git rebase instead of merge | yes | clean |
| 7b | git cherry-pick instead of merge | yes | clean |
Every case ran twice and printed the same thing both times. The script and its output are in this site's repository, linked at the end.
1. The clone never learned the driver
.gitattributes is a committed file, so it travels with the repository. The
driver's definition does not. git's own manual is explicit that it lives in
.git/config or ~/.gitconfig, and git never copies config on clone.
So in a fresh clone (case 2) the attribute says merge=keepboth, there is no
keepboth to run, and git falls back to its normal text merge. Two branches
that each appended a line got an ordinary conflict.
The trap is the diagnostic people reach for first. In that clone,
git check-attr merge -- log.txt still answered merge: keepboth. It tells you
the attribute is set. It does not tell you a driver exists. The command that does:
git config --get merge.keepboth.driverIt printed nothing.
This one bites anywhere a merge runs before your setup step: CI that checks out
and merges, a teammate's first git pull, a new machine.
2. The command is not on PATH
This is the dangerous one. In case 3 the driver was defined, but git could not find the command. git printed one line, easy to miss in merge output:
keepboth .merge_file_… .merge_file_… .merge_file_…: keepboth: command not found
Auto-merging log.txt
CONFLICT (content): Merge conflict in log.txtThen it marked the file as conflicted, which is correct. But the file had no conflict markers. It held our side and nothing else.
That follows from git's contract with a driver. The driver is handed our version
as %A and is expected to overwrite it with the result, and a non-zero exit
means "there were conflicts". A driver that never started never overwrote
anything, and the shell's not-found status is non-zero. So git reports a
conflict and leaves %A as it was.
The result is a conflicted file that looks resolved. Search it for <<<<<<< and
you find nothing. Open it and it parses. git add it and the other side is gone.
That is not hypothetical:
yukineko/claude-harnesses#1293,
opened on 2026-10-03, reports exactly this with a TOML task file, including the
git add and commit that dropped the other branch's changes.
Two defences. Put an absolute path in merge.<name>.driver, or a wrapper in the
repository that fails loudly. And when a merge stops, trust
git diff --name-only --diff-filter=U over your eyes. It listed the file in
case 3 when the content gave no sign.
3. The branch is older than the attribute
In case 4 the attribute was committed on main after branch old was cut. On
old, before merging, git check-attr said merge: unspecified, and merging
main into it (4a) gave a plain conflict. What 4a to 4c show is that the
attribute counts only if the branch you are on already has it.
Merging the other way (4b), from main, called the driver and merged clean. So
did merging main into old after first committing main's .gitattributes onto
old (4c).
Two things make this one hard to see. "Update branch" on a pull request is
exactly direction 4a. And after the failed merge, git check-attr on old
reported merge: keepboth, because the merge had already brought
.gitattributes into the working tree. Run the diagnostic after the fact and it
tells you the driver was configured.
paiml/aprender#3308 found the
same thing and titled it well: the driver is retroactively inert for every pull
request older than the declaration.
4. The merge happened on the server
We did not test this one, and are reporting it rather than confirming it. A
driver is a command on your machine, so a merge button that merges on the
host's servers has nothing to run.
gitlab-org/gitlab#18830,
still open, says GitLab reads union and binary from .gitattributes but not
custom drivers, because those live in gitconfig.
deep-code-review#862
reports the same of a hosted merge and recommends merging the base branch locally
and pushing. If your driver is the only thing that makes a file merge, the host's
"conflicts" badge is answering a different question.
Two that are not failures, and one that is worse
Case 5: only one branch changed the file, and the driver was not called. That is right. There was nothing to merge, so git took the changed side. If you are testing a driver, make both branches touch the file or you will think it is broken.
Case 7: git rebase and git cherry-pick both called the driver. A driver is
not merge-only.
Case 6 is the one to read twice. Both branches changed the file, one at the top and one at the bottom. git's own text merge would have combined those without a conflict. Because the attribute names a driver, git called the driver instead, and our toy driver, which only knows how to keep lines from both sides, produced this, with exit 0:
top-a 1 2 3 4 5 6 bottom top bottom-bNo conflict, no warning, and a file that is wrong. A driver does not run after git fails to merge. It runs instead of git, on every merge where both sides touched the path. So it has to be at least as good as the text merge on the easy cases, not only the one you wrote it for. A keep-both driver like ours is only safe on files that are append-only, where every line is a whole record.
A four-line check
When a driver seems not to have run, these answer the four questions in order:
git check-attr merge -- path/to/file # is the attribute set on this branch, now?
git config --get merge.NAME.driver # does this clone define the driver?
command -v the-driver-command # can git's shell find it?
git diff --name-only --diff-filter=U # what git still counts as conflicted, markers or notRun the first one before the merge, not after. Case 4a is why.
What this does not cover
- One machine and one git version: 2.39.5, Apple's build. Other versions may differ in what they print, and we did not check them
- One file, one line per branch, a driver we wrote to be simple. Real drivers do more and fail in more ways
- No hosted merge was run. Section 4 is other people's reports
- We did not test
merge.<name>.recursive,%Por%L, or drivers that crash (git's manual says those should exit above 128 and are treated as a failure, not a conflict)
The script and both runs are in this site's repository under
docs/search-log/bench/. kadence avoids the question by writing one
file per event, so its merges never need a driver; the comparison with a
tracker that uses one is where we measured
what that trade costs.
Where the numbers come from
- Bench run 2026-10-06 with git 2.39.5 (Apple Git-154), run twice with identical output: 10 merges in throwaway repositories, one driver `keepboth` that logs every call. The driver was called in cases 1, 4b, 4c, 6, 7a and 7b (6 merges) and not in cases 2, 3, 4a and 5 (4 merges). Script and full output in this site's repository: docs/search-log/bench/2026-10-06-merge-driver-not-running.sh and .txt
- Case 2 of the bench: fresh `git clone`, `git config --get merge.keepboth.driver` empty, `git check-attr merge` reports keepboth, merge exit 1 with conflict markers, 0 driver calls
- Case 3 of the bench: driver defined but `keepboth` not on PATH; git printed `keepboth: command not found`, exit 1, file listed as unmerged, no conflict markers, file held only our side, 0 driver calls
- Cases 4a, 4b and 4c of the bench: attribute committed on main after branch `old` was cut. `git check-attr` on old before merging: unspecified. 4a, on old, merge main: exit 1, markers, 0 calls, and `check-attr` afterwards reports keepboth. 4b, on main, merge old: exit 0, 1 call. 4c, on old after committing main's .gitattributes: exit 0, 1 call
- Cases 5, 6 and 7 of the bench: only one side changed the file, 0 calls; both sides changed non-overlapping lines, 1 call, exit 0, and the toy driver's output was wrong; rebase and cherry-pick each called the driver once
- git documentation, gitattributes(5), section 'Defining a custom merge driver': the driver is defined in $GIT_DIR/config or $HOME/.gitconfig, not in .gitattributes; it must leave the result in %A and exit zero if it merged cleanly, non-zero if there were conflicts; crashes exit above 128 — https://git-scm.com/docs/gitattributes
- yukineko/claude-harnesses#1293, read with `gh issue view` on 2026-10-06: open, created 2026-10-03. Reports `backlog: command not found` during a merge, task files marked UU, the working file holding our side verbatim with no markers, and a `git add` plus commit that discarded the other side
- paiml/aprender#3308, read with `gh issue view` on 2026-10-06: 'A .gitattributes merge driver is retroactively inert: every PR older than the declaration still conflicts', closed 2026-09-15, the day it was opened
- gitlab-org/gitlab#18830, fetched 2026-10-06: open; states GitLab reads union and binary from .gitattributes but does not use custom merge drivers in merge requests, because they are defined in gitconfig
- remigiusz-antczak/deep-code-review#862, read with `gh issue view` on 2026-10-06: 'A git merge-driver resolves conflicts only on LOCAL merge/rebase — the host's server-side merge never runs it', closed 2026-09-21
- Probe run 2026-10-06: `why is my custom git merge driver not running` returned an objective-git issue, a 2020 blog post on writing a driver, Praqma/git-merge-driver, four copies of GitLab's gitattributes documentation, gitlab-org/gitlab#18830 and a driver README