Git is an enormously configurable tool: author name and e-mail, text editor, behaviour when integrating changes, output colours, shortcuts, credentials and several hundred more options. All of it is managed with a single command, git config, and stored in plain text files with a simple format.

This lesson is about the mechanism: where the configuration lives, what levels exist, who wins when two levels disagree, how to read, write and delete values, and how to keep separate profiles for work and for personal projects. We deliberately will not list yet which values are worth setting the first time round: that is the content of the next lesson, Initial Configuration. Here we learn to handle the controls; next, which buttons to press.

Ana needs this right now: the laptop she uses for her company's work is the same one she uses for task-manager, and she wants each project to carry the right e-mail without having to remember every time.

Contents

  1. What git config is
  2. The three configuration levels
  3. Precedence: who wins
  4. Reading values
  5. Writing values
  6. Deleting values
  7. Inside the .gitconfig file
  8. Direct editing with --edit
  9. Conditional configuration with includeIf
  10. Special cases: multiple values and types

  1. What git config is

git config is the command that reads and writes Git's configuration. Its basic form is:

git config <level> <section>.<key> <value>

A concrete example, without going into what the value means yet:

git config --global core.editor "nano"

Breaking down each part:

  • git config → the command.
  • --global → the level: which file gets written. If omitted, the local level is used.
  • core.editor → the key, made up of a section (core) and a name (editor), separated by a dot.
  • "nano" → the value. Always quote it whenever it contains spaces or special characters.

It matters that you understand that git config does not validate keys. If you write core.editorr with two r's, Git will accept it without complaint and the option will simply have no effect whatsoever. It is the number one cause of configurations that "don't work".

A note on versions. Since Git 2.46 there is a more explicit alternative syntax: git config get, git config set, git config unset and git config list. It is equivalent to the classic form and somewhat more readable. In this course we will use the classic syntax with options (--get, --unset, --list) because it works on every version and it is what you will find in existing documentation.

  1. The three configuration levels

Git reads its configuration from several files, organised into levels of decreasing scope. The three main ones are:

Level Option Scope File path
System --system Every user on the machine Linux: /etc/gitconfig
macOS (Homebrew): /opt/homebrew/etc/gitconfig
Windows: C:\Program Files\Git\etc\gitconfig
Global --global Every repository belonging to your user Linux/macOS: ~/.gitconfig or ~/.config/git/config
Windows: C:\Users\<user>\.gitconfig
Local --local The current repository only <repository>/.git/config

There are also two less common levels worth knowing about:

Level Option Scope
Worktree --worktree The current working copy only, when several are in use (module 6)
Command -c key=value That one command's execution only

The command level is very handy for one-off tests, because it leaves no trace:

git -c user.email="ana.ferrer@personal.example" commit -m "Fix list styles"

Notice that -c goes before the subcommand, not after: it is an option of git, not of commit.

When to use each level

Level Use it for
System Machine-wide policy; in practice administrators touch it, almost never you
Global Your identity and your personal preferences: editor, colours, shortcuts
Local Whatever is specific to a project: a different e-mail, its own line-ending settings

The general rule: always start with --global. Save --local for the genuine exceptions of one particular project.

  1. Precedence: who wins

When several levels define the same key, the most specific one wins. The order, from lowest to highest priority:

graph TD
    S["/etc/gitconfig<br/>--system<br/>lowest priority"] --> G["~/.gitconfig<br/>--global"]
    G --> L[".git/config<br/>--local"]
    L --> W[".git/config.worktree<br/>--worktree"]
    W --> C["git -c key=value<br/>highest priority"]

A concrete example on Ana's laptop:

File Value of user.email
/etc/gitconfig (not defined)
~/.gitconfig ana.ferrer@company.com
task-manager/.git/config ana.ferrer@personal.example

Result: inside the task-manager repository, Git will use ana.ferrer@personal.example. In any other repository on Ana's laptop, ana.ferrer@company.com.

This is exactly the tool Ana needed: the global configuration covers the majority case (her job) and the local one handles the exception (her personal project). In section 9 we will see how to automate it so she never has to remember in each new repository.

  1. Reading values

A key's effective value

git config --get user.email

This returns the value Git would use right now, with precedence already resolved. It also works without --get, which is the shorter and more common form:

git config user.email

If the key is not defined, the command prints nothing and returns a non-zero exit code. To supply a default when it does not exist:

git config --default "undefined" --get user.email

Reading one specific level

Combining --get with a level queries one specific file, ignoring the rest:

git config --global --get user.email     # the global file only
git config --local --get user.email      # the repository's file only
git config --system --get user.email     # the system file only

