TechEarl

How to Use .gitignore (with Examples)

A practical guide to .gitignore: pattern syntax, per-repo vs global ignore, ready-made templates, and the gotcha that trips everyone up - already-tracked files keep showing up.

Ishan Karunaratne⏱️ 10 min readUpdated
Share thisCopied
How to use a .gitignore file to stop Git from tracking build output, dependencies, and secrets

A .gitignore file tells Git which files to leave alone: build output, dependencies, secrets, OS junk. You create a plain text file named .gitignore in your repository, list one pattern per line (like node_modules/ or *.log), and commit it. From then on, anything matching those patterns stays out of git status and never gets committed. The one trap to know up front: .gitignore only stops Git from tracking files it is not already tracking. A file you have already committed keeps showing up until you untrack it. More on that fix below.

What does .gitignore actually do?

When you run git status, Git lists every file in your working directory that has changed or is new. Most of the time that is what you want. But some files have no business being in version control:

  • Dependency folders you can reinstall (node_modules/, vendor/, venv/).
  • Build artifacts you can regenerate (dist/, build/, *.o).
  • Local config and secrets (.env, config.local.php).
  • Editor and OS cruft (.DS_Store, .idea/, *.swp).

.gitignore is how you tell Git to skip those. Each line is a pattern. If a file's path matches a pattern, Git pretends it does not exist for the purposes of git status and git add .. It will not be staged, it will not be committed, and it will not nag you in the status output.

This matters early. If you are still getting your bearings, the Git for beginners guide is the place to start, and understanding what the staging area is doing makes ignore rules click faster: ignored files simply never reach the staging area.

Creating your first .gitignore

The file lives at the root of your repository, alongside your .git folder. Create it like any text file:

bash
touch .gitignore
git status before and after adding a .gitignore file
Once .gitignore is in place, the ignored files drop out of git status.

Open it and add patterns, one per line. A simple Node.js project might start like this:

text
# Dependencies
node_modules/

# Build output
dist/
build/

# Environment files with secrets
.env
.env.local

# Logs
*.log

# macOS
.DS_Store

Save it, then commit it. The .gitignore file itself is meant to be tracked and shared, so everyone working on the project ignores the same things:

bash
git add .gitignore
git commit -m "Add .gitignore for build output and secrets"

That is the whole basic workflow. The interesting part is the pattern syntax.

.gitignore pattern syntax

Each non-blank, non-comment line is a pattern. Here is what the syntax gives you.

Comments and blank lines

A line starting with # is a comment. Blank lines are ignored and are handy for grouping. To match a file that literally starts with a hash, escape it with a backslash, as in \#weird-filename.

Plain names match anywhere

A bare name with no slash matches that name at any depth in the tree:

text
*.log

This ignores app.log, logs/error.log, and src/debug/trace.log, all of them, because there is no leading or internal slash to anchor it.

A trailing slash means "directory only"

Adding a / at the end restricts the match to directories:

text
build/

This ignores any directory named build and everything inside it, but it would not match a file named build (no extension). Use trailing slashes for folders like node_modules/, dist/, and .cache/.

A leading or internal slash anchors to the repo root

A slash anywhere except the very end anchors the pattern relative to the location of the .gitignore file:

text
/config.local.php
docs/build/

The first line ignores config.local.php only at the repository root, not a config.local.php buried in a subfolder. The second ignores build only inside docs, leaving a top-level build alone.

Wildcards: *, ?, and **

The glob characters behave roughly as they do in a shell:

PatternMatches
*.logAny file ending in .log
temp?temp1, tempA, any single character after temp
*.{jpg,png} is not supportedbrace expansion does not work in gitignore
**/logsA logs directory at any level
logs/**Everything under logs, at any depth
a/**/ba/b, a/x/b, a/x/y/b

Note the third row: gitignore does not support shell-style {jpg,png} brace expansion. Write two separate lines instead. A single * does not cross a directory separator; ** is the one that spans multiple path segments.

Negation: re-include something with !

A leading ! re-includes a file that an earlier pattern excluded. This is how you ignore a whole directory but keep one file in it:

