..

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.sshCommand scopes the key to the repository’s location, so you don’t need separate Host entries 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 git directly 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 includeIf blocks matters when directory patterns could overlap – Git applies them in the order they appear, and later matching blocks can override earlier ones.
  • ~ inside path = is expanded, but on Windows it’s safer to use forward slashes or fully-qualified paths, since backslashes in gitdir patterns 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 with git config inside that repo) overrides anything pulled in via includeIf – 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.