This is essential for debugging: if the effective value is not what you expected, checking level by level reveals where it comes from.

Listing the whole configuration

git config --list

This prints every active key, one per line, as key=value:

user.name=Ana Ferrer
user.email=ana.ferrer@company.com
core.editor=nano
init.defaultbranch=main
color.ui=auto

And now the most useful command in the whole lesson:

git config --list --show-origin

It prefixes each line with the file it came from:

file:/etc/gitconfig            core.autocrlf=input
file:/home/ana/.gitconfig      user.name=Ana Ferrer
file:/home/ana/.gitconfig      user.email=ana.ferrer@company.com
file:/home/ana/.gitconfig      core.editor=nano
file:.git/config               user.email=ana.ferrer@personal.example
file:.git/config               remote.origin.url=git@git.example.com:team/task-manager.git

Look at the last appearance of user.email: when a key shows up several times, the last line is the one that wins, because the files are processed from least to most specific. Here the effective value is ana.ferrer@personal.example.

An even more informative variant:

git config --list --show-origin --show-scope

This adds the level's name as well (system, global, local), which saves you from working it out from the path.

Command What it shows
git config <key> The effective value
git config --global <key> The value at one specific level
git config --list Every active key
git config --list --show-origin Every key and the file it came from
git config --list --show-scope Every key and its level
git config --get-regexp <pattern> Only the keys matching a regular expression

That last one is very practical when you are looking for something and cannot remember the exact name:

# Every key in the user section
git config --get-regexp '^user\.'

# Everything to do with aliases
git config --get-regexp '^alias\.'

  1. Writing values

We already know the basic form:

git config --global user.name "Ana Ferrer"

Points to keep in mind:

  • If the key exists, it is overwritten. There is no confirmation and no warning.
  • Quotes are required when the value contains spaces. git config --global user.name Ana Ferrer would fail, because Git would read Ferrer as an extra argument.
  • Keys are case-insensitive in the section and the name (init.defaultBranch and init.defaultbranch are the same key), but values are not.
  • If you give no level, --local is used, which requires being inside a repository. Outside one, Git raises the error fatal: not in a git directory.

That last point deserves an example, because it is a classic stumble:

# Inside ~/projects/task-manager: writes to .git/config
git config user.email "ana.ferrer@personal.example"

# On the desktop, outside any repository: ERROR
git config user.email "ana.ferrer@personal.example"
# fatal: not in a git directory

Get into the habit of always stating the level explicitly. It saves confusion that is hard to diagnose.

Writing at the system level

This requires administrator privileges, because the file sits outside your home folder:

sudo git config --system core.autocrlf input

On Windows you have to open Git Bash or PowerShell as administrator.

  1. Deleting values

To remove a key, use --unset:

git config --global --unset core.editor

If the key appears several times in the same file (which is possible for certain keys, as we will see in section 10), --unset will fail with the message warning: <key> has multiple values. In that case use --unset-all:

git config --global --unset-all core.editor

To remove an entire section:

git config --global --remove-section alias
Command Effect
--unset <key> Deletes a key with a single value
--unset-all <key> Deletes every appearance of the key
--remove-section <section> Deletes the whole section

One important nuance: deleting a key from one level does not leave it "with no value", it lets the level below win. If Ana deletes user.email from task-manager's .git/config, the e-mail becomes the one in her ~/.gitconfig, not nothing at all.

And a warning: --unset with no explicit level operates on the local file. If you meant to clean up the global one and forget --global, you will delete something you did not intend to.

  1. Inside the .gitconfig file

Every Git configuration file uses the same format: INI, plain text, readable and editable by hand. This could be Ana's ~/.gitconfig:

[user]
	name = Ana Ferrer
	email = ana.ferrer@company.com

[core]
	editor = nano
	autocrlf = input

[init]
	defaultBranch = main

[color]
	ui = auto

[alias]
	st = status
	co = checkout
	last = log -1 HEAD

[pull]
	rebase = false

The rules of the format:

  • Sections go in square brackets: [user], [core]. They correspond to the part before the dot in the key.
  • Keys are written as name = value, indented with a tab by convention (Git writes a tab; spaces work too).
  • Comments begin with # or ;.
  • Values with spaces need no quotes inside the file, unless you want to preserve leading or trailing spaces.

The mapping between command and file is direct:

Command Result in the file
git config --global user.name "Ana Ferrer" [user]name = Ana Ferrer
git config --global alias.st status [alias]st = status
git config --global color.ui auto [color]ui = auto

Subsections

Some sections take one more level, in quotes. You see them mostly on remotes and branches:

