Git Configuration Management at Scale Jump to heading
Git configuration is per machine, which means that on a team of twenty there are twenty slightly different Gits. Most of the time that is harmless. Occasionally it is the reason one person’s commits arrive with the wrong email, another’s produce a diff full of line-ending changes, and a third’s hooks never run — and each of those costs an afternoon to diagnose because the code is identical and the behaviour is not. This part of Git Workflow Architecture & Branching Strategies covers what can travel with the repository, what cannot, and how to tell the difference.
Prerequisites Jump to heading
What Can and Cannot Travel Jump to heading
Three mechanisms carry configuration, and they cover different things. Knowing which is which removes most of the confusion about why a setting “did not apply”.
The practical rule: if a wrong value produces a wrong commit, it belongs in attributes or in committed configuration. If a wrong value only inconveniences one person, leave it local.
Step 1 — Commit the Settings That Are Team Policy Jump to heading
Git cannot read arbitrary configuration out of a repository — that would be a security hole — but it can be told to include a committed file.
# .gitconfig-team, committed to the repository
[core]
autocrlf = false
hooksPath = .githooks
[pull]
rebase = true
[fetch]
prune = true
[rebase]
updateRefs = true
autosquash = true
[merge]
conflictStyle = zdiff3
[diff]
algorithm = histogram
colorMoved = default # Each clone includes it once, during setup
git config --local include.path ../.gitconfig-team # Verification: the values are visible and come from the right file
git config --show-origin --get merge.conflictStyle
git config --get-all include.path zdiff3 alone justifies the exercise: it shows the common ancestor’s text in a conflict, which turns a guess into a decision. The details of what each setting buys are in shipping a team gitconfig with includeIf.
Step 2 — Scope Identity by Directory Jump to heading
The wrong author email is the most common configuration mistake and the most annoying to fix afterwards, because it is baked into commits.
# ~/.gitconfig
[user]
name = Ada Lovelace
email = [email protected]
[includeIf "gitdir:~/work/"]
path = ~/.gitconfig-work # ~/.gitconfig-work
[user]
email = [email protected]
signingkey = ~/.ssh/id_ed25519_signing.pub
[commit]
gpgsign = true # Verification: the identity differs by directory, as intended
git -C ~/work/app config --get user.email
git -C ~/personal/blog config --get user.email Conditional includes are evaluated per repository path, so the right identity applies without anyone remembering. Pair it with the check in rewriting author emails during a migration — preventing the problem is much cheaper than rewriting history to fix it.
Step 3 — Put Path Behaviour in Attributes Jump to heading
Attributes need no setup at all: they are committed, and Git applies them on checkout.
cat > .gitattributes <<'ATTR'
* text=auto eol=lf
*.sh text eol=lf
*.bat text eol=crlf
*.png binary
*.pdf binary
package-lock.json linguist-generated=true -diff
ATTR # Verification: what Git will do with a given path
git check-attr -a -- src/main.sh package-lock.json assets/logo.png
git ls-files --eol | head -5 This is the mechanism that fixes line endings for everyone at once, including people who never change a setting — the full procedure is in standardising line endings with gitattributes.
Step 4 — Make the Setup a Single Command Jump to heading
Any setup that takes five commands will be done wrong on some machines.
#!/usr/bin/env sh
# scripts/setup — make this clone match team policy.
set -eu
root=$(git rev-parse --show-toplevel)
git -C "$root" config --local include.path ../.gitconfig-team
git -C "$root" config --local core.hooksPath .githooks
# Identity: prompt rather than guess, and only if it is unset or wrong.
email=$(git -C "$root" config --get user.email || echo '')
case "$email" in
*@acme.com) ;;
*) printf 'Work email: '; read -r e; git -C "$root" config --local user.email "$e" ;;
esac
echo "configured: $(git -C "$root" config --get user.email)" # Verification: a fresh clone is correct after one command
git clone "$(git remote get-url origin)" /tmp/fresh && (cd /tmp/fresh && ./scripts/setup) Step 5 — Audit What Machines Actually Have Jump to heading
Configuration decays. A one-line check in CI catches the drift that matters.
# A pre-push hook, or a job that runs on every developer machine
for k in core.hooksPath merge.conflictStyle pull.rebase; do
printf '%-24s %s\n' "$k" "$(git config --get "$k" || echo 'UNSET')"
done # The check worth failing on: hooks that are not installed
test "$(git config --get core.hooksPath)" = ".githooks" \
|| { echo "hooks are not configured in this clone; run ./scripts/setup" >&2; exit 1; } Keeping Policy and Preference Apart Jump to heading
The most common way this goes wrong is not technical. A team decides to standardise configuration, someone puts their whole personal setup into the committed file, and the next person to disagree with an alias removes the include entirely — taking the settings that mattered with it.
The way to avoid it is to be explicit about the boundary. Team policy is anything where a different value produces a different commit or a different file: line endings, hook paths, signing, conflict style, whether a pull rebases. Personal preference is everything else: aliases, pager choice, colour schemes, editor. Committing the first set and leaving the second alone gives people no reason to opt out, and an include that nobody resents is an include that stays.
It also helps to write down why each policy setting is there, in the file itself. rebase.updateRefs = true looks arbitrary until you know it is what keeps stacked branches working; merge.conflictStyle = zdiff3 looks like a preference until someone resolves a conflict with the ancestor visible for the first time. A one-line comment per setting turns a list of impositions into a set of decisions, and makes the file reviewable when someone proposes a change.
Finally, expect the audit to find things and treat that as the system working rather than as people being careless. Machines get rebuilt, tools rewrite configuration files, and a colleague who pair-programmed on someone else’s laptop last week may have left an identity behind. A check that runs automatically and says clearly what to run is worth more than any amount of documentation about what everyone should have done.
Configuration Reference Jump to heading
| Setting | Recommended | Effect | Scope |
|---|---|---|---|
core.hooksPath | .githooks | Where hooks live, committed with the code | Team policy |
core.autocrlf | false | Leaves line endings to attributes | Team policy |
merge.conflictStyle | zdiff3 | Shows the common ancestor in conflicts | Team policy |
diff.algorithm | histogram | Produces more readable diffs | Team policy |
pull.rebase | true | Avoids accidental merge commits on pull | Team policy |
fetch.prune | true | Removes refs deleted on the remote | Team policy |
rebase.updateRefs | true | Keeps stacked branches intact | Team policy |
user.email | per directory | Identity recorded in every commit | Personal, scoped |
commit.gpgsign | true for work | Signs every commit | Team policy, personal key |
Troubleshooting Jump to heading
| Symptom | Likely cause | Fix |
|---|---|---|
| Hooks do not run for one person | core.hooksPath unset in that clone | Run the setup script; add the audit check |
| Whole files appear changed with no edits | Line-ending conversion differs per machine | Set attributes and renormalise once |
| Commits arrive with a personal email | No conditional include for the work directory | Add includeIf by directory |
| Committed settings have no effect | include.path never added to the clone | Run the setup script |
| A setting differs and nobody knows why | Set at a different scope | git config --show-origin --get <key> |
| New clone behaves differently from everyone else | Setup step skipped | Make setup a single command and check it in CI |
Frequently Asked Questions Jump to heading
Why can Git not just read configuration from the repository? Jump to heading
Because a repository you clone is untrusted input, and configuration can run commands — hooks, pagers, diff drivers, credential helpers. Automatically honouring a cloned repository’s configuration would mean cloning a repository could execute code. The explicit include.path step is the deliberate consent that makes it safe.
Should aliases be part of team configuration? Jump to heading
Better not. Aliases are the part people have strong opinions about and the part with no correctness implications, so including them is the quickest way to make someone opt out of the whole file. Share them in documentation for anyone who wants them.
How do we handle contractors and contributors from outside? Jump to heading
They get the attributes automatically, which covers the settings that affect committed content. The include step should be in the contributing documentation, and the pipeline checks should not assume it — enforce what matters on the server, exactly as with hooks. The reasoning is in mirroring local hook checks in server-side policy.
Does any of this work on Windows? Jump to heading
All of it, and the line-ending settings matter more there than anywhere else. The one adjustment is that core.autocrlf is often set globally by the installer, so the audit should check it explicitly rather than assuming the attributes will win.
What about settings that only matter in continuous integration? Jump to heading
Keep them in the pipeline definition rather than in the shared file. A setting like advice.detachedHead=false or a credential helper configured for a runner is noise on a developer machine and confusing when someone finds it in the team configuration and wonders why their local Git behaves differently from the examples.
The line to draw is whether the setting affects what ends up committed. Line endings, hook paths and signing all do, so they belong to everyone. Runner-specific tuning — fetch parallelism, garbage-collection thresholds, output verbosity — belongs to the environment that needs it, set in the job rather than inherited. Mixing the two produces a shared file that is half policy and half infrastructure, which is exactly the file people stop reading before they stop including.
There is one genuine overlap worth calling out: settings that make the pipeline reproduce a developer’s environment closely enough that a failure means the same thing in both places. Merge conflict style, diff algorithm and attribute handling all qualify, and having them identical in CI and locally is what lets you say with confidence that a conflict a developer resolved is the conflict the pipeline would have seen.
Related Jump to heading
- Shipping a Team gitconfig With includeIf — the include mechanism and what belongs in the file.
- Standardising Line Endings With gitattributes — fixing the whole-file-diff problem permanently.
- Bootstrapping a Developer Machine for Git — one command from a fresh machine to a correct clone.
- Enforcing a Minimum Git Version — when a setting simply does not exist on an old client.
- Auditing Local Git Configuration Drift — finding the machines that have wandered.