Git

What Is Gitignore? Understand It in 5 Minutes

What Is Gitignore? Understand It in 5 Minutes

Every project folder collects files Git has no business storing. Compiled output, a node\_modules folder with thousands of entries, last Tuesday’s log file, an .env file with live credentials. A .gitignore file tells Git to look the other way. It’s a plain text file with one pattern per line, and it keeps matching untracked files out of git status, staging, and commits.

Git checks four places for ignore patterns. The Git manual ranks them like this: command-line patterns, .gitignore files, .git/info/exclude, and the file named by core.excludesFile.

Only the repository’s .gitignore gets copied when somebody clones the project, so it ends up being the shared layer for the whole team.

What is a .gitignore file?

YouTube player

It lives in the working tree of a Git repository, usually at the root, and gets committed like any source file. Everyone who clones the project receives the same rules.

The Git manual calls it a list of intentionally untracked files. Nothing in it deletes, moves, or hides anything on disk.

It won’t remove files from disk or from the repository. It won’t stop Git from tracking a file that’s already committed either, because that takes git rm –cached. And anybody can still force-add an ignored file if they really want to.

An ignored file is just an untracked file that matches a pattern, so it stops showing up in git status. It still exists and works normally.

A tidy .gitignore also keeps the history of any version control setup readable. No logs, no compiled output, no dependency folders.

How does Git decide which files to ignore?

Before Git lists a path in git status or stages it with git add, it checks every untracked path against the ignore patterns. A match hides the path. Tracked files skip the check entirely, so a new rule never touches them.

Run git status right after adding a rule and the matching files drop out of the untracked list. If a tracked file matches the same pattern, nothing changes for it. Git keeps tracking it and its edits keep showing up.

Why is GitHub the heart of open source?

Uncover GitHub statistics: developer community growth, repository trends, collaboration patterns, and the platform that powers modern software development.

Explore GitHub Data →

The Git manual states it plainly: files already tracked are not affected.

That’s why a rule added today does nothing for a file committed last week. Plenty of developers lose an afternoon to this before they read the manual.

Directories follow a related rule. Git does not list excluded directories for performance reasons, so patterns for files inside an ignored directory have no effect, wherever they are defined (Git manual, gitignore page).

Ignored paths also stay out of the staging area when git add runs on a whole folder.

Patterns can come from several sources, and inside a single source the last matching pattern decides the outcome.

Looking to sharpen your Git skills? Branching, merging, rebasing, and everything else you need - including git rebase, git stash, and commit workflows - is on one page in the Git Cheat Sheet.

How to create a .gitignore file

YouTube player

Make a plain text file named .gitignore in the repository root, add one pattern per line, and commit it. Do it before the first commit so build output and dependencies never get into history.

A brand new project needs git init first. After that the steps are the same on every operating system.

  1. Create a file named .gitignore in the root folder, from a code editor or with touch .gitignore in a terminal.
  2. Add one pattern per line. Use # comment lines to explain rules that are not obvious.
  1. Run git status and confirm the ignored files no longer appear as untracked.
  2. Commit the file so collaborators receive the same rules.

Subdirectory .gitignore files work too. Their patterns match relative to their own folder.

Add the file before your first commit. A file committed earlier stays tracked, and a later rule can’t hide it.

These files grow along with the project. The top-level .gitignore of the Linux kernel lists archive and compressed-output patterns such as \.lz4, \.lzma, \.lzo, \.xz, and \*.zst, and tells contributors to put rules for files generated in a specific subdirectory in that subdirectory’s own .gitignore.

▶ GIT EXPLAINED

If you commit, branch, or merge code every day, it helps to know how Git came to be. This video covers how Linus Torvalds created Git in just 10 days after a license dispute and explains how its data model actually works.

Linus Torvalds Built Git in Days Out of Spite
Built in 10 Days Out of Spite. Git and its data model explained in plain language.

Using a template or generator

YouTube player

Writing every rule by hand is rarely worth it. The github/gitignore repository has language and framework templates at the root, a Global folder for editors, tools, and operating systems, and a community folder for more specialized stacks.

gitignore.io is a generator that combines rules for whatever languages and tools you name. GitHub’s REST API can also apply a .gitignore template when a repository is created (GitHub Docs).

Read the result before you commit it. A template should match your stack, not only your language.

Pattern syntax: what each rule matches

YouTube player

Each line is one pattern. A blank line separates, # starts a comment, ! negates, and glob characters match names. When patterns in the same file overlap, the last match wins (Git manual).

