Skip to content

Git Configuration: Scopes, Precedence and Settings

Lesson 9 of 11Intermediate → Advanced11 min readModern Git Workflows · Modern Git ProductivityVerified: Git 2.43.0 on Ubuntu 24.04; every command and output in this lesson was run

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.

ScopeFlagFileApplies to
System--system/etc/gitconfigEvery user on the machine
Global--global~/.gitconfig or ~/.config/git/configYour user, all repositories
Local--local.git/configOne repository
Worktree--worktree.git/config.worktreeOne worktree (opt-in)
Command-c key=valueOne invocation

Later entries override earlier ones. A local setting beats a global one; -c on the command line beats everything.

Terminal window
git config --local mga.demo local-value
git config --global mga.demo global-value
git config --get mga.demo
local-value

The local value wins, even though the global one was set afterwards. Precedence is about scope, not about when you set it.

This is the command that answers most configuration questions.

Terminal window
git config --list --show-scope --show-origin

What 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=Dev
global file:/home/you/.gitconfig user.email=personal@example.com
local file:.git/config core.repositoryformatversion=0
local file:.git/config core.filemode=true

For a single key:

Terminal window
git config --show-scope --show-origin --get user.email

To see every value for a key across all scopes, in precedence order:

Terminal window
git config --show-scope --get-all mga.demo
global global-value
local local-value

The last line is the winner.

Terminal window
git config --get user.email # one value
git config --get-all remote.origin.fetch # all values for a multi-valued key
git config --get-regexp '^alias\.' # keys matching a pattern
git config --list # everything, merged

Writing:

Terminal window
git config --global user.email "you@example.com" # set
git config --global --add remote.origin.fetch "..." # add to a multi-valued key
git config --global --unset user.signingkey # remove one
git config --global --unset-all core.gitproxy # remove all values
git config --global --rename-section old new # rename a section
git config --global --remove-section alias # remove a section entirely

Editing the file directly is often easier for bulk changes:

Terminal window
git config --global --edit

The format is INI:

[user]
name = Dev
email = you@example.com
[core]
editor = nano
[alias]
st = status --short --branch

Note that Git normalises key names to lowercase when reading, so user.Email and user.email are the same key.

By default all worktrees share one configuration. To make a setting apply to just one, enable the extension first:

Terminal window
git config extensions.worktreeConfig true
git 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.

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.

~/.gitconfig
[user]
name = Dev
email = personal@example.com
[includeIf "gitdir:~/work/"]
path = ~/.gitconfig-work
~/.gitconfig-work
[user]
email = work@example.com

Now any repository under ~/work/ uses the work address, and everything else uses the personal one:

Terminal window
cd ~/work/some-project && git config --get user.email
work@example.com
Terminal window
cd ~/personal/side-project && git config --get user.email
personal@example.com

And --show-origin confirms which file supplied it:

Terminal window
cd ~/work/some-project && git config --show-origin --get user.email
file:/home/you/.gitconfig-work work@example.com

The available conditions:

ConditionMatches
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-work

Unconditional includes let you split configuration into files:

[include]
path = ~/.gitconfig-aliases

Paths 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:

Terminal window
git config --local include.path ../.gitaliases

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

Terminal window
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.

Terminal window
git config --global init.defaultBranch main
Terminal window
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.

Perhaps the most valuable setting on this list:

Terminal window
git config --global pull.ff only

git 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:

Terminal window
git config --global pull.rebase true

Pick one deliberately. pull.rebase takes precedence over pull.ff when both are set.

Terminal window
git config --global push.default simple
git config --global push.autoSetupRemote true

push.autoSetupRemote means a first git push on a new branch sets its upstream automatically, removing the git push -u origin <branch> step.

Terminal window
# Windows
git config --global core.autocrlf true
# macOS and Linux
git config --global core.autocrlf input

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

Terminal window
git config --global rebase.autoStash true
git config --global rebase.autoSquash true
git config --global rebase.updateRefs true

Respectively: stash uncommitted changes around a rebase, honour fixup! commits automatically, and keep stacked branches coherent.

Terminal window
git config --global merge.conflictStyle zdiff3
git config --global rerere.enabled true

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

Terminal window
git config --global diff.algorithm histogram
git config --global diff.colorMoved zebra

