Back to Blog
Best Practicesgitgitignoredeveloper

Git Ignore Patterns: Keeping Your Repository Clean and Secure

Master .gitignore syntax and patterns. Learn common ignore rules for every project type, prevent credential leaks, and manage ignore files in monorepos.

Loopaloo TeamFebruary 8, 202613 min read

Git Ignore Patterns: Keeping Your Repository Clean and Secure

Every developer who has used Git has encountered the .gitignore file, yet surprisingly few understand the full depth of Git's ignore mechanism. On the surface it seems simple — list the files you don't want tracked, and Git leaves them alone. But beneath that simplicity lies a pattern-matching system with nuanced precedence rules, subtle gotchas around already-tracked files, and genuine security implications when things go wrong. Misconfigured ignore rules have led to leaked credentials, bloated repositories, and broken builds. This guide walks through the mechanics, syntax, best practices, and security considerations of Git's ignore system so you can keep your repositories clean, lean, and safe.

How Git's Ignore Mechanism Works

To understand .gitignore, you first need to understand how Git thinks about files. Git categorizes every file in your working directory into one of three states: tracked (the file is in the index and Git is actively managing it), untracked (the file exists in the working directory but Git doesn't know about it), and ignored (the file matches a pattern that tells Git to pretend it doesn't exist). The critical detail here is that .gitignore only affects untracked files. If a file has already been added to Git's index — whether intentionally or by accident — adding it to .gitignore afterward will have absolutely no effect. Git will continue tracking that file and its changes until you explicitly remove it from the index. This is one of the most common sources of confusion and is directly responsible for many accidental credential leaks.

When you run git status or git add, Git evaluates each untracked file against the ignore rules to determine whether it should be shown or included. Ignored files won't appear in git status output, won't be staged by git add ., and won't be included in commits unless you explicitly force them with the -f flag.

Pattern Syntax

Git's ignore pattern syntax is more expressive than most developers realize. A simple filename like debug.log matches any file named debug.log in any directory of the repository. Adding a forward slash at the beginning anchors the pattern to the directory where the .gitignore file lives, so /debug.log only matches debug.log in the root directory, not in subdirectories. A trailing slash restricts the pattern to directories only — logs/ matches the directory named logs and everything inside it, but would not match a file named logs.

