How .gitignore Works, and How to Build One
After reading this you will know exactly how Git decides which files to ignore, how to read a pattern line by line, and how to assemble a correct .gitignore from language and editor templates without breaking files you already committed.
What a .gitignore actually does
A .gitignore file is a list of patterns. Git checks every untracked file against those patterns and hides the matches from status output and from git add. That is the whole job. It does not delete files, and it does not touch anything Git is already tracking.
Here is the hook. Suppose your project has a node_modules folder with 41,382 files. Without a rule, git status lists all of them. Add one line:
Now those 41,382 files vanish from status in one step. The trailing slash means "match a directory named node_modules and everything inside it". Git stops descending into that folder entirely, which is also why a single directory rule is faster than 41,382 individual file rules.
Git reads ignore rules from several places: .gitignore files committed in the repository, a .git/info/exclude file that stays local, and a global file set by core.excludesFile. The generator here produces the repository .gitignore, which is the one your whole team shares.
When to use a generated template, and when not
Reach for a generated .gitignore at the start of a project, or when you add a new language or tool to an existing one. The templates cover the files that a Python interpreter, a Node install, a JetBrains IDE, or macOS drops into your working tree without asking: __pycache__/, node_modules/, .idea/, .DS_Store. These are predictable across every project, so a curated list beats writing them from memory.
Do not use a template as a place to hide secrets. A .env file with database passwords should be ignored, yes, but ignoring it is not the same as securing it. If that file was ever committed, the password is in your history forever until you rewrite it. The ignore rule only stops future accidental commits.
Also skip the template for files that are specific to one machine or one developer. Those belong in .git/info/exclude or your global ignore file, not in the shared repository file, because your teammates do not have your editor or your local scratch files.
How Git matches a pattern
The rules come from the gitignore specification, and they are more precise than most people assume. Six facts govern every match.
- Blank lines and comments
- A blank line matches nothing and is used for spacing. A line beginning with
#is a comment. To match a literal file named#test, escape it as\#test. - Trailing slash
- A pattern ending in
/matches directories only.build/matches a directory but not a file calledbuild. - Leading slash
- A slash at the start anchors the pattern to the
.gitignorefile's own directory./config.jsonmatches only at the top level, notsrc/config.json. - Middle slash
- Any slash that is not at the end also anchors the pattern relative to the file.
doc/frotzmatchesdoc/frotzbut nota/doc/frotz. - No slash
- A pattern with no slash (except a trailing one) matches at any depth.
*.logmatchesapp.logandlogs/server/app.logalike. - Negation
- A line starting with
!re-includes a file the previous rules excluded. Order matters, and you cannot re-include a file if its parent directory is already ignored.
The wildcards themselves follow shell globbing. A single * matches any run of characters except /. A ? matches exactly one character except /. A double ** is special: **/foo matches foo at any depth, foo/** matches everything under foo, and a/**/b matches a/b, a/x/b, a/x/y/b and so on.
The negation trap catches everyone once. If you write logs/ and then !logs/keep.log, the keep line does nothing. Git already refused to descend into the logs directory, so it never sees the file. To re-include one file, ignore contents narrowly instead: logs/* followed by !logs/keep.log.
The precedence rule, stated exactly
When more than one pattern could apply, the last matching pattern wins. Read the file top to bottom; the final line that matches a given path decides its fate.
Here f is a file path, and p_1, p_2, \ldots, p_n are the pattern lines in file order. Whichever p_i with the largest index i matches f sets the outcome: ignore if it is a normal pattern, keep if it starts with !. If no pattern matches, the file is not ignored.
This is why a generator labels and orders its blocks carefully. If a Python block ignores *.log and a later custom block re-includes !important.log, the order of those two lines is the entire difference between committing that file and losing it.
Worked example: the default Python and macOS template
The demo assembles the default blocks. Take a Python plus macOS combination and trace six files through it. The relevant lines are:
__pycache__/from the Python block.*.py[cod]from the Python block, matching.pyc,.pyo,.pyd..envfrom the Python block..DS_Storefrom the macOS block.- No rule for
main.pyorREADME.md.
Now walk each candidate file:
| File | Matching line | Result |
|---|---|---|
main.py | none | tracked |
main.cpython-311.pyc | *.py[cod] | ignored |
__pycache__/x.pyc | __pycache__/ | ignored |
.env | .env | ignored |
.DS_Store | .DS_Store | ignored |
README.md | none | tracked |
Four of six files are ignored. The two source files you actually want in version control, main.py and README.md, pass through untouched because no pattern names them.
Merging templates without duplication
Combine three or four templates by hand and you will produce the same line several times. Node and a generic editor template both ignore *.log. macOS and Windows both list clutter files. The generator merges identical lines so each pattern appears once, and it labels each block with a comment header so you can see where a rule came from.
Deduplication matters for readability, not correctness: Git treats a pattern that appears twice exactly as it treats it once. But a 200-line file with 40 duplicate lines is hard to audit, and auditing is the point. You should be able to read your .gitignore and know why every file is hidden.
Common mistakes
The re-inclusion trap above is the most frequent, but four others cost real time.
Editing after committing. Adding secrets.txt to .gitignore does nothing if you already ran git add secrets.txt. Git keeps tracking it. Run git rm --cached secrets.txt to stop tracking while keeping the file on disk, then commit that change.
Anchoring by accident. A single slash anywhere except the end anchors the pattern to the file's directory. People write src/*.tmp expecting it to match .tmp files in every src folder; it only matches the top-level one. Use **/src/*.tmp or drop the path.
Trailing whitespace. A space at the end of a line is significant unless escaped or unless it is trailing space that Git strips. To match a filename that genuinely ends in a space, escape the final space with a backslash. Most such lines are typos.
Ignoring the whole config folder. Teams ignore .vscode/ wholesale, then lose shared launch settings. Ignore the machine-specific files inside it and commit the rest.
Related tools
A .gitignore often sits beside config files you also need to check. If your repository holds JSON config, the JSON Formatter & Validator confirms it parses before you commit. To move settings between formats, the data converter handles JSON, YAML, TOML, XML and CSV. When you want to see exactly what changed between two config versions, the JSON Diff gives a structural comparison.
The pattern matching in .gitignore uses glob syntax, not regular expressions, but if you are testing true regular expressions for a related task the Regex Tester explains each token in plain English. To escape a filename with special characters for a shell command like git rm, the String Escaper handles shell and JSON escaping.
Frequently asked questions
Does .gitignore work on files I already committed?
No. It only affects untracked files. Anything Git already tracks keeps being tracked until you run git rm --cached path and commit the removal. The file stays on your disk; Git just stops watching it.
Why is my ignore rule not working?
Check three things in order. First, is the file already tracked? Run git status; if the file shows as modified rather than untracked, the rule is being overridden by tracking. Second, does a slash in your pattern anchor it somewhere you did not intend? Third, run git check-ignore -v path, which prints the exact file and line number of the rule that matched, or nothing if no rule matched.
Where do I put the .gitignore file?
Usually one file at the repository root. You can also place additional .gitignore files in subdirectories, and their patterns apply relative to that subdirectory. Rules in a deeper file take precedence over rules in a parent for files under that directory.
Can I ignore everything except a few files?
Yes, with two lines: * to ignore everything, then !keep.txt to re-include what you want. Remember the directory rule: to re-include a file in a subfolder you must also re-include the folder, for example !subdir/ then !subdir/keep.txt.
Are the generated rules safe to commit?
Yes. The templates follow the patterns used by GitHub's official template collection and contain only pattern lines, no data from your machine. This generator runs entirely in your browser, so nothing you type leaves your device.