What Is .gitignore?

.gitignore is a plain-text rule file that defines which files and directories in a Git repository must never be tracked. It is placed in the project root, and Git automatically excludes everything matching the patterns in that file both from git status output and from the scope of commands such as git add .. Without a .gitignore, a repository fills up with dependency folders, build output and personal editor settings as the project grows; this inflates the repository size unnecessarily and causes meaningless merge conflicts between team members.

Three kinds of content appear in almost every .gitignore file. The first is folders like node_modules/ that can be reinstalled from a dependency list such as package.json; committing them grows the repository for no reason, when they can simply be recreated with npm install. The second is build output: directories like dist/, build/ and target/ are generated from the source code and do not need to be kept in the repository alongside the source itself. The third is junk produced by the operating system or the editor; .DS_Store on macOS and Thumbs.db on Windows are the typical examples. On top of these, files that contain API keys and passwords, such as .env, must always be added to .gitignore, because once such secrets are pushed to a remote repository they remain in the history even if they are deleted later.

  • Dependency folders: node_modules/, vendor/ — reinstallable with a package manager.
  • Build output: dist/, build/, target/ — regenerated from the source code.
  • Secret files: .env, .env.local — may contain API keys and passwords.
  • Operating system/editor files: .DS_Store, Thumbs.db, .vscode/, .idea/.
  • Log and cache files: *.log, .eslintcache, __pycache__/.

How Does the Pattern Syntax Work?

Git's .gitignore pattern matching consists of a small number of rules, but each of them behaves differently. The * character acts as a wildcard and matches any sequence of characters within a single directory level; lines beginning with # are comments and are not evaluated as patterns by Git, they only serve to keep the file readable.

PatternMeaning
<code>*.log</code>Ignores every file whose name ends in .log, in whichever directory it sits.
<code>build/</code>The trailing <code>/</code> targets only the directory named <code>build</code>; if there is a file called <code>build</code>, it is unaffected.
<code>!.env.example</code>The leading <code>!</code> makes an exception for a file ignored by an earlier rule and brings it back into tracking.
<code>**/logs</code>A double asterisk (<code>**</code>) matches directories at any depth; the <code>logs</code> folder is ignored whether it sits in the project root or in a subdirectory.
<code># Dependencies</code>A comment line; it affects no file and only divides the file into sections.

.gitignore is processed line by line from top to bottom, so the order of the patterns matters. There is also one limitation worth knowing: if an entire directory has been ignored by a rule, you cannot re-include individual files inside that directory with ! — Git never looks inside a directory it already ignores. If you want to define an exception, you have to write patterns that target specific file names rather than the parent directory.

The Critical Trap: .gitignore Does Not Remove Files That Are Already Tracked

The most common misconception is this: if a file was already committed to the repository with git add and git commit before it was added to .gitignore, adding the rule afterwards does not on its own stop the file from being tracked. Once Git has started tracking a file, it ignores any matching patterns in .gitignore for that file; the rule applies only to files that have never been tracked (untracked).

The classic example of this is the node_modules/ folder being accidentally included in the first commit. Even if node_modules/ is added to the .gitignore file later, and even though git status shows a clean working directory, the files stay in the repository and continue to be sent with every git push. To fix this, the file has to be removed from Git's tracking index separately.

After adding a .gitignore file like the one above, if node_modules was committed earlier you need to run the following commands:

The -r flag applies the operation recursively to every file inside the directory, while the --cached flag removes the files only from Git's tracking index; it does not touch the actual files on disk. The node_modules folder therefore keeps working on your machine, it simply stops being tracked by the repository from this commit onward. The file is not deleted from past commits entirely; cleaning the repository history from the start is a separate topic and requires extra tools such as git filter-repo.

Common Mistakes

  • Adding the .gitignore file not at the very start of the project but after node_modules or build output has already been committed; if the git rm --cached step above is skipped in that case, the files stay tracked.
  • Writing overly broad patterns: for example, carelessly using a general pattern instead of a specific log file can accidentally ignore a file the project needs or a required folder as well.
  • Not using a template specific to the language or framework: applying a Node.js template to a Python project (or the other way around) both adds unnecessary rules and leads to forgetting the patterns that actually matter, such as __pycache__/ or .venv/.
  • Forgetting to commit the .gitignore file itself; if the rule file is not added to the repository, your teammates do not benefit from the same protection.
  • Trying to re-include single files inside an already ignored directory with !; because of how Git behaves this does not work, since it never looks inside an ignored directory.

Generate the Right Template Instead of Writing It by Hand

Rather than memorising the right combination of patterns for every project, using a tool that automatically merges the standard patterns for your language, framework and operating system both saves time and reduces the risk of a missing or wrong pattern. A real project usually needs several templates stacked on top of each other; a macOS user developing a Node.js project, for example, needs Node, macOS and editor-specific patterns all at once.