Git ignore rules influence which untracked paths ordinary discovery and add operations consider. They are useful for generated files, editor state, and local configuration. They do not create an access-control boundary, remove a file already in the index, or erase earlier commits. Treat the ignore file as a project convention with observable matching rules, not as a guarantee that a sensitive file cannot enter history.

When it breaks

A rule added after a file was tracked does not stop tracking it. Changing that file and running an ordinary add can still stage the new content. This explains why adding a leaked configuration filename to .gitignore does not repair the leak. Index membership, current worktree contents, and historical blobs are distinct states that need separate inspection.

Negation is another common surprise. Ignoring an entire directory prevents Git from descending into it, so a later exception for one child cannot rescue that child. Ignore the directory’s contents while leaving its parent traversable, or explicitly restore every excluded parent before restoring a descendant. A nested ignore file cannot help if Git never visits that directory.

Precedence is not simply “the most specific-looking pattern wins.” Git defines priority between command-line patterns, directory ignore files, repository-local exclusions, and global exclusions. Within the same precedence level, the last matching pattern wins. A lower directory’s ignore file can override a parent’s rule for paths it governs. Pattern spelling and source location both matter.

Check it quickly

Run this Python script with Git available. It creates and removes a temporary repository, disables system and global Git configuration for its subprocesses, and uses invented fixture files. The recorded run used Git 2.50.1. No command operates on the repository from which you launch the script:

from pathlib import Path
from tempfile import TemporaryDirectory
import os,subprocess

env=dict(os.environ,GIT_CONFIG_NOSYSTEM='1',GIT_CONFIG_GLOBAL=os.devnull)
with TemporaryDirectory(prefix='git-ignore-lab-') as directory:
    root=Path(directory)
    def git(*args, expected=0):
        result=subprocess.run(['git',*args],cwd=root,env=env,text=True,capture_output=True)
        assert result.returncode==expected,(args,result.returncode,result.stderr)
        return result.stdout
    def write(path,content='fixture\n'):
        target=root/path;target.parent.mkdir(parents=True,exist_ok=True);target.write_text(content)
    git('init','-q')
    git('config','user.name','Ignore Lab')
    git('config','user.email','ignore@example.invalid')
    git('config','commit.gpgsign','false')
    write('tracked.txt','first version\n')
    git('add','--','tracked.txt');git('commit','-qm','Track fixture')
    write('build/keep.txt');write('build/output.bin')
    write('.gitignore','/build/\n!/build/keep.txt\n')
    git('check-ignore','-q','build/keep.txt')
    print('excluded parent prevents child exception')
    write('.gitignore','/build/*\n!/build/keep.txt\n*.log\n/root-only.txt\ntracked.txt\n')
    git('check-ignore','-q','build/keep.txt',expected=1)
    rule=git('check-ignore','-v','build/keep.txt')
    assert ':2:!/build/keep.txt\t' in rule
    git('check-ignore','-q','build/output.bin')
    print('ignoring contents permits keep.txt; verbose output includes its negation')
    write('debug.log');write('nested/debug.log');write('nested/.gitignore','!debug.log\n')
    write('root-only.txt');write('nested/root-only.txt')
    git('check-ignore','-q','debug.log')
    git('check-ignore','-q','nested/debug.log',expected=1)
    git('check-ignore','-q','root-only.txt')
    git('check-ignore','-q','nested/root-only.txt',expected=1)
    print('nested exception and root anchor checked')
    write('tracked.txt','second version\n')
    git('add','-A')
    assert 'tracked.txt' in git('diff','--cached','--name-only').splitlines()
    git('check-ignore','-q','tracked.txt',expected=1)
    assert 'tracked.txt' in git('check-ignore','--no-index','-v','tracked.txt')
    git('rm','--cached','--','tracked.txt')
    assert (root/'tracked.txt').is_file()
    assert 'tracked.txt' not in git('ls-files').splitlines()
    git('cat-file','-e','HEAD:tracked.txt')
    print('tracked file still staged; cached removal keeps disk copy and old commit')
    git('add','-f','--','build/output.bin')
    assert 'build/output.bin' in git('ls-files').splitlines()
    print('force-add bypasses ignore rules')

The first check confirms that excluding build/ defeats the child exception. The revised rules exclude entries inside build instead, allowing keep.txt while still excluding output.bin. The quiet checks assert actual ignored status; exit one means the tested path is not ignored.

Verbose output for the allowed file still includes the matching negated rule. Therefore, seeing output from git check-ignore -v is not by itself proof that a path is excluded. Inspect the reported pattern, including its exclamation mark, and use the quiet result when you need a boolean decision. This distinction matters in scripts that turn diagnostic output into an automated gate.

The nested log exception overrides the root log rule within nested. The leading slash on /root-only.txt anchors it to the root ignore file’s directory, so the nested file remains eligible. These checks use actual created paths rather than guessing from a pattern’s visual shape.

The tracked file remains staged after its modification even though a new ignore pattern matches its name. --no-index asks the diagnostic command to consider that pattern without ordinary tracked-file exclusion. Removing the path with git rm --cached leaves its working copy on disk and removes it from the index, while the original committed blob remains accessible. Finally, force-add places an ignored build output into the index.

Review what the next commit will contain

Use shared .gitignore files for project-wide conventions, .git/info/exclude for exclusions local to one repository checkout, and a configured global excludes file for personal editor clutter. Do not make a team’s correct build depend on an undocumented global rule. Keep essential source inputs eligible even when neighboring generated output is excluded.

Before committing, inspect the staged diff and tracked paths, not just a clean-looking default status display. A broad pattern can hide a new source file; a forced addition can override an intended exclusion. Commit a change that stops tracking a local configuration file only with a replacement provisioning path, because other checkouts applying that deletion may remove their tracked copy.

For an actual credential exposure, revoke or replace the credential and investigate its distribution. Removing an index entry does not invalidate access already granted by that credential, and rewriting one repository cannot control existing copies elsewhere. Ignore rules prevent many routine mistakes, but staged-content review and appropriate secret detection provide additional checks with their own limits.

Sources

  • Git ignore rules, scope, precedence, and excluded-parent behavior, checked 2026-09-12.
  • git check-ignore, verbose negated patterns and index-aware diagnostics, checked 2026-09-12.
  • git rm, removing index entries while retaining working files, checked 2026-09-12.