Stop Committing as the Wrong Person
If you work across several organizations, clients, or personal projects on the same machine, you’ve probably run into the same annoyance: each context needs its own user.name, user.email, and often its own SSH key – but Git only gives you one global ~/.gitconfig.
The common workaround is to manually run git config user.email "..." in every new repository, or worse, to forget to do it and push a commit under the wrong identity. Git has a built-in, much cleaner solution for this: conditional includes.
This post covers how they work, why they’re worth adopting, and a complete setup for a directory layout like:
~/w/a → identity, email, and SSH key for Company A
~/w/b → identity, email, and SSH key for Company B
How Conditional Includes Work
Since Git 2.13, the [includeIf] directive lets you load a separate config file automatically, based on a condition – most commonly, the location of the repository on disk. Instead of one flat ~/.gitconfig, you split your configuration into:
- One base file (
~/.gitconfig) with settings that apply everywhere. - One file per context (e.g.
~/.gitconfig-a,~/.gitconfig-b) with the settings specific to that context. - A set of rules in the base file telling Git which context file to load, depending on where you’re running the command.
Git evaluates these rules every time it reads configuration for a repository, so the switch is completely automatic – there’s nothing to remember once it’s set up.
The gitdir Condition
The most useful condition for this purpose is gitdir, which matches based on the path of the repository’s .git directory:
# ~/.gitconfig
[includeIf "gitdir:~/w/a/"]
path = ~/.gitconfig-a
[includeIf "gitdir:~/w/b/"]
path = ~/.gitconfig-b
Two details matter here and are easy to get wrong:
- The trailing slash is required.
~/w/a/matches the directory and everything nested inside it (equivalent to~/w/a/**). Without the slash, Git expects an exact match on the repository path, not a prefix. ~expands to your home directory, so you can write portable rules without hardcoding an absolute path.
There’s also a case-insensitive variant, gitdir/i:, which is mainly relevant on Windows or other case-insensitive filesystems – you generally don’t need it on Linux or macOS.
The Per-Context Config File
Each included file only needs to define what’s different for that context – Git merges it with the base config, and later values override earlier ones. A typical ~/.gitconfig-a looks like:
# ~/.gitconfig-a
[user]
name = Your Name
email = you@company-a.com
signingkey = ~/.ssh/id_ed25519_company_a.pub
[core]
sshCommand = ssh -i ~/.ssh/id_ed25519_company_a -F /dev/null
[commit]
gpgsign = true
[gpg]
format = ssh
The core.sshCommand override is what solves the SSH key problem: instead of relying on a global SSH config with Host aliases, Git itself tells SSH which private key to use for that context – no rewriting of remote URLs required, and git clone git@git.company.com:org/repo.git works exactly as expected.
Your base ~/.gitconfig then holds only what’s shared across all contexts:
# ~/.gitconfig
[init]
defaultBranch = main
[pull]
rebase = true
[core]
excludesfile = ~/.gitignore_global
[includeIf "gitdir:~/w/a/"]
path = ~/.gitconfig-a
[includeIf "gitdir:~/w/b/"]
path = ~/.gitconfig-b
Why This Is Worth Setting Up
- No accidental cross-identity commits. The right name, email, and signing key are applied automatically the moment you’re inside the matching directory – there’s no manual step to forget.
- Per-context SSH keys, without SSH config gymnastics.
core.sshCommandscopes the key to the repository’s location, so you don’t need separateHostentries or rewritten clone URLs per account. - Clean separation of concerns. Each config file maps 1:1 to a context (client, company, personal), which makes it trivial to add, audit, or remove one without touching the others.
- Works transparently with any tool built on Git. IDEs, CLI tools, and CI helpers that call
gitdirectly all pick up the correct identity, since the resolution happens inside Git itself. - Scales beyond just identity. The same mechanism can scope any Git setting – proxy configuration,
commit.template,core.hooksPath, GPG signing – per directory, not just user info.
Verifying the Setup
After adding the rules, confirm Git is resolving the right identity in each location:
cd ~/w/a/repo-a && git config user.email
# → you@company-a.com
cd ~/w/b/repo-b && git config user.email
# → you@company-b.com
To see exactly which files Git combined to produce the final configuration for a repository, use:
git config --list --show-origin
This is the fastest way to debug a rule that isn’t matching – it shows precisely which file each setting came from.
Common Pitfalls
- Missing trailing slash on
gitdir– the single most common mistake; it silently turns a directory match into an exact-path match. - Order of
includeIfblocks matters when directory patterns could overlap – Git applies them in the order they appear, and later matching blocks can override earlier ones. ~insidepath =is expanded, but on Windows it’s safer to use forward slashes or fully-qualified paths, since backslashes ingitdirpatterns aren’t parsed the way you’d expect.- Local repo config still wins. A setting placed directly in a repository’s
.git/config(or set withgit configinside that repo) overrides anything pulled in viaincludeIf– useful for the rare one-off exception.
Summary
Conditional includes turn a single, monolithic ~/.gitconfig into a small, composable set of files – one shared base plus one file per context – that Git assembles automatically based on where you’re working. For anyone juggling multiple companies, clients, or SSH identities on one machine, it removes an entire category of “wrong identity” mistakes for a few minutes of one-time setup.