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:
touch .gitignore
Open it and add patterns, one per line. A simple Node.js project might start like this:
# Dependencies
node_modules/
# Build output
dist/
build/
# Environment files with secrets
.env
.env.local
# Logs
*.log
# macOS
.DS_StoreSave 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:
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:
*.logThis 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:
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:
/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:
| Pattern | Matches |
|---|---|
*.log | Any file ending in .log |
temp? | temp1, tempA, any single character after temp |
*.{jpg,png} is not supported | brace expansion does not work in gitignore |
**/logs | A logs directory at any level |
logs/** | Everything under logs, at any depth |
a/**/b | a/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:
# Ignore everything in logs/
logs/*
# ...except the rule that documents the format
!logs/README.mdThere 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 .gitignore | Global gitignore | |
|---|---|---|
| Location | Root of the repository (committed) | A file you point Git at, e.g. ~/.gitignore_global |
| Shared with the team? | Yes, it is committed and pushed | No, it is personal to your machine |
| Good for | Project-specific output: dist/, vendor/, .env | Things 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:
git config --global core.excludesFile ~/.gitignore_globalThen add your personal patterns to ~/.gitignore_global:
.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:
git rm --cached config.local.phpThe --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:
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:
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:
git check-ignore -v path/to/fileIt 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
.gitignorebefore 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.gitignoreis usually already there waiting for you when you clone. - Ignore the artifacts, commit the recipe. Ignore
node_modules/, but commitpackage-lock.json. Ignorevendor/, but commitcomposer.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:
# 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.
- gitignore - Git reference documentationgit-scm.com
- Pro Git - Recording Changes to the Repositorygit-scm.com
- GitHub Docs - Ignoring filesdocs.github.com