colorMoved highlights lines that moved rather than changed, which makes reviewing a refactor substantially easier.

Terminal window
git config --global transfer.fsckObjects true
git config --global fetch.fsckObjects true
git config --global receive.fsckObjects true

These make Git verify object integrity on transfer, rejecting malformed objects. There is a small performance cost and a meaningful robustness gain.

Terminal window
git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_ed25519.pub
git config --global commit.gpgsign true
git config --global tag.gpgsign true

commit.gpgsign true signs every commit without needing -S. See Signed Commits.

Terminal window
# macOS
git config --global credential.helper osxkeychain
# Windows
git config --global credential.helper manager
# Linux, in-memory for one hour
git 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=7200

Credential Managers covers the mechanism.

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.

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:

Terminal window
git config --global --add safe.directory /path/to/repo

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

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]
bare

Integers accept k, m and g suffixes:

[core]
packedGitLimit = 512m
[http]
postBuffer = 1m

Colours take a foreground, an optional background, and attributes:

[color "branch"]
current = green bold
remote = red dim

Paths expand ~ and ~user:

[core]
excludesFile = ~/.gitignore_global

Multi-valued keys accumulate rather than replace. remote.origin.fetch and safe.directory are the common examples:

Terminal window
git config --global --add safe.directory /path/one
git config --global --add safe.directory /path/two
git config --global --get-all safe.directory

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

One setting worth calling out separately, because most people configure it once and benefit forever:

Terminal window
git config --global core.excludesFile ~/.gitignore_global
~/.gitignore_global
.DS_Store
Thumbs.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.

A procedure for “this setting is not working”:

  1. Find the effective value and its origin:

    Terminal window
    git config --show-scope --show-origin --get <key>
  2. See every value across scopes:

    Terminal window
    git config --show-scope --get-all <key>

    The last line wins.

  3. Check whether an include is involved. --show-origin names the actual file, which may be an included one rather than ~/.gitconfig.

  4. Confirm you are in the repository you think you are:

    Terminal window
    git rev-parse --show-toplevel
  5. Check for a typo. Git happily stores unknown keys without complaint:

    Terminal window
    git config --global --get-regexp 'user\.'

    A stray user.emial will sit there silently forever.

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

A few environment variables override configuration, which matters mostly for scripts and CI:

VariableOverrides
GIT_CONFIG_GLOBALPath to the global config file
GIT_CONFIG_SYSTEMPath to the system config file
GIT_AUTHOR_NAME / GIT_AUTHOR_EMAILAuthor identity
GIT_COMMITTER_NAME / GIT_COMMITTER_EMAILCommitter identity
GIT_EDITORThe editor Git opens
GIT_DIR / GIT_WORK_TREERepository 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.

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 = osxkeychain

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

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.

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-origin tells you which sheet you are actually reading.

  • Scopes run system → global → local → worktree → command line, most specific winning.
  • git config --list --show-scope --show-origin identifies where every value came from.
  • --get-all with --show-scope shows 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 zdiff3 and rerere.enabled are high-value defaults.
  • Git does not validate key names, so typos are silent.
  • Never run git config --global under sudo.
  1. Demonstrate precedence:

    Terminal window
    mkdir ~/config-lab && cd ~/config-lab && git init
    git config --global mga.demo global-value
    git config --local mga.demo local-value
    git config --get mga.demo

    Predict which wins.

  2. See both: git config --show-scope --get-all mga.demo.

  3. Override from the command line:

    Terminal window
    git -c mga.demo=command-value config --get mga.demo
  4. Set up a conditional include. Create ~/.gitconfig-work containing:

    [user]
    email = work@example.com

    Then add to ~/.gitconfig:

    [includeIf "gitdir:~/work/"]
    path = ~/.gitconfig-work
  5. Test it:

    Terminal window
    mkdir -p ~/work/test && cd ~/work/test && git init
    git config --show-origin --get user.email
  6. Confirm it does not leak:

    Terminal window
    cd ~/config-lab && git config --get user.email
  7. Break it deliberately: remove the trailing slash from gitdir:~/work and re-test step 5.

  8. Clean up:

    Terminal window
    git config --global --unset mga.demo
    git 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.

Credential configuration is important enough — and easy enough to get insecurely wrong — to deserve its own lesson.