Confirmed against the official man pages (gitignore(5), git-config(1), gitrepository-layout(5)) and cross-checked against your actual machine — both files exist here and are demonstrably active.
$HOME/.config/git/ignore — your per-user, all-repositories ignore list. It isn't hardcoded; it's the default value of the config variable core.excludesFile. Verbatim from git-config(1) (your local 2.54.0 man page):
core.excludesFile Specifies the pathname to the file that contains patterns to describe paths that are not meant to be tracked, in addition to
.gitignore(per-directory) and.git/info/exclude. Defaults to$XDG_CONFIG_HOME/git/ignore. If$XDG_CONFIG_HOMEis either not set or empty,$HOME/.config/git/ignoreis used instead. See gitignore(5).
So the path you asked about is the XDG fallback: XDG_CONFIG_HOME is unset on most Linux desktops, so $XDG_CONFIG_HOME resolves to $HOME/.config, hence $HOME/.config/git/ignore. Same fallback rule the docs state for config itself:
$XDG_CONFIG_HOME/git/config,~/.gitconfig— User-specific configuration files. When the XDG_CONFIG_HOME environment variable is not set or empty,$HOME/.config/is used as$XDG_CONFIG_HOME. These are also called "global" configuration files.
/path/to/repo/.git/info/exclude — the per-repository, non-shared ignore list. From gitrepository-layout(5):
info/exclude — This file, by convention among Porcelains, stores the exclude pattern list.
.gitignoreis the per-directory ignore file. git status, git add, git rm and git clean look at it but the core Git commands do not look at it.
Because it lives under .git/, it is never committed and never travels through clone/push — that's the whole point of it.
gitignore(5) gives an explicit three-way split, which is the passage you want to cite:
Which file to place a pattern in depends on how the pattern is meant to be used.
- Patterns which should be version-controlled and distributed to other repositories via clone (i.e., files that all developers will want to ignore) should go into a
.gitignorefile.- Patterns which are specific to a particular repository but which do not need to be shared with other related repositories (e.g., auxiliary files that live inside the repository but are specific to one user's workflow) should go into the
$GIT_COMMON_DIR/info/excludefile.- Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files generated by the user's editor of choice) generally go into a file specified by
core.excludesFilein the user's~/.gitconfig.
And the precedence order, from the same DESCRIPTION section (highest → lowest):
- Patterns read from the command line…
- Patterns read from a
.gitignorefile in the same directory as the path, or in any parent directory…- Patterns read from
$GIT_COMMON_DIR/info/exclude.- Patterns read from the file specified by the configuration variable
core.excludesFile.
Note the modern docs say $GIT_COMMON_DIR, not $GIT_DIR — that matters for git worktree: linked worktrees share the main repo's info/exclude, they don't each get their own. The version-agnostic way to locate it is git rev-parse --git-path info/exclude (here: .git/info/exclude).
★ Insight ─────────────────────────────────────
- The precedence is inverted from what people assume: your personal global list is the weakest source, so a project's committed
.gitignore(including a negation like!.vscode/settings.json) always wins over your~/.config/git/ignore. That's deliberate — a project's shared policy shouldn't be silently overridable by one developer's dotfiles. - Neither file can un-ignore a tracked file. Ignore rules only apply to untracked paths; once something is in the index you need
git rm --cached(orupdate-index --skip-worktree) — a common source of "my ignore rule doesn't work". .git/info/excludeis a cheap, unreviewed escape hatch, which is exactly why it should never hold rules your teammates also need: nothing about it is discoverable by anyone else, so shared build artifacts belong in the committed.gitignoreinstead.─────────────────────────────────────────────────
Both files are in use in this repo, and git check-ignore -v names the winning source explicitly:
$ git check-ignore -v --no-index .claude/settings.local.json .claude/mailbox/x .vscode/settings.json
/home/user/.config/git/ignore:1:**/.claude/settings.local.json .claude/settings.local.json
.git/info/exclude:13:**/.claude/mailbox/ .claude/mailbox/x
.gitignore:640:!.vscode/settings.json .vscode/settings.json
Contents today: ~/.config/git/ignore holds one line (**/.claude/settings.local.json) — a genuinely machine-wide preference; .git/info/exclude holds ~10 Claude Code runtime paths (scheduled_tasks.lock, mailbox/, worktrees/, first-run, …) — repo-local tool droppings, correctly kept out of the committed .gitignore. core.excludesFile is not set (--get exits 1), which confirms the XDG default is what's doing the work rather than an explicit override.
That third line also illustrates the precedence rule in practice: .vscode/settings.json is un-ignored by a committed negation at .gitignore:640, and no user-level rule could override that.
Sources:
- gitignore(5) — git-scm.com/docs/gitignore
- git-config(1) — git-scm.com/docs/git-config (
core.excludesFile, FILES section) - gitrepository-layout(5) — git-scm.com/docs/gitrepository-layout (
info/exclude) - git-check-ignore(1) — git-scm.com/docs/git-check-ignore