The asterisk wildcard matches everything within a single directory level. The pattern *.log matches all files ending in .log in any directory. The double asterisk ** is more powerful: it matches across directory boundaries. The pattern **/logs matches a directory called logs anywhere in the repository tree, while logs/** matches everything inside the logs directory regardless of nesting depth. The pattern **/logs/** combines both, matching any path that contains a logs directory segment.

The question mark matches any single character, and square brackets define character classes — [0-9] matches any digit, [abc] matches a, b, or c. Negation is achieved with the exclamation mark: if you've ignored all .log files with *.log but want to keep important.log, you add !important.log on a subsequent line. Negation patterns must come after the pattern they're overriding, and there's an important limitation — you cannot negate a file inside an ignored directory. If you ignore build/, writing !build/output.js will not work because Git never looks inside ignored directories.

Where .gitignore Files Live

Git supports .gitignore files at multiple levels, each with different scope and precedence. The most common location is the repository root, where a single .gitignore file covers the entire project. But you can also place .gitignore files in subdirectories, where their rules apply only to files within that directory and its children. This is particularly useful in monorepos where different subdirectories have different technology stacks and therefore different ignore needs.

Beyond repository-level files, Git supports a global .gitignore that applies to all repositories on your machine. You configure this with git config --global core.excludesfile ~/.gitignore_global. The global file is the correct place for patterns specific to your personal development environment — your IDE's configuration directories, operating system artifacts, and editor swap files. Patterns like .idea/, .vscode/, *.swp, and .DS_Store belong in your global gitignore rather than in project-level files, because they reflect your tools, not the project's requirements.

There's also a per-repository exclude file at .git/info/exclude that works identically to .gitignore but is not committed to the repository. This is useful for personal ignores that you don't want to impose on other contributors.

The Precedence Hierarchy

When multiple ignore sources exist, Git evaluates them in a specific order, with later sources taking higher precedence. Patterns from the command line (via git add -f or --no-ignore) override everything. Next come patterns from .gitignore files in the same directory or parent directories, with closer directories taking precedence over more distant ones. Then the .git/info/exclude file, and finally the global gitignore file. Within a single file, later lines override earlier lines when they conflict.

This hierarchy means that a .gitignore in a subdirectory can override patterns set by the root .gitignore. If the root ignores *.log but a subdirectory's .gitignore contains !important.log, the file subdirectory/important.log will not be ignored. Understanding this precedence is essential for managing complex projects where different components have different needs.

Common Patterns by Project Type

Different technology stacks produce different artifacts that should be ignored. Node.js projects generate the notoriously large node_modules/ directory, which should never be committed. The package-lock.json or yarn.lock file, however, should be committed — it ensures reproducible dependency resolution across environments. Build outputs in dist/, .next/, or build/ directories are typically ignored since they can be regenerated from source.

Python projects accumulate __pycache__/ directories containing bytecode files, .pyc files scattered through the source tree, virtual environment directories like .venv/ or env/, and distribution artifacts in *.egg-info/ and dist/. Java and Kotlin projects using Maven or Gradle should ignore the target/ or build/ directories respectively, along with compiled .class files.

Operating system artifacts are a universal concern. macOS creates .DS_Store files in every directory you browse in Finder, Windows generates Thumbs.db and desktop.ini files, and Linux environments may produce *~ backup files from editors. IDE-specific directories like .idea/ (JetBrains), .vscode/ (Visual Studio Code settings — though some shared settings like recommended extensions are worth committing), and .eclipse/ should typically be ignored.

Rather than assembling these patterns manually, the Gitignore Generator can produce comprehensive, project-specific .gitignore files based on your technology stack, saving time and reducing the risk of missing critical patterns.

Security Implications

The security consequences of a misconfigured .gitignore can be severe and long-lasting. Environment files (.env) commonly contain database credentials, API keys, and other secrets. If committed even once, these secrets become permanently embedded in the Git history — removing the file in a later commit does not expunge it from the repository's object store. Anyone who clones the repository or accesses its history can extract those credentials.

This is not a theoretical concern. There have been numerous high-profile incidents where API keys, database passwords, and private encryption keys were accidentally committed to public repositories on GitHub. Automated bots continuously scan public repositories for patterns that look like credentials, and compromised keys are often exploited within minutes of being pushed. AWS keys in particular are a frequent target, with attackers spinning up expensive compute instances for cryptocurrency mining before the key owner even realizes what happened.

The defense is straightforward but requires discipline: add sensitive patterns to .gitignore before you create the files, not after. Common patterns to ignore include .env, .env.local, *.pem, *.key, credentials.json, and any file containing the word "secret" in its name. Provide a .env.example or .env.template file with placeholder values so that other developers know which environment variables the application expects without exposing actual secrets.

Fixing Already-Tracked Files

When a file that should be ignored has already been committed, simply adding it to .gitignore is insufficient. You need to remove it from Git's index while keeping your local copy. The command git rm --cached filename does exactly this — it untracks the file without deleting it from your working directory. For directories, add the -r flag: git rm --cached -r directory/. After running this command, commit the change, and from that point forward the .gitignore pattern will take effect. Remember, though, that the file's contents remain in the repository's history. If the file contained secrets, you'll need to use tools like git filter-branch, git filter-repo, or BFG Repo-Cleaner to purge it from all historical commits, and you should consider any exposed credentials compromised regardless.

The .gitkeep Convention

Git does not track empty directories. If your project structure requires an empty directory to exist — perhaps an uploads/ folder that needs to be present at runtime but should never contain committed files — the community convention is to place a file named .gitkeep inside it. Despite the name, .gitkeep has no special meaning to Git; it's simply a placeholder file whose sole purpose is to force Git to track the otherwise-empty directory. Some projects use .keep or an empty .gitignore for the same purpose. If the directory should exist but its contents should be ignored, you can place a .gitignore inside it containing * followed by !.gitignore, which ignores everything in the directory except the .gitignore file itself.

Related Ignore Files

The concept of file-ignore patterns extends beyond Git. Docker uses .dockerignore files with similar (but not identical) syntax to exclude files from the build context sent to the Docker daemon. A well-crafted .dockerignore can dramatically reduce build times and image sizes by excluding node_modules/, .git/, test files, and documentation from the build context. NPM uses .npmignore to determine which files to exclude when publishing a package, though if no .npmignore exists it falls back to .gitignore. Many developers are unaware of this fallback behavior, which occasionally leads to either bloated packages (because the .gitignore wasn't restrictive enough) or broken packages (because the .gitignore excluded files needed at runtime).

Debugging Ignore Rules

When files are unexpectedly ignored or unexpectedly showing up, Git provides a diagnostic tool: git check-ignore -v filename. This command tells you exactly which ignore rule is affecting a file and which .gitignore file contains that rule. The -v (verbose) flag is essential — without it you only get confirmation that a file is ignored, not why. For the inverse problem — figuring out why a file is not being ignored — check whether it's already tracked with git ls-files filename. If it returns the filename, the file is in the index and .gitignore rules won't apply until you git rm --cached it.

Crafting .gitignore for Monorepos

Monorepos present unique challenges for ignore patterns. A repository containing a React frontend, a Python API server, and shared infrastructure code needs patterns for all three ecosystems. The preferred approach is to place universal patterns (OS artifacts, common IDE files) in the root .gitignore and technology-specific patterns in .gitignore files within each sub-project's directory. This keeps the rules close to the code they affect and makes it easier to extract a sub-project into its own repository in the future.

Avoid the temptation to dump every possible pattern into a massive root .gitignore. While it technically works, it becomes difficult to maintain, obscures which patterns serve which parts of the codebase, and may ignore files in one sub-project that should actually be tracked in another.

A thoughtfully constructed .gitignore is a small thing, but it pays dividends throughout a project's life. It keeps your commits focused on meaningful changes, prevents sensitive data from leaking, reduces repository bloat, and eliminates the noise of build artifacts and platform-specific detritus from your diffs. Taking the time to understand the syntax, hierarchy, and security implications — and using tools like the Gitignore Generator to get a solid starting point — is one of those modest investments that saves disproportionate amounts of trouble down the line.

Related Tools

Related Articles

Try Our Free Tools

200+ browser-based tools for developers and creators. No uploads, complete privacy.

Explore All Tools