.gitignore
.gitignore is a file of path patterns that tells Git which files to leave untracked, so an API key, a virtual environment, a data folder and a saved model never reach a commit.
Last updated: 12 Oct, 2026 · git 2.54
Reading status taught you that every untracked file shows up in the answer. A working project collects files that should never be tracked: the key in .env that costs money if it leaks, a venv folder of hundreds of megabytes that each person rebuilds anyway, the documents in data/, a saved model, and the checkpoint files a notebook leaves behind. One file keeps all of them out of your commits and out of your status.
Syntax: one pattern per line in a file named .gitignore, in the top folder of the repository. A blank line is skipped and a line starting with # is a comment.
# secrets
.env
# python
venv/
__pycache__/
# notebooks
.ipynb_checkpoints/
# data and model files
data/
*.ptView the code here
[user]
name = Reader
email = reader@example.com
What a project collects in a week
This repository has two committed files and six things that should never be committed: the key, the virtual environment, compiled Python, a notebook checkpoint, the documents and a saved model.
mkdir ask-docs && cd ask-docs
git init -q -b main
printf 'Support bot\n' > README.md
printf 'chunk_size = 500\n' > loader.py
git add . && git commit -q -m "Start the project"
printf 'OPENAI_API_KEY=sk-live-9f2a\n' > .env # the key, worth money
git status --short?? .env
The key is untracked, so one careless git add . would stage it. Those five lines above it are the throwaway repository for the rest of this page, and the blocks below start from them. The other five things a week of work leaves behind go in next.
mkdir -p venv/lib && printf 'x\n' > venv/lib/site.py # the virtual environment
mkdir -p __pycache__ && printf 'x\n' > __pycache__/loader.pyc
mkdir -p .ipynb_checkpoints && printf '{}\n' > .ipynb_checkpoints/explore-checkpoint.ipynb
mkdir -p data && printf 'doc one\n' > data/corpus.jsonl # the documents
printf 'weights\n' > model.pt # a saved model
git status --short?? .env ?? .ipynb_checkpoints/ ?? __pycache__/ ?? data/ ?? model.pt ?? venv/
Every one is untracked, so a careless git add . would stage the lot. Git prints a folder once instead of listing every file inside it, which is why venv/ is one line and not a thousand.
Writing the .gitignore file
The patterns go in the top folder of the repository, next to .git. Writing the file in an editor is the normal way; this block writes it in one command so the example runs end to end.
printf '# secrets\n.env\n\n# python\nvenv/\n__pycache__/\n\n# notebooks\n.ipynb_checkpoints/\n\n# data and model files\ndata/\n*.pt\n' > .gitignore
cat .gitignore# secrets .env # python venv/ __pycache__/ # notebooks .ipynb_checkpoints/ # data and model files data/ *.pt
cat reads the file back so you can see what landed in it. A blank line separates the groups and each # line is a comment, so Git reads six patterns out of those thirteen lines.
git status --short?? .gitignore
- The six noisy entries are gone from the status. They are still on disk, and Git has been told to leave them untracked.
- ?? .gitignore is the one line left. The ignore file itself belongs in the repository, so that everyone working on the project ignores the same things. Commit it.
Checking which pattern matched
When a file is ignored and you cannot see why, git check-ignore -v names the file and the line that did it.
git check-ignore -v .env venv/lib/site.py data/corpus.jsonl model.pt.gitignore:2:.env .env .gitignore:5:venv/ venv/lib/site.py .gitignore:12:data/ data/corpus.jsonl .gitignore:13:*.pt model.pt
Each line reads file:line:pattern followed by the path it matched, so .gitignore:5:venv/ says the pattern on line 5 caught that file. The other direction is git status --ignored, which lists the ignored paths instead of hiding them.
git status --ignored --short?? .gitignore !! .env !! .ipynb_checkpoints/ !! __pycache__/ !! data/ !! model.pt !! venv/
!! marks an ignored path and ?? marks an untracked one. A folder pattern such as data/ is reported once for the folder, not once per file.
The patterns worth knowing
| Pattern | Matches | Use it for |
|---|---|---|
.env | that exact name, in any folder | the file your keys live in |
venv/ | a folder of that name and everything in it | virtual environments, node_modules/ |
__pycache__/ | the folder, at any depth | compiled Python |
*.pt | any file with that extension | saved models, *.ckpt, *.h5 |
data/ | the whole data folder | documents and datasets |
!data/sample.jsonl | an exception to an earlier pattern | the one small sample you do want committed |
Order matters for the last one: a ! line only rescues a file that an earlier pattern caught, and it cannot rescue a file inside an ignored folder, so data/sample.jsonl needs data/* above it rather than data/. GitHub keeps a ready-made file per language at github/gitignore, and the full rules are on the gitignore page.
The key that was committed before the pattern existed
.gitignore only decides what happens to untracked files. A file that is already in a commit stays tracked, and every change to it keeps appearing, however many patterns match its name.
printf 'OPENAI_API_KEY=sk-live-9f2a\n' > .env
git add . && git commit -q -m "Start the project" # the key went in
printf '.env\n' > .gitignore
printf 'OPENAI_API_KEY=sk-live-0000\n' > .env # change it
git status --shortM .env ?? .gitignore
M .env is the answer: the pattern is in place, the file is still tracked, and the change is still reported. A pattern cannot untrack a file; one more command does that.
git rm --cached .env
git status --shortrm '.env' D .env ?? .gitignore
- git rm --cached .env takes the file out of Git's tracking and leaves it on your disk.
git rmwithout the flag would delete your copy as well. - D .env is that removal, staged and waiting for a commit, and
?? .gitignoreis the ignore file waiting with it. After committing both, the pattern finally keeps the key quiet.
Removing the file does not remove the key from the history. The commit that added it is still there, git log -p prints it, and anyone who cloned the repository has it. Once such a commit is pushed, treat the key as public: scanners read new public repositories within minutes. The only fix that works is to create a new key and revoke the old one with the provider, and then keep the new one out of Git.
.gitignore and commit it before the secret file exists, not after. Keep an .env.example next to it, holding the variable names with no values, and commit that instead: the next person then knows which keys the project needs without ever seeing yours.Where you write a .gitignore
- At the start of a project. Start from GitHub's file for your language, add
.envand your data folder, and commit it before anything else. - When a tool starts writing new output. A new build folder or cache folder gets a new line the day it appears.
- Per machine, for your own files. Editor folders such as
.idea/and.vscode/belong in your global ignore file, set withgit config --global core.excludesfile, so your editor's habits stay out of the team's repository.
Related
- Previous: Reading git status
- Next: Looking at history with git log
- Reference: gitignore in the Git reference and GitHub's collection of ignore files
- Add
*.logto the file, createdebug.log, and watch it stay out ofgit status. - Change
data/todata/*and add!data/sample.jsonlon the next line. Create bothdata/corpus.jsonlanddata/sample.jsonland see only one of them come back. - Run
git check-ignore -v README.md, a file no pattern covers. Git prints nothing and exits with a non-zero status, which is how a script asks the question.
Every expert started right here.