text
# Ignore everything in logs/
logs/*

# ...except the rule that documents the format
!logs/README.md

There is one catch worth memorizing: you cannot re-include a file if its parent directory is itself ignored. Git never looks inside an ignored directory, so a ! on a file deep inside it has nothing to act on. Ignore the directory's contents (logs/*), not the directory itself (logs/), if you want a negation to work.

Per-repo vs global gitignore

There are two places ignore rules can live, and they serve different purposes.

Per-repo .gitignoreGlobal gitignore
LocationRoot of the repository (committed)A file you point Git at, e.g. ~/.gitignore_global
Shared with the team?Yes, it is committed and pushedNo, it is personal to your machine
Good forProject-specific output: dist/, vendor/, .envThings tied to your tools: .DS_Store, .idea/, *.swp

The rule of thumb: if everyone on the project should ignore it, it belongs in the committed .gitignore. If it is an artifact of your editor or operating system that nobody else needs to care about, put it in your global ignore so you are not pushing .DS_Store rules into a Linux teammate's project.

Set up a global ignore once:

bash
git config --global core.excludesFile ~/.gitignore_global

Then add your personal patterns to ~/.gitignore_global:

text
.DS_Store
.idea/
*.swp
.vscode/

There is also a third, repo-local-but-uncommitted option: .git/info/exclude. It works exactly like .gitignore but lives inside the .git folder and is never committed (the .git/info/exclude file is itself part of the repo's internal layout, the same .git directory whose absence triggers the "not a git repository" error). I reach for it rarely, when I have a scratch file in one clone that I do not want to track and do not want to impose on anyone else.

The number-one gotcha: ignoring a file Git already tracks

This is the question I have answered more times than any other gitignore question, so here it is in full.

You add config.local.php to your .gitignore. You expect it to disappear from git status. It does not. Every change you make to it still shows up. You re-read your pattern, it looks correct, and you start to doubt your sanity.

The pattern is fine. The problem is that .gitignore only affects untracked files. Once a file has been committed, Git is tracking it, and a tracking decision overrides an ignore rule. Git assumes that if a file is already in the repository, you meant to keep it there, so it keeps reporting your edits. (If your goal is simply to discard those local changes instead of committing them, that is a different fix; what follows untracks the file entirely.)

The fix is to stop tracking the file while keeping it on disk:

bash
git rm --cached config.local.php

The --cached flag is the important part: it removes the file from Git's index (it stops being tracked) but leaves the actual file untouched in your working directory. Run git status now and you will see the file staged for deletion and showing up as ignored. Commit the removal:

bash
git commit -m "Stop tracking config.local.php; it is in .gitignore"

From that commit forward, the .gitignore rule takes effect and the file stays out of your way. There is a fuller walkthrough of this exact scenario in removing a file from Git without deleting it locally.

To untrack a whole directory you already committed, add -r:

bash
git rm -r --cached node_modules/

A word of caution for anyone tempted to use this on a secret: untracking a file with git rm --cached removes it going forward, but the file's contents are still sitting in your commit history, recoverable by anyone with the repo. If you accidentally committed an API key or a password, untracking it is not enough. You have to scrub it from history; see how to remove a secret from Git history. And if a public web server is exposing your .git folder, the whole history is downloadable, which is exactly the exposed .git directory attack worth understanding.

Force-checking why a file is (not) ignored

When a pattern is not behaving, do not guess. Ask Git which rule is matching:

bash
git check-ignore -v path/to/file

It prints the exact .gitignore file, line number, and pattern responsible for ignoring that path, or nothing at all if the file is not ignored (in which case the "already tracked" explanation above is your likely culprit). It is the fastest way to debug a stubborn pattern.

Ready-made templates so you do not start from scratch

You almost never need to write a .gitignore by hand. GitHub maintains a large public collection of templates at github/gitignore, one per language and framework: Node.gitignore, Python.gitignore, Java.gitignore, and dozens more. When you create a new repository on GitHub, the "Add .gitignore" dropdown pulls straight from that collection, which is the easiest way to start a fresh project well.

For an existing local project, gitignore.io (now run by Toptal) generates a combined file from any mix of languages, editors, and operating systems you name. Type node, macos, visualstudiocode and it gives you one tidy file covering all three.

A couple of habits that have saved me grief:

  • Add the .gitignore before the first commit. It is far easier than the untracking dance above. If you are starting clean, setting up Git for a new project is the right moment to drop the template in. Adding version control to something that already exists is its own small process, covered in adding Git to an existing project. And if you are working on a project someone else already set up, the .gitignore is usually already there waiting for you when you clone.
  • Ignore the artifacts, commit the recipe. Ignore node_modules/, but commit package-lock.json. Ignore vendor/, but commit composer.lock. The lockfile is how anyone else regenerates the ignored folder exactly.

If your project is WordPress, the ignore strategy is specific enough that I gave it its own treatment in using Git with WordPress: you typically ignore wp-content/uploads/ and core, and track only your themes and plugins.

A sane default .gitignore to copy

Here is a reasonable starting point for a mixed JavaScript and PHP project. Trim it to fit:

text
# Dependencies
node_modules/
vendor/

# Build output
dist/
build/
*.min.js

# Secrets and local config
.env
.env.*
!.env.example
config.local.php

# Logs and caches
*.log
.cache/
.parcel-cache/

# OS and editor files (better in your global ignore)
.DS_Store
Thumbs.db
.idea/
.vscode/

Notice the .env.* plus !.env.example pair: it ignores every environment file except the example template you actually want to commit so teammates know which variables to set. That is negation doing real work.

FAQ

Once ignore rules are out of the way and only the right files are staged, the next things worth learning are writing good commit messages and branching. Both are short, and both pay off on every project after.

Sources

Authoritative references this article was fact-checked against.

TagsgitignoreGitVersion ControlDevOpsGit Basics

Found this useful? Pass it on.

Copied

Ishan Karunaratne

Tech Architect · Software Engineer · AI/DevOps

Tech architect and software engineer with 20+ years building software, Linux systems, and DevOps infrastructure, and lately working AI into the stack. Currently Chief Technology Officer at a healthcare tech startup, which is where most of these field notes come from.

Keep reading

Related posts

How to Use Git with WordPress

How to put a WordPress project under Git: what to track vs ignore, a ready .gitignore, version-controlling just your theme or plugin, and deploy options.

How to Use git stash

How to use git stash to set work aside without committing. Save, list, pop, apply, and drop stashes, stash untracked files, do a partial stash, and switch branches cleanly.

How to Use ElasticPress with WP_Query

Wire ElasticPress to WP_Query so WordPress queries hit Elasticsearch instead of MySQL. Covers installation, indexable post types, ep_integrate, the wp-cli index command, faceted search with aggregations, and when ES actually beats MySQL FULLTEXT.