PatternWhat it matchesExample hit
\*.logAny file ending in .log, at any depthlogs/app.log
/buildOnly the build entry at the .gitignore levelbuild, not src/build
build/Directories named build, at any levelsrc/build/
docs/\*.htmlHTML files directly inside docsdocs/a.html, not docs/x/a.html
a/\\/bb under a, with zero or more folders betweena/b, a/x/y/b

Slashes decide scope. A slash at the beginning or middle anchors the pattern to the .gitignore location. A slash at the end limits it to directories. With no slash at all, the pattern can match at any level below.

A single asterisk never crosses a slash. The pattern foo/\* matches foo/test.json and the directory foo/bar, but not foo/bar/hello.c.

A backslash before a leading # or ! makes it literal. Trailing spaces are ignored unless you quote them with a backslash, and a backslash at the very end of a pattern is invalid and never matches.

Negation has one hard limit. A file can’t be re-included if a parent directory is excluded.

The Git manual’s recipe for keeping only foo/bar lists /\* first, then !/foo, then /foo/\*, then !/foo/bar. Every level has to be re-included before its contents.

What should go in a .gitignore file?

YouTube player

Dependency folders, generated output, secrets and local config, and the clutter editors and operating systems leave behind. What they share is that Git can rebuild them or should never see them.

Dependency folders are the biggest single win. node\_modules (created by npm install) and Python virtual environments are large, one command rebuilds them, and a lockfile already records the exact versions.

Generated output covers build and dist folders, \_\pycache\\_, compiled files, and \*.log. Secrets mostly means .env files read by dotenv-style loaders. Ignore the real .env file and commit a template if the team needs one.

Editor and OS files are the debatable group. That’s .DS\_Store on macOS, Thumbs.db on Windows, plus .vscode from Visual Studio Code and .idea from JetBrains IDEs. Some teams commit shared settings from those folders on purpose, so ignore only what’s personal.

Ignore what the build produces. Commit what the build consumes.

.gitignore, .git/info/exclude, or a global ignore file?

YouTube player

Use .gitignore for patterns every contributor needs, .git/info/exclude for patterns that matter in one clone only, and the global file for patterns you want in every repository on your machine. The Git manual draws the same lines.

SourceLocationTravels with clonesTypical content
.gitignoreRepository, any folderYesBuild output, dependency folders
.git/info/excludeInside .git, one cloneNoPersonal scratch files in this repository
Global ignore filecore.excludesFile, default ~/.config/git/ignoreNoEditor backups, OS files

Set the global file with a git config command: git config –global core.excludesFile ~/.gitignore\_global.

If the variable is unset, Git reads $XDG\_CONFIG\_HOME/git/ignore, or $HOME/.config/git/ignore when that variable is empty (Git manual).

When sources disagree, the order from highest to lowest precedence goes like this.

  1. Patterns given on the command line, for commands that accept them
  2. .gitignore files, where a file in a deeper folder overrides one above it
  3. .git/info/exclude
  4. The file named by core.excludesFile

The higher source wins. A negated pattern in the repository can therefore re-include a file that your global rules exclude.

Patterns in the exclude file and the global file are read as if they sat at the repository root, so a leading slash anchors to the root of the repository.

Putting editor clutter in the global file keeps the shared .gitignore short. It also stops every project from carrying your tooling choices.

Why is .gitignore not working, and how to fix it

YouTube player

Usually the file is already tracked, the pattern doesn’t match the path as written, or a parent directory is excluded. git check-ignore -v shows which rule applies to a path.

Find which rule is ignoring a file

With -v, git check-ignore prints one line per path: the source file, the line number, the pattern, then the path.

No output means no pattern matches the path. A pattern starting with ! means the path matched a negated pattern, so it isn’t excluded. The exit status is 0 when a path is ignored, 1 when none is, and 128 on a fatal error.

By default the command doesn’t report tracked files at all, because exclude rules don’t apply to them. Add –no-index to debug why git add . picked up a path you expected to be ignored (git-check-ignore manual).

One quiet cause worth ruling out is a .gitignore that’s a symbolic link. Git doesn’t follow symlinks when it reads the file from the working tree.

Stop tracking a file that is already committed

YouTube player

The Git manual’s fix is git rm –cached, which removes a file from the index only. GitHub Docs gives the same order: untrack first, then add the rule.

  1. Run git rm –cached path/to/file (add -r for a folder). The file leaves the index and stays on disk.
  2. Add the matching pattern to .gitignore.
  3. Commit the removal and the new rule together.
  4. Push, and warn collaborators who rely on a local copy, because pulling that commit removes the file from their working tree.

