Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save eevmanu/cfde1642c5b69133bae44c43df9435c4 to your computer and use it in GitHub Desktop.

Select an option

Save eevmanu/cfde1642c5b69133bae44c43df9435c4 to your computer and use it in GitHub Desktop.
understanding different layers of ignore / exclude on git

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.

What each path means

$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_HOME is either not set or empty, $HOME/.config/git/ignore is 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. .gitignore is 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.

Why they're useful — the docs' own decision rule

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 .gitignore file.
  • 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/exclude file.
  • 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.excludesFile in 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 .gitignore file 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 (or update-index --skip-worktree) — a common source of "my ignore rule doesn't work".
  • .git/info/exclude is 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 .gitignore instead. ─────────────────────────────────────────────────

Verified live on this machine

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:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment