Security Guide
How to Stop Committing .env Files to Git
It happens to every team eventually. Someone commits a .env file, a database URL hits a public repo, and the next few hours are spent rotating credentials and auditing damage. This guide covers how to detect existing leaks, prevent future ones, and ultimately stop relying on .env files entirely.
Why This Keeps Happening
Developers know about .gitignore. The problem is that knowing isn't enough. Leaks happen through gaps that no amount of awareness training can fully close:
- 1.New repos start without a .gitignore. A quick
git init && git add .catches everything, including that.envyou just created. - 2.Subfolder .env files get missed. Your root
.gitignorehas.env, but the monorepo service atpackages/api/.env.localslips through because the pattern doesn't match nested paths. - 3.AI-generated code creates .env files. LLMs scaffold projects with placeholder secrets, generate multiple
.envvariants, and sometimes skip.gitignoreentries entirely. - 4.Renamed files bypass .gitignore rules. Files like
.env.backup,env.production(without the dot), orconfig.envare common and rarely covered by gitignore rules. - 5.git add -A in a hurry. Under deadline pressure, developers stage everything without reviewing. The .env file makes it into the commit before anyone notices.
Step 1: Check If You've Already Leaked
Before fixing the process, find out if .env files already exist in your Git history. Even if the file has since been deleted, the values are still recoverable from the commit log.
# Find any .env file that was ever added to the repo
git log --all --diff-filter=A -- '*.env*'
# Search for specific patterns in history
git log --all -p -S "DATABASE_URL" -- '*.env*'
# Check if .env is currently tracked
git ls-files --error-unmatch .env 2>/dev/null && echo "TRACKED" || echo "not tracked"If you have a GitHub repository, you can also search directly:
# In GitHub's search bar, search within your org:
org:your-org filename:.env
org:your-org "STRIPE_SECRET_KEY"
org:your-org "DATABASE_URL" extension:envImportant: If you find leaked credentials in your Git history, rotate them immediately. Removing the file from the repo does not invalidate the secret — anyone who cloned the repo already has access to every value that was ever committed.
Step 2: Scan Your Codebase
Beyond .env files, secrets frequently end up hardcoded in source files, configuration scripts, and deployment manifests. Use secr scan to audit your entire codebase — no account or installation required:
# Run without installing (npx downloads temporarily)
npx @secr/cli scan
# Or install globally for repeated use
npm i -g @secr/cli
secr scanThe scanner checks 20+ patterns including AWS keys, Stripe keys, database URLs, GitHub tokens, OpenAI keys, and more. Here is typical output:
$ secr scan
✗ Found 4 potential secret(s)
.env.backup
[HIGH] AWS Access Key ID L2:1
[HIGH] AWS Secret Access Key L3:1
src/config.ts
[HIGH] Stripe Live Secret Key L28:18
docker-compose.yml
[MED] Database URL with Password L12:7
4 high, 1 medium, 0 low | 203 files scanned in 96msEverything runs locally. No files are sent to any server. The scanner respects your .gitignore and skips binary files and node_modules automatically.
Step 3: Install a Pre-Commit Hook
Scanning is reactive. A pre-commit hook is proactive — it blocks the commit before secrets reach the repository. One command installs it:
secr guard installNow, every time you run git commit, secr scans the staged files for secrets. If anything is detected, the commit is blocked with a clear explanation:
$ git add src/config.ts
$ git commit -m "update configuration"
✗ Found 1 potential secret(s)
src/config.ts
[HIGH] Stripe Live Secret Key L14:22
Commit aborted. Remove the secret and try again.
Tip: Use process.env.STRIPE_SECRET_KEY instead of hardcoding the value.The hook scans only staged files, so it runs in milliseconds even on large codebases. You can check the status or remove it at any time:
# Check if the hook is installed
secr guard status
# Remove the hook
secr guard uninstallStep 4: Remove .env Files from Git History
If .env files were previously committed, deleting them in a new commit is not enough. The values remain in the Git history. You need to rewrite history to remove them.
Warning: Rewriting Git history is a destructive operation. Coordinate with your team before doing this on a shared repository. Everyone will need to re-clone or reset their local copies.
The recommended tool is git filter-repo (faster and safer than the older git filter-branch):
# Install git-filter-repo (macOS)
brew install git-filter-repo
# Remove all .env files from the entire history
git filter-repo --invert-paths --path '.env' --path '.env.local' --path '.env.production'
# Force push (coordinate with your team first)
git push origin --force --allIf you cannot install git-filter-repo, the older git filter-branch approach also works:
git filter-branch --force --index-filter \
'git rm --cached --ignore-unmatch .env .env.local .env.production' \
--prune-empty --tag-name-filter cat -- --allAfter rewriting history, rotate every credential that was ever in those files. The old values are still cached in forks, CI caches, and anyone who cloned before the rewrite.
The Permanent Fix: Stop Using .env Files Entirely
Gitignore rules, pre-commit hooks, and history scrubbing are all patches for the same root cause: secrets stored as plaintext files on disk. The permanent fix is to eliminate .env files from your workflow entirely.
With secr, secrets are stored encrypted on a server and injected into your process at runtime. No file is ever written to disk:
# One-time setup
npm i -g @secr/cli
secr login
secr init
# Import your existing .env file into secr
secr migrate .env
# Delete the .env file — you don't need it anymore
rm .env .env.local .env.production
# Run your app with secrets injected
secr run -- npm startFrom this point on, your development workflow is: secr run -- npm dev instead of npm dev. Secrets are pulled from the server, decrypted in memory, and injected as environment variables. There is no file to commit, no file to leak, and no file to share over Slack.
Comprehensive .gitignore Template
Even if you stop using .env files, keep a thorough .gitignore as a safety net. This template covers the most common secret file patterns:
# Environment files
.env
.env.*
.env.local
.env.development
.env.development.local
.env.staging
.env.production
.env.production.local
.env.test
.env.test.local
.env.backup
*.env
# Secret/credential files
.secret
.secrets
*.pem
*.key
*.p12
*.pfx
credentials.json
service-account.json
serviceAccountKey.json
.gcp-credentials.json
# IDE and tool configs that may contain tokens
.idea/workspace.xml
.vscode/settings.json
# Docker env files
docker-compose.override.yml
.docker-envTip: Apply your .gitignore globally across all repos with git config --global core.excludesfile ~/.gitignore_global. This protects you even in repos that lack their own .gitignore.
Stop the leak at the source
Scanning and gitignore rules catch mistakes after the fact. secr prevents them structurally by removing .env files from your workflow entirely.
npm i -g @secr/cli
secr scan
secr guard install
secr migrate .env
secr run -- npm dev