[remote "origin"]
	url = git@git.example.com:team/task-manager.git
	fetch = +refs/heads/*:refs/remotes/origin/*

[branch "main"]
	remote = origin
	merge = refs/heads/main

On the command line, the subsection goes in the middle: remote.origin.url, branch.main.remote. Git writes these entries automatically when you add a remote or set up a branch's tracking, both matters for module 4. You will rarely write them by hand, but being able to read them helps enormously when diagnosing problems.

Important: unlike keys, subsections are case-sensitive. [remote "Origin"] and [remote "origin"] are two different remotes.

  1. Direct editing with --edit

For changes spanning several lines, editing the file directly is far more comfortable than stringing commands together:

git config --global --edit

This opens ~/.gitconfig in the editor set in core.editor (or in whichever the EDITOR environment variable names). Save and close, and the changes take effect immediately: Git reads the configuration on every run, it does not cache it.

The three variants:

git config --system --edit    # the system file (requires permissions)
git config --global --edit    # your personal file
git config --local --edit     # the current repository's file

You can also open the file with any editor, bypassing Git:

nano ~/.gitconfig
code ~/.gitconfig

That is just as valid. The advantage of --edit is that you do not need to remember the path, which varies between systems.

Careful: if you write a configuration file with a syntax error (an unclosed bracket, say), Git will complain on every command you run from that moment on:

fatal: bad config line 12 in file /home/ana/.gitconfig

The fix is to reopen the file and correct the line it names. It is reversible and it damages no repository, but it is disconcerting the first time.

  1. Conditional configuration with includeIf

We come now to the most elegant feature of the configuration system, and the one that solves Ana's problem for good.

The problem

Ana uses the same laptop for two contexts:

  • Her company's projects, which must be signed with ana.ferrer@company.com.
  • Personal projects such as task-manager, which must go out with ana.ferrer@personal.example.

She can handle it with local configuration repository by repository, but that means remembering every time she clones or creates one. Sooner or later she will forget and make commits with the wrong e-mail, something only a history rewrite can fix.

The solution: including configuration based on the path

Git lets you include another configuration file only when a condition holds. The most useful condition is the repository's path.

Ana organises her folders like this:

/home/ana/
├── work/             ← the company's repositories
│   └── client-portal/
└── personal/         ← her own projects
    └── task-manager/

And she edits her ~/.gitconfig:

[user]
	name = Ana Ferrer
	email = ana.ferrer@company.com

[core]
	editor = nano

[init]
	defaultBranch = main

# If the repository sits under ~/personal/, include this other file
[includeIf "gitdir:~/personal/"]
	path = ~/.gitconfig-personal

And she creates the file ~/.gitconfig-personal:

[user]
	email = ana.ferrer@personal.example

How it works, step by step:

  1. Git reads ~/.gitconfig from top to bottom and sets user.email = ana.ferrer@company.com.
  2. On reaching includeIf, it checks whether the current repository sits inside ~/personal/.
  3. If it does, it reads ~/.gitconfig-personal at that point, and its user.email overwrites the previous one.
  4. If it does not, the line is ignored and the company e-mail stands.

Result: Ana never has to remember again. She only has to keep each project in the right folder.

Checking it:

# Inside ~/personal/task-manager
git config --get user.email
# → ana.ferrer@personal.example

# Inside ~/work/client-portal
git config --get user.email
# → ana.ferrer@company.com

The conditions available

Condition Holds when
gitdir:<path> The repository sits under that path (case-sensitive)
gitdir/i:<path> The same, case-insensitive (handy on Windows and macOS)
onbranch:<name> The current branch matches that name or pattern
hasconfig:remote.*.url:<pattern> One of the repository's remotes matches that URL pattern

The rules for the path in gitdir:

  • It must end in / for it to apply to everything inside, recursively. Without the trailing slash it would only match that exact folder, and since Git compares against the path of the .git directory, it would practically never work the way you expect. This is the most frequent mistake when using includeIf.
  • It accepts ~ for the home folder.
  • It accepts wildcards: gitdir:~/clients/*/ matches any first-level subfolder.

The last condition, hasconfig:remote.*.url, is particularly powerful because it does not depend on how you organise your folders:

# Any repository whose remote lives on the company server
[includeIf "hasconfig:remote.*.url:git@git.company.com:**"]
	path = ~/.gitconfig-work

That way, even if Ana clones a company repository onto her desktop in a hurry, the right e-mail is applied anyway.

Unconditional include

There is also a version with no condition, useful for splitting a long configuration into reusable pieces:

