A .gitignore prevents the next accident. It does nothing about the last one.
It does not untrack what is already committed
This is the most important thing to know. Adding a rule for a file already in the repository changes nothing — git continues to track it, and every historical version remains in the history, in every clone, and in every fork.
If a secret is already committed:
- Rotate it. This is what actually revokes access. Everything else is housekeeping.
- Remove it from the working tree and add the ignore rule.
- Rewrite history with
git filter-repoif you want it gone from the repository. - Force-push, and tell every collaborator to re-clone.
Step 1 is the one that matters. Steps 3 and 4 do not un-share what has already been shared.
Filenames are half the problem
Ignore rules catch files named predictably. They do not catch a key pasted into config.js, a token in a test fixture, or a private key in a README.
That is what the pre-commit hook is for. It does two things:
- Refuses to stage files matching credential filename patterns.
- Greps the staged diff for high-confidence credential signatures —
sk_live_,ghp_,AKIA,BEGIN PRIVATE KEY.
# Install it
cp pre-commit .git/hooks/pre-commit
chmod +x .git/hooks/pre-commitThe catch is that git hooks are local and not shared by cloning. For a team, use core.hooksPath with a committed hooks directory, or a managed framework like pre-commit or Husky.
The global ignore file
The rules that should apply to every repository you touch, including ones you clone:
git config --global core.excludesfile ~/.gitignore_globalThis is what protects you when you clone a repository whose .gitignore does not cover .env, and then create one.
Negation for example files
.env
.env.*
!.env.exampleThe third line un-ignores the example file so it can be committed. Order matters — the negation must come after the pattern it overrides.
Frequently asked questions
Should I commit .env.example?
Yes. Keys with empty values, no real secrets. It documents what the application needs and makes a missing variable immediately obvious to a new developer.
Does .gitignore protect files already committed?
No. Use git rm --cached <file> to stop tracking it going forward, and rewrite history to remove it from the past — but rotate the credential first, because that is the only step that actually revokes access.
Why ignore *.pem and *.key but not *.pub?
Public keys are meant to be shared and are often committed deliberately — an authorized_keys entry, a JWKS file. The !*.pub negation keeps them committable while ignoring their private counterparts.
Is a pre-commit hook enough?
It is the cheapest layer and catches most accidents. Add CI-side scanning as well, because hooks can be bypassed with --no-verify and are not installed on every machine. The secret leak scanner covers the manual review case.