Git Configuration: Scopes, Precedence and Settings
Git reads configuration from several files, in a defined order, and the most specific one wins. Almost every “why is this setting not applying?” question is a precedence question.
This lesson covers the scopes, how to find where a value came from, conditional includes for multiple identities, and the settings actually worth changing.
The scopes
Section titled “The scopes”| Scope | Flag | File | Applies to |
|---|---|---|---|
| System | --system | /etc/gitconfig | Every user on the machine |
| Global | --global | ~/.gitconfig or ~/.config/git/config | Your user, all repositories |
| Local | --local | .git/config | One repository |
| Worktree | --worktree | .git/config.worktree | One worktree (opt-in) |
| Command | -c key=value | — | One invocation |
Later entries override earlier ones. A local setting beats a global one; -c on the command line beats
everything.
git config --local mga.demo local-valuegit config --global mga.demo global-valuegit config --get mga.demolocal-valueThe local value wins, even though the global one was set afterwards. Precedence is about scope, not about when you set it.
Finding where a value came from
Section titled “Finding where a value came from”This is the command that answers most configuration questions.
git config --list --show-scope --show-originWhat it doesPrints every setting with the scope it came from and the file that defines it.
Why we run itWhen a value is not what you expect, this identifies the file responsible — which is almost always the actual question.
Expected resultOne line per setting, prefixed with system, global or local and the path to its file.
global file:/home/you/.gitconfig user.name=Devglobal file:/home/you/.gitconfig user.email=personal@example.comlocal file:.git/config core.repositoryformatversion=0local file:.git/config core.filemode=trueFor a single key:
git config --show-scope --show-origin --get user.emailTo see every value for a key across all scopes, in precedence order:
git config --show-scope --get-all mga.demoglobal global-valuelocal local-valueThe last line is the winner.
Reading and writing
Section titled “Reading and writing”git config --get user.email # one valuegit config --get-all remote.origin.fetch # all values for a multi-valued keygit config --get-regexp '^alias\.' # keys matching a patterngit config --list # everything, mergedWriting:
git config --global user.email "you@example.com" # setgit config --global --add remote.origin.fetch "..." # add to a multi-valued keygit config --global --unset user.signingkey # remove onegit config --global --unset-all core.gitproxy # remove all valuesgit config --global --rename-section old new # rename a sectiongit config --global --remove-section alias # remove a section entirelyEditing the file directly is often easier for bulk changes:
git config --global --editThe format is INI:
[user] name = Dev email = you@example.com[core] editor = nano[alias] st = status --short --branchNote that Git normalises key names to lowercase when reading, so user.Email and user.email are the
same key.
Worktree-scoped configuration
Section titled “Worktree-scoped configuration”By default all worktrees share one configuration. To make a setting apply to just one, enable the extension first:
git config extensions.worktreeConfig truegit config --worktree user.email "other@example.com"The value goes into that worktree’s own config.worktree file. This is how
sparse checkout manages per-worktree path lists.
You rarely need this manually, and enabling the extension changes how Git reads configuration for the repository — do it deliberately rather than experimentally.
Conditional includes
Section titled “Conditional includes”The best solution to “work commits need one email, personal commits another”. Rather than remembering to set a local override in every repository, make the choice automatic based on where the repository is.
[user] name = Dev email = personal@example.com
[includeIf "gitdir:~/work/"] path = ~/.gitconfig-work[user] email = work@example.comNow any repository under ~/work/ uses the work address, and everything else uses the personal one:
cd ~/work/some-project && git config --get user.emailwork@example.comcd ~/personal/side-project && git config --get user.emailpersonal@example.comAnd --show-origin confirms which file supplied it:
cd ~/work/some-project && git config --show-origin --get user.emailfile:/home/you/.gitconfig-work work@example.comThe available conditions:
| Condition | Matches |
|---|---|
gitdir:<path> | Repository located under <path> |
gitdir/i:<path> | Same, case-insensitively |
onbranch:<pattern> | The checked-out branch matches |
hasconfig:remote.*.url:<pattern> | A remote URL matches — useful for matching by host |
The hasconfig form is useful when repositories are not organised by directory:
[includeIf "hasconfig:remote.*.url:git@github.com:mycompany/**"] path = ~/.gitconfig-workPlain includes
Section titled “Plain includes”Unconditional includes let you split configuration into files:
[include] path = ~/.gitconfig-aliasesPaths are relative to the file containing the directive. In a repository’s .git/config, ../ refers to
the working tree root, which is how a project can ship shared aliases:
git config --local include.path ../.gitaliasesThe file .gitaliases at the repository root is then read as configuration. The include.path line itself
is local config and is not cloned, so it still needs running per clone.
Settings worth changing
Section titled “Settings worth changing”Identity
Section titled “Identity”git config --global user.name "Your Name"git config --global user.email "you@example.com"Required before your first commit, and part of every commit’s identity permanently.
Default branch
Section titled “Default branch”git config --global init.defaultBranch mainEditor and pager
Section titled “Editor and pager”git config --global core.editor "nano"git config --global core.pager "less -FRX"-F on less exits immediately if the output fits on one screen, which removes the mildly irritating
pager-for-three-lines behaviour.
Pull behaviour
Section titled “Pull behaviour”Perhaps the most valuable setting on this list:
git config --global pull.ff onlygit pull becomes “fetch and fast-forward, or fail and tell me”. Without it, a diverged branch produces a
merge commit you did not ask for. The alternative, if you prefer rebasing:
git config --global pull.rebase truePick one deliberately. pull.rebase takes precedence over pull.ff when both are set.
Push behaviour
Section titled “Push behaviour”git config --global push.default simplegit config --global push.autoSetupRemote truepush.autoSetupRemote means a first git push on a new branch sets its upstream automatically, removing
the git push -u origin <branch> step.
Line endings
Section titled “Line endings”# Windowsgit config --global core.autocrlf true# macOS and Linuxgit config --global core.autocrlf inputA committed .gitattributes with * text=auto is the better team-wide answer, since it does not depend on
everyone configuring their machine. See Installing Git on
Windows.
Rebase quality of life
Section titled “Rebase quality of life”git config --global rebase.autoStash truegit config --global rebase.autoSquash truegit config --global rebase.updateRefs trueRespectively: stash uncommitted changes around a rebase, honour fixup! commits automatically, and keep
stacked branches coherent.
Conflict style
Section titled “Conflict style”git config --global merge.conflictStyle zdiff3git config --global rerere.enabled truezdiff3 shows the common ancestor in conflict markers, which frequently makes the correct resolution
obvious. rerere records how you resolved a conflict and reapplies it if the same one recurs.
Diff readability
Section titled “Diff readability”git config --global diff.algorithm histogramgit config --global diff.colorMoved zebracolorMoved highlights lines that moved rather than changed, which makes reviewing a refactor
substantially easier.
Safety
Section titled “Safety”git config --global transfer.fsckObjects truegit config --global fetch.fsckObjects truegit config --global receive.fsckObjects trueThese make Git verify object integrity on transfer, rejecting malformed objects. There is a small performance cost and a meaningful robustness gain.
Signing
Section titled “Signing”git config --global gpg.format sshgit config --global user.signingkey ~/.ssh/id_ed25519.pubgit config --global commit.gpgsign truegit config --global tag.gpgsign truecommit.gpgsign true signs every commit without needing -S. See
Signed Commits.
Credentials
Section titled “Credentials”# macOSgit config --global credential.helper osxkeychain# Windowsgit config --global credential.helper manager# Linux, in-memory for one hourgit config --global credential.helper 'cache --timeout=3600'Helpers can also be configured per host, which matters when different remotes need different authentication:
[credential "https://github.com"] helper = manager[credential "https://git.internal.example.com"] helper = cache --timeout=7200Credential Managers covers the mechanism.
URL rewriting
Section titled “URL rewriting”Occasionally useful when a repository’s committed submodule URLs use a protocol you cannot:
[url "git@github.com:"] insteadOf = https://github.com/Every https://github.com/ URL is transparently rewritten to SSH. The reverse is common in CI, where SSH
keys are unavailable and a token over HTTPS is used instead.
Configuration you should not set casually
Section titled “Configuration you should not set casually”credential.helper store. Writes credentials to a plain-text file. See
Credential Managers.
http.sslVerify false. Disables certificate verification for all hosts. If a specific internal host has
a private CA, configure that host’s CA bundle rather than disabling verification globally.
safe.directory *. Git refuses to operate on repositories owned by another user, which is a genuine
protection. Add specific paths instead:
git config --global --add safe.directory /path/to/repocore.fileMode false. Makes Git ignore executable-bit changes. Occasionally necessary on filesystems
that cannot represent them; otherwise it hides real changes.
gc.auto 0. Disables automatic maintenance entirely. The repository will degrade over time.
Value types and syntax
Section titled “Value types and syntax”Git interprets configuration values by expected type, and the accepted forms are worth knowing because error messages when you get one wrong are unhelpful.
Booleans accept true, false, yes, no, on, off, 1 and 0. A key present with no value is
also true:
[core] bareIntegers accept k, m and g suffixes:
[core] packedGitLimit = 512m[http] postBuffer = 1mColours take a foreground, an optional background, and attributes:
[color "branch"] current = green bold remote = red dimPaths expand ~ and ~user:
[core] excludesFile = ~/.gitignore_globalMulti-valued keys accumulate rather than replace. remote.origin.fetch and safe.directory are the
common examples:
git config --global --add safe.directory /path/onegit config --global --add safe.directory /path/twogit config --global --get-all safe.directoryUsing plain git config on a multi-valued key sets a single value and discards the rest, which is
occasionally a nasty surprise. Use --add to append and --unset-all to clear.
Quoting and comments. Values containing #, ; or leading whitespace need double quotes; # and ;
start comments:
[alias] todo = "grep -n TODO # find outstanding work"Case. Section and key names are case-insensitive and normalised to lowercase on read. Subsection names
— the quoted part, as in [remote "origin"] — are case-sensitive.
A global gitignore
Section titled “A global gitignore”One setting worth calling out separately, because most people configure it once and benefit forever:
git config --global core.excludesFile ~/.gitignore_global.DS_StoreThumbs.db*.swp.idea/.vscode/This is where editor and operating-system noise belongs. A project’s .gitignore should describe what
that project produces — build output, dependencies, generated files. Your editor’s directory is your
concern, and adding it to every project’s .gitignore imposes your toolchain on everyone else.
Debugging precedence
Section titled “Debugging precedence”A procedure for “this setting is not working”:
-
Find the effective value and its origin:
Terminal window git config --show-scope --show-origin --get <key> -
See every value across scopes:
Terminal window git config --show-scope --get-all <key>The last line wins.
-
Check whether an include is involved.
--show-originnames the actual file, which may be an included one rather than~/.gitconfig. -
Confirm you are in the repository you think you are:
Terminal window git rev-parse --show-toplevel -
Check for a typo. Git happily stores unknown keys without complaint:
Terminal window git config --global --get-regexp 'user\.'A stray
user.emialwill sit there silently forever. -
Test with a command-scope override, which beats every file:
Terminal window git -c user.email=test@example.com config --get user.email
Step 5 catches a surprising proportion of real cases. Git does not validate key names, so a misspelling is indistinguishable from a setting that is being ignored.
Environment variables
Section titled “Environment variables”A few environment variables override configuration, which matters mostly for scripts and CI:
| Variable | Overrides |
|---|---|
GIT_CONFIG_GLOBAL | Path to the global config file |
GIT_CONFIG_SYSTEM | Path to the system config file |
GIT_AUTHOR_NAME / GIT_AUTHOR_EMAIL | Author identity |
GIT_COMMITTER_NAME / GIT_COMMITTER_EMAIL | Committer identity |
GIT_EDITOR | The editor Git opens |
GIT_DIR / GIT_WORK_TREE | Repository and working tree locations |
Setting GIT_CONFIG_GLOBAL to a scratch file is the clean way to run Git in a test with completely
isolated configuration — which is exactly how the examples in this pillar were verified.
Managing configuration across machines
Section titled “Managing configuration across machines”Once you have a configuration you like, keeping it consistent across machines becomes the problem.
Commit your dotfiles. ~/.gitconfig in a private repository, symlinked into place, is the common
approach. The complication is machine-specific values — a signing key path, a credential helper that
differs by operating system.
Split machine-specific settings out, and include them conditionally or optionally:
# ~/.gitconfig — committed[include] path = ~/.gitconfig-local[core] editor = nano[init] defaultBranch = main# ~/.gitconfig-local — NOT committed[user] signingkey = ~/.ssh/id_ed25519.pub[credential] helper = osxkeychainA missing include file is not an error, so the same committed config works on a machine that has not created its local overrides yet.
Never commit credentials or key material. A .gitconfig containing a credential.helper that embeds a
token is a credential in a repository. Keep secrets in the credential store, not in configuration.
Common mistakes
Section titled “Common mistakes”Setting identity locally by accident. git config user.email without --global writes to the
repository. Symptom: “I set my email but new repositories still have the wrong one.”
Running git config --global under sudo. Configures root.
Expecting a local setting to be shared. .git/config is not cloned.
Missing the trailing slash in gitdir:. The include silently does not apply.
Assuming pull.ff applies when pull.rebase is set. Rebase wins.
Typos in key names. Stored silently, never used.
Disabling safe.directory globally. Removes a real protection; add specific paths.
Not checking --show-origin first. It answers the question directly.
Mental Model
Section titled “Mental Model”Configuration is a stack of transparencies.
System is the bottom sheet, then global, then the repository’s own, then the worktree’s, then anything on the command line. Look down through them and you see the topmost value for each setting.
--show-origintells you which sheet you are actually reading.
What You Learned
Section titled “What You Learned”- Scopes run system → global → local → worktree → command line, most specific winning.
git config --list --show-scope --show-originidentifies where every value came from.--get-allwith--show-scopeshows every value for a key in precedence order.- Conditional includes (
includeIf) select configuration by directory, branch or remote URL. - Trailing slashes matter in
gitdir:patterns. - Worktree-scoped config requires
extensions.worktreeConfig. pull.ff only,push.autoSetupRemote,merge.conflictStyle zdiff3andrerere.enabledare high-value defaults.- Git does not validate key names, so typos are silent.
- Never run
git config --globalundersudo.
Try It Yourself
Section titled “Try It Yourself”-
Demonstrate precedence:
Terminal window mkdir ~/config-lab && cd ~/config-lab && git initgit config --global mga.demo global-valuegit config --local mga.demo local-valuegit config --get mga.demoPredict which wins.
-
See both:
git config --show-scope --get-all mga.demo. -
Override from the command line:
Terminal window git -c mga.demo=command-value config --get mga.demo -
Set up a conditional include. Create
~/.gitconfig-workcontaining:[user]email = work@example.comThen add to
~/.gitconfig:[includeIf "gitdir:~/work/"]path = ~/.gitconfig-work -
Test it:
Terminal window mkdir -p ~/work/test && cd ~/work/test && git initgit config --show-origin --get user.email -
Confirm it does not leak:
Terminal window cd ~/config-lab && git config --get user.email -
Break it deliberately: remove the trailing slash from
gitdir:~/workand re-test step 5. -
Clean up:
Terminal window git config --global --unset mga.demogit config --global --remove-section 'includeIf.gitdir:~/work/'
Step 7 is the one worth doing. Watching the include silently stop applying because of one character is how you remember to check it first next time.
Next Lesson
Section titled “Next Lesson”Credential configuration is important enough — and easy enough to get insecurely wrong — to deserve its own lesson.