
My work email was in all 27 personal repos — fixing it with git includeIf and filter-repo --mailmap
Git identity belongs to the directory, not the repo. includeIf gitdir fixes it going forward; git filter-repo --mailmap fixes the commits already pushed.
On this page
Introduction
I set up a personal blog, pushed the first commit, and looked at the author line:
Author: my-work-name <me@company.example>My employer’s address, on a personal project, in a repo I intended to make public. Nothing broke. Git had done exactly what I configured it to do years ago and then never thought about again. And when I went to check how far it had spread, it was not one repo. It was twenty-seven.
The fix turned out to be a git feature I had never used: includeIf "gitdir:",
which picks an identity based on where the repository lives, so the answer
follows the directory instead of having to be set per repo. The commits already
pushed needed something else again, git filter-repo --mailmap.
This article covers the count that reframed the problem, the one-command fix I tried first and why it was the wrong layer, the conditional-include setup and the two details that silently break it, and what went wrong rewriting history that was already on GitHub.
The one-command fix, and why it was the wrong layer
The obvious fix is one command:
git config user.email personal@example.comThat fixes this repo. It does not fix the problem, because the problem was never this repo. I only saw that after counting.
Count first
Before writing any config, I counted:
for d in ~/Developer/personal/*/; do [ -d "$d/.git" ] || continue printf '%-30s %s\n' "$(basename $d)" "$(git -C "$d" config user.email)"doneTwenty-seven repositories. Every one of them the work address. Not because I had made twenty-seven mistakes. Because I had made zero decisions: every repo inherited the same global default, and the default was set once, on a work machine, for work.
That reframes the fix. git config user.email in one repo is a patch applied at
the wrong layer. It leaves twenty-six repos wrong and guarantees the twenty-eighth
will be wrong too, because nothing about creating a new repo will remind me.
Conditional includes
Git can pick an identity from where the repository lives. This has been in git since 2.13 and I had never used it:
; Default: work[user] name = my-work-name email = me@company.example
; Anything under ~/Developer/personal/ overrides the above[includeIf "gitdir:~/Developer/personal/"] path = ~/.gitconfig-personal[user] name = my-personal-handle email = personal@example.comNow the identity is a property of where the code lives, which is the thing that actually determines which identity is correct. New personal repos are covered the moment they are created. No per-repo setup, nothing to remember.
Both directories read the same ~/.gitconfig. Only the personal one also
matches the includeIf condition, so it alone pulls in the second file.
Two details silently break this, and neither errors:
The trailing slash matters. gitdir:~/Developer/personal/ matches that
directory and everything beneath it. Without the trailing slash you are matching
a single path, and every repo inside it misses.
Order matters. Git config is last-write-wins, so the includeIf must come
after the [user] block it overrides. Put it at the top of the file and the
default [user] below simply overwrites what the include just set. The file
looks correct. The behaviour is unchanged.
Verify against both sides rather than assuming:
git -C ~/Developer/personal/some-repo config user.email # personalgit -C ~/Developer/work/some-repo config user.email # workIf the second one has silently become your personal address, your pattern is too broad.
Which address
The instinct is to use your real personal email. Consider the GitHub noreply address instead:
26102772+oharu121@users.noreply.github.comThat is <user-id>+<username>@users.noreply.github.com, and you can get your id
from gh api user --jq .id. Commits authored with it still link to your account
and still count toward your contribution graph, but no real inbox ends up in a
public repository.
The real-address version has a failure mode worth knowing. If the address is not verified on your GitHub account, commits show up unlinked, with no avatar, no profile link, and no contribution credit. And if you have Block command line pushes that expose my email enabled, pushing with it fails outright.
Fixing what you already pushed
Going forward is solved. The commits that already exist are not.
This is worth doing immediately or never. With two commits it is a five-minute job. With two years of history and other people’s clones, it is a migration. The cost only goes up.
git commit --amend --reset-author is the tempting one-liner, and it is wrong for
anything but the tip commit. It rewrites authorship to your current identity
for whatever it touches, including commits that were never yours. Use a mailmap
instead, which rewrites only entries that match:
New Name <new@example.com> Old Name <old@company.example>git filter-repo --mailmap mailmap.txt --forceBecause it matches on the old identity, commits from bots and collaborators pass through untouched. In my case a Dependabot commit sat on top of mine; a blanket rewrite would have reattributed it to me.
Three things that bit me
Tags do not follow the branch. My v0.1.0 tag pointed at the commit being
rewritten, so after the rewrite it still referenced the old object. The branch
and the tag are separate refs and both need force-pushing:
filter-repo moves main onto the rewritten commits. The v0.1.0 tag is a
separate ref it never touches, so it keeps pointing at the orphaned original.
git push --force-with-lease=main:<old-sha> origin maingit push --force origin refs/tags/v0.1.0Use --force-with-lease with an explicit expected SHA, not a bare --force. If
anything landed on the remote while you were working (in my case CI had
auto-merged a dependency PR minutes earlier), the push aborts instead of
destroying it.
git filter-repo deletes your origin remote. Deliberately: it assumes you
are operating on a throwaway clone and wants to stop you reflexively pushing a
rewritten history back. It tells you, then you re-add it:
git remote add origin <url>Check the release survived. A GitHub Release is bound to a tag name, so force-updating the tag keeps the release attached, but verify rather than assume:
gh release view v0.1.0 --json tagName,body --jq '.tagName, (.body | length)'gh api repos/<owner>/<repo>/git/ref/tags/v0.1.0 --jq '.object.sha'One caveat worth stating honestly: the old objects remain on GitHub, unreferenced but reachable by SHA, until they are garbage collected. If the exposure genuinely matters rather than merely being untidy, that needs a support request; the force push alone does not erase them.
Summary
The bug lived one layer up from any config file: identity was set globally when it is actually a property of which project you are in.
- Count how many repos are affected before deciding where to apply the fix.
- Use
includeIf "gitdir:…/"so the answer follows the directory. Mind the trailing slash and put it after[user]. - Prefer the GitHub noreply address over a real inbox.
- Rewrite existing history now, while it is two commits. Use a mailmap so you only rewrite yourself, and remember tags are separate refs.
The check that would have caught this years earlier is one line:
git log -1 --format='%an <%ae>'Run it once after the first commit in a new repo.