[include]
	path = ~/.gitconfig-alias
	path = ~/.gitconfig-colors

A common pattern in teams: keep a shared file of aliases and colours in a repository, and have each person include it from their ~/.gitconfig without giving up their own identity.

  1. Special cases: multiple values and types

Keys with several values

Most keys hold a single value, but some accept several. To add without overwriting, use --add:

git config --global --add safe.directory /opt/projects/shared
git config --global --add safe.directory /srv/repos/internal

The file ends up with two lines in the same section:

[safe]
	directory = /opt/projects/shared
	directory = /srv/repos/internal

Reading them all requires --get-all, because --get would return only the last one:

git config --get-all safe.directory
Command Behaviour with multiple values
git config <key> <value> Overwrites every existing value
git config --add <key> <value> Adds one more
git config --get <key> Returns the last one
git config --get-all <key> Returns them all
git config --unset-all <key> Deletes them all

Value types

Git interprets values according to the type the key expects. You can force the interpretation when reading:

# Read as a boolean: accepts true/false, yes/no, on/off, 1/0
git config --type=bool core.ignorecase

# Read as an integer, allowing the suffixes k, m, g
git config --type=int core.bigFileThreshold

# Expand a path containing ~ into its absolute form
git config --type=path core.excludesFile

Booleans are flexible when writing. These six lines are equivalent:

git config --global color.ui true
git config --global color.ui yes
git config --global color.ui on
git config --global color.ui 1
git config --global color.ui TRUE
git config --global color.ui True

And a boolean key written with no value is read as true:

[core]
	filemode

That is valid, though hardly readable. Better to write the value explicitly.

Common Mistakes and Tips

  • Forgetting --global and writing into the repository by accident. This is the most frequent error. With no explicit level, git config writes to .git/config, so your preference applies to one project only and you are left wondering why it does not work in the others. Get into the habit of always stating the level.
  • Misspelling a key's name. Git validates nothing: core.editorr or pull.rebasse are stored without a murmur and do nothing at all. If an option has no effect, check it with git config --get <key> and compare it against the documentation (git help config).
  • Not quoting values with spaces. git config --global user.name Ana Ferrer either errors or stores just Ana. Use quotes whenever there are spaces.
  • Forgetting the trailing slash in includeIf "gitdir:...". Without the final /, the condition almost never holds and the included file is ignored silently, with no warning at all. It is a particularly hard failure to spot.
  • Editing .git/config with a syntax error. It leaves Git unusable in that repository until the line is fixed. It is neither serious nor destructive, but it is frightening. The message names the exact file and line.
  • Mixing up the levels when diagnosing. When a value is not what you expect, do not guess: run git config --list --show-origin --show-scope and find which file it comes from.
  • Tip: keep your ~/.gitconfig in a repository of your own. It is a small text file that represents hours of tuning. Many professionals keep it under version control alongside their other personal configuration files.
  • Tip: run git config --list --show-origin on every new machine. It is the first thing worth looking at when you start on an unfamiliar computer, and also when debugging odd behaviour on a company machine, where the system level can hold surprises.

Exercises

Exercise 1: Resolving precedence

On Bruno's laptop, the configuration files contain this:

/etc/gitconfig:

[core]
	autocrlf = input
[user]
	email = bruno@corporate-config.example

~/.gitconfig:

[user]
	name = Bruno Salas
	email = bruno.salas@company.com
[core]
	editor = vim

~/personal/task-manager/.git/config:

[user]
	email = bruno.salas@personal.example

Answer, from inside the task-manager repository:

  1. What value does user.email have?
  2. And user.name?
  3. And core.editor?
  4. And core.autocrlf?
  5. What command would you run to find out which file each one comes from without opening them?
  6. If Bruno runs git config --unset user.email inside task-manager, what will the new effective value be?

Exercise 2: Separate profiles with includeIf

Carla works in three contexts on the same laptop:

  • Her company's projects, in ~/work/, with the e-mail carla.vidal@company.com.
  • An outside client's projects, in ~/clients/acme/, with the e-mail c.vidal@acme-contractor.example.
  • Personal projects, in ~/code/, with the e-mail carla@example.com.

Her name is always "Carla Vidal" and her editor is always nano.

Write the complete contents of her ~/.gitconfig and of whatever auxiliary files she needs. Then say which command she would use to check, from inside ~/clients/acme/sales-dashboard, that the e-mail being applied is the right one.

Exercise 3: Diagnosis

Ana complains that she has configured her favourite editor but Git still opens Vim every time it asks her to write a message. This is what she ran:

