Local Ignore Rules in Git: .git/info/exclude and core.excludesFile

Intro

I was working on a big project together with a team. Not everyone on the project used Claude. While pairing, I kept dropping things into the project root. I would scan a topic and get docs as .md output, throw in temporary json files, add images. None of these had anything directly to do with the project.

There were two things I did not want. First, adding these files to .gitignore, because .gitignore gets committed and goes to everyone as an unrelated change. Second, having them show up in the file changes; I did not want to pollute the diff with my own local work.

So I needed an ignore solution that touched the repo not at all and only applied on my own machine. I looked, and I found it: .git/info/exclude.

Three places, three purposes

In Git there are three places you can write an ignore rule, and each serves a different purpose:

  • .gitignore: rules shared with the repo, committed, binding on everyone.
  • .git/info/exclude: only for your machine and your clone. Not committed, pushed, or cloned; nobody on the team sees it.
  • core.excludesFile: a file you point to via git config. Can be global (for all your repos, per-user) or local to a single repo.

What I needed was the second one, but knowing all three is useful.

.git/info/exclude, personal and never enters the repo

This was exactly what I was looking for. The rules stay only with me and touch the repo not at all. The file already exists in every repo, you do not need to create anything extra, just appending lines is enough:

echo "*.local" >> .git/info/exclude
echo "scratch/" >> .git/info/exclude

I tested this locally. After writing the *.local rule, the untracked note.local file was ignored:

git check-ignore -v note.local
# .git/info/exclude:1:*.local	note.local

The best part is that this file is not cloned and not pushed. So I kept the docs and json files I dropped into the project root here, and they did not show up in anyone's diff.

Important pitfall: a file that is already tracked

This is very commonly misunderstood, so I am writing it separately. If a file is already committed (tracked), adding it to exclude changes nothing. Git does not apply ignore rules to tracked files.

I tested it: I added a tracked file to exclude, and git check-ignore -v returned nothing. Because a tracked file is not considered "ignored".

The fix is to first drop the file from the index. The file in your working directory is not deleted, only git's tracking ends:

git rm --cached tracked.txt
# the file stays on disk, git just no longer tracks it
# after this the .gitignore or exclude rule kicks in

core.excludesFile, a global personal ignore

Then there are things you do not want to write over and over in every repo. Like .DS_Store, .idea/, *.swp. Instead of writing these separately in every project, you can define them centrally once:

git config --global core.excludesFile ~/.gitignore_global
echo "*.conf" >> ~/.gitignore_global

I tried it locally, and the *.conf rule ignored app.conf:

git check-ignore -v app.conf
# ~/.gitignore_global:1:*.conf	app.conf

Global is not required; without --global, you can also use it for a single repo with git config core.excludesFile <path>.

Precedence order

Now we get to the part that really causes confusion. If there is a rule in more than one place for the same file, who wins? I tested it locally; from highest to lowest the order is:

  1. Command-line patterns (like git ls-files)
  2. .gitignore files (a rule in a deeper directory overrides the one above, and within the same file the last matching rule wins)
  3. .git/info/exclude
  4. core.excludesFile

I confirmed this with two negation tests. When I wrote !note.local into .gitignore, it overrode the *.local rule in exclude and the file was not ignored. So .gitignore has priority over exclude. Then when I wrote !build/ into exclude, it overrode the build/ rule in core.excludesFile. So exclude also has priority over core.excludesFile.

In short, the shared rule (.gitignore) always overrides your personal rules. That is worth keeping in mind.

Debugging: why is a file ignored?

When you wonder why a file is ignored (or not ignored), a single command is enough:

git check-ignore -v <file>

The -v flag shows which file, which line, and which rule matched:

git check-ignore -v note.local
# .git/info/exclude:1:*.local	note.local

If no output comes, there are two possibilities: either the file is not ignored, or it is already tracked (tracked files are not reported by this command).

From the team's perspective: exclude or a shared rule?

In my case the answer was clear. The files I dropped were entirely personal, the team did not care about them, so .git/info/exclude was the right choice. I kept my own mess on my own machine without touching the repo.

I have a simple distinction in mind:

  • Things specific only to you (IDE settings, personal scratch, pairing leftovers): .git/info/exclude or global core.excludesFile. Does not pollute the repo.
  • Things that should be excluded by the whole team (node_modules/, build output): a committed .gitignore. Because exclude is not shared, and everyone needs to see the same behavior.

Conclusion

In short, .gitignore is not the only place. For personal work there are .git/info/exclude and core.excludesFile, and these touch the repo not at all. The next time you drop a temporary file into the project root and think "how do I hide this", this is the solution.

Share