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”.

Three mechanisms, three scopesAttributes travel with the repository and apply automatically. A committed config file travels but must be included by each machine once. Local settings never travel at all, which is correct for identity and credentials and a problem for everything else..gitattributescommittedapplies automaticallyper pathCommitted configcommittedneeds one includeper repositoryLocal confignever travelsidentity, credentialsper machineanything that must be true for everyone belongs in the first two columns

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.

How a setting reaches a developer's machineAttributes arrive with the checkout and apply immediately. Committed configuration arrives with the checkout but needs one include command per clone. Local configuration is set on the machine and never travels, so it must be set up deliberately.Attributescheckoutapplies at onceCommitted configone include commandBootstrap scriptruns the includesets identityAuditfinds machines that driftedthe fourth box exists because the second and third depend on a step someone has to run

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; }
What an audit of twenty clones typically findsMost machines are configured correctly. A meaningful minority have hooks disabled, a stale identity, or a line-ending setting that will produce a whole-file diff the next time they touch a text file.clones out of twenty with each problemfully correct13hooks not installed4wrong or unset identity2line-ending setting wrong1the red bar is the one that silently disables every local check you rely on

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

SettingRecommendedEffectScope
core.hooksPath.githooksWhere hooks live, committed with the codeTeam policy
core.autocrlffalseLeaves line endings to attributesTeam policy
merge.conflictStylezdiff3Shows the common ancestor in conflictsTeam policy
diff.algorithmhistogramProduces more readable diffsTeam policy
pull.rebasetrueAvoids accidental merge commits on pullTeam policy
fetch.prunetrueRemoves refs deleted on the remoteTeam policy
rebase.updateRefstrueKeeps stacked branches intactTeam policy
user.emailper directoryIdentity recorded in every commitPersonal, scoped
commit.gpgsigntrue for workSigns every commitTeam policy, personal key

Troubleshooting Jump to heading

SymptomLikely causeFix
Hooks do not run for one personcore.hooksPath unset in that cloneRun the setup script; add the audit check
Whole files appear changed with no editsLine-ending conversion differs per machineSet attributes and renormalise once
Commits arrive with a personal emailNo conditional include for the work directoryAdd includeIf by directory
Committed settings have no effectinclude.path never added to the cloneRun the setup script
A setting differs and nobody knows whySet at a different scopegit config --show-origin --get <key>
New clone behaves differently from everyone elseSetup step skippedMake 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.