cd ~/personal/task-manager
git config core.editorr "nano"
  1. Identify two distinct errors in that command.
  2. Write the correct command.
  3. Write the commands to clean up the wrong key that was left behind.
  4. Write the command that would have let her spot the problem herself.

Solutions

Solution to Exercise 1

  1. user.email = bruno.salas@personal.example. The local level is the most specific of the three present and beats both the global and the system one.

  2. user.name = Bruno Salas. It is only defined in the global file; nothing overwrites it.

  3. core.editor = vim. It is only in the global file.

  4. core.autocrlf = input. It is only at the system level, and no higher level redefines it, so it applies.

  5. The diagnostic command:

git config --list --show-origin --show-scope

It will show each key preceded by its level and by the file's path. For a single key:

git config --show-origin --get user.email
  1. The new effective value would be bruno.salas@company.com. Deleting the key at the local level does not leave it empty: it simply lets the next level win, which is the global one. The system one would once again be masked by the global.

Solution to Exercise 2

~/.gitconfig:

[user]
	name = Carla Vidal
	email = carla@example.com

[core]
	editor = nano

[init]
	defaultBranch = main

[includeIf "gitdir:~/work/"]
	path = ~/.gitconfig-company

[includeIf "gitdir:~/clients/acme/"]
	path = ~/.gitconfig-acme

~/.gitconfig-company:

[user]
	email = carla.vidal@company.com

~/.gitconfig-acme:

[user]
	email = c.vidal@acme-contractor.example

Decisions worth justifying:

  • The personal e-mail goes in the main file as the default value, because any repository outside the two specific folders should end up with the personal one, which is the least compromising option if she forgets.
  • The name and the editor go once in the main file, since they do not change between contexts.
  • Every path ends in /, an indispensable condition for the match to be recursive.
  • The includeIf blocks go at the end of the file: they are processed in order, and what is included later overwrites what came before.

Checking from ~/clients/acme/sales-dashboard:

git config --get user.email
# → c.vidal@acme-contractor.example

# The diagnostic version, which also names the file responsible:
git config --show-origin --get user.email
# → file:/home/carla/.gitconfig-acme	c.vidal@acme-contractor.example

Solution to Exercise 3

  1. The two errors:

    • The key is misspelled: core.editorr with two r's. Git accepts it without validating and stores it as a meaningless key, so it has no effect at all.
    • The --global level is missing: without it, the value would have been written only into task-manager's .git/config, that is, for that one repository. Even with the key spelled correctly, the editor would still be Vim in all of Ana's other projects.
  2. The correct command:

git config --global core.editor "nano"
  1. Cleaning up the wrong key (it is at the local level, because that is how it was written):
cd ~/personal/task-manager
git config --local --unset core.editorr

# Check that nothing is left of the core section locally
git config --local --list
  1. The diagnostic command that would have revealed the problem:
git config --show-origin --get core.editor

It would have returned nothing, a sign that the correct key was not defined at any level. And with:

git config --list --show-origin | grep editor

she would have seen the line file:.git/config core.editorr=nano, where both the typo and the wrong level jump straight out at you.

Conclusion

You now have Git's configuration mechanism under control. Everything goes through git config, which reads and writes INI-format text files spread across three levels: system (the whole machine), global (your user) and local (one repository), joined by the worktree level and the -c option for a single run. When several levels define the same key, the most specific one wins, and the command git config --list --show-origin --show-scope is the definitive tool for finding out where each value comes from.

You have seen how to read (--get, --get-all, --list, --get-regexp), write (with and without --add), delete (--unset, --unset-all, --remove-section) and edit the files directly with --edit. And you have met includeIf, the piece that lets you keep separate profiles — personal, company, client — without having to remember anything in each new repository, thanks to conditions on the path (gitdir:), the branch (onbranch:) or the remote's URL (hasconfig:).

You know how to handle the controls; what remains is deciding which buttons to press. In the module's last lesson, Initial Configuration, we will apply all of this to Ana's laptop to get it ready: her identity, the default branch name, the editor, how line endings are handled, the behaviour when integrating changes, the credential store and the output colours. By the end, Ana will finally have everything in place to create the task-manager repository in module 2.

Mastering Git: From Beginner to Advanced

Module 1: Introduction to Git

Module 2: Basic Git Operations

Module 3: Branching and Merging

Module 4: Working with Remote Repositories

Module 5: Advanced Git Operations

Module 6: Git Tools and Techniques

Module 7: Collaboration and Workflow Strategies

Module 8: Git Best Practices and Tips

Module 9: Troubleshooting and Debugging

Module 10: Git in the Real World

© Copyright 2026. All rights reserved