Adding an ignored file on purpose

The git add manual says ignored files are skipped by default. Name an ignored file exactly and git add fails with a list of ignored files, while a directory or glob skips it silently.

The -f flag on git add overrides the rule, and from then on the file is tracked. git check-ignore stops reporting it unless –no-index is set.

If a file you force-added keeps showing changes, the rule didn’t fail. The file is simply tracked.

When .gitignore does not protect you

A .gitignore file only keeps untracked files out of future commits. It does nothing for a secret already in history, for a clone that already holds the data, or for a push that nothing checks.

GitHub counted more than 39 million secrets leaked across its platform in 2024 (GitHub Blog, 2025).

A committed secret stays in history

Adding the file to .gitignore afterward leaves every earlier commit untouched. Anyone who cloned the repository, and any fork, keeps the secret.

Rotate it first. GitHub Docs lists revoking or rotating the secret as the first step and says that alone may settle the problem, so a history rewrite isn’t always warranted.

What rewriting history costs

git filter-repo is the tool GitHub’s guide documents for the rewrite, and its –sensitive-data-removal flag needs version 2.47 or later. BFG Repo-Cleaner is a separate community tool that does the same kind of job, though GitHub’s current guide doesn’t cover it.

The commit that introduced the data and every commit after it get new hashes. A teammate with an old clone who runs git pull and then git push brings the data right back. Force pushes are blocked by branch protection, so the protection has to be switched off temporarily.

There’s collateral damage too. git filter-repo strips commit and tag signatures, and diffs of closed pull requests stop rendering. Forks, other clones, and cached views on GitHub still expose the commits (GitHub Support clears cached views only when rotating credentials can’t contain the risk).

Layers that block a leak

A .gitignore file is advice to Git, not enforcement.

Pre-commit hooks built on git-secrets or gitleaks help, as long as every collaborator installs them. Push protection goes further and blocks a push that contains a detected secret before it reaches the repository. The cleanest option is keeping secrets out of code entirely, in environment variables or a manager such as HashiCorp Vault.

The habits behind keeping an API key out of a GitHub repository start before any tool does. Write the ignore rule before the first commit, and never paste a key into source code.

.gitignore File FAQ

Can a repository have more than one .gitignore file?

Yes. Any folder can hold one, and each applies to its own subtree.

Atlassian’s Git tutorial still calls a single root file the simplest approach, because patterns spread across files make it hard to trace why a path is ignored.

How do I keep an empty folder in a repository?

Git tracks files, not folders, so an empty folder can’t be committed. Add a placeholder file such as .gitkeep (a naming convention, not a Git feature).

To keep a folder while ignoring its contents, write logs/\* followed by !logs/.gitkeep.

What is the difference between .gitignore and .gitattributes?

.gitignore decides which untracked files Git leaves out. A .gitattributes file assigns settings to paths, such as line-ending conversion, diff behavior, and merge drivers.

Both use the same pattern rules, except that attribute files forbid negative patterns (Git manual).

Does .gitignore work the same on Windows, macOS, and Linux?

The syntax is identical, since the manual defines the forward slash as the directory separator everywhere.

The file system is what differs. Windows and macOS fold case by default, so two names that differ only in case can collide in one working tree (Git FAQ).

Can I put a comment on the same line as a pattern?

No. The Git manual treats only a line that starts with # as a comment.

Trailing text becomes part of the pattern, so the rule stops matching the file you meant. Put the comment on its own line above the rule.

Can I ignore changes to a file Git already tracks?

Not reliably. The Git FAQ says Git offers no way to do it, and the assume-unchanged and skip-worktree bits of git update-index don’t work properly for this purpose.

Commit a template file with defaults instead, and ignore the local copy that people edit.

Cleaning Up a Repository That Has No .gitignore File

Rotate any committed credentials first, untrack the clutter second, and add the matching ignore rules in the same commit. Credential exposure is the only item with a deadline.

Rewrite history only for data that can’t be rotated.

The Git manual’s order is to untrack with git rm –cached and then add the filename to .gitignore, which keeps the file from being reintroduced in later commits. Skip the rule and the next git add . puts the files straight back.

The trade-off is that untracking leaves earlier versions of those files reachable in history. That’s acceptable for build output and not for personal data, which can’t be revoked like a token.

This order holds as of the Git 2.56 manuals (September 2026). A git cheat sheet keeps the commands within reach while you work through it.

Bogdan Sandu

Stay sharp. Ship better code.

Every week: one curated article, one tool worth knowing, one tip you can use tomorrow. No noise, no padding.