I still remember the first time I saw a live AWS key sitting inside a public GitHub repo. It wasn’t some obscure side project either — it was a production .env file that someone had committed “just for testing.” Within minutes of me finding it, I knew a bot somewhere had probably found it first. That’s the reality of secret leaks: they don’t wait for you to notice.
If you write code and push it to Git, this post is for you. I’m going to walk through exactly how secrets end up in repositories, how to stop it from happening, and what to do if it already has.
What Counts as a “Secret” in Git?
Before I get into prevention, let me define what I mean by a secret, because people often underestimate the list:
- API keys and tokens (AWS, Stripe, Twilio, OpenAI, etc.)
- Database connection strings and passwords
- Private SSH and TLS keys
- OAuth client secrets
- Webhook signing secrets
- Encryption keys and JWT signing secrets
.envfiles with any of the above
Any of these, once committed, become part of Git’s history — and Git history doesn’t forget easily.
Why Git Makes This Problem Worse
Git is designed to remember everything. That’s a feature for code, but a liability for secrets. Even if you delete a secret in your next commit, it still exists in the commit history, accessible to anyone with clone access or, if the repo is public, to anyone on the internet.
flowchart LR
A[Developer writes code] --> B[Secret hardcoded in file]
B --> C[git add & git commit]
C --> D[git push to remote]
D --> E{Repo public?}
E -->|Yes| F[Scraped by bots in minutes]
E -->|No| G[Still exposed to anyone with repo access]
F --> H[Secret abused or sold]
G --> H
That’s the exact path I’ve seen play out more times than I’d like to admit. The fix isn’t a single tool — it’s a layered process.
Step 1: Never Hardcode Secrets in the First Place
This sounds obvious, but it’s the root cause in almost every leak I’ve investigated. Instead of hardcoding values, I always push developers toward environment variables or a dedicated secrets manager.
# Bad
API_KEY = "sk_live_51Hxxxxxxxxxxxxxxxxxxxx"
# Better
import os
API_KEY = os.environ.get("API_KEY")
For local development, I keep secrets in a .env file and make sure it’s excluded from version control from day one.
# .gitignore
.env
*.pem
*.key
config/secrets.yml
If I’m working with Linux servers where credentials or SSH configs come into play, I also recommend reviewing how Secure Shell for remote logins is configured, since SSH keys are one of the most commonly leaked secret types.
Step 2: Use Pre-Commit Hooks to Catch Secrets Before They’re Committed
I treat pre-commit hooks as my first line of defense — they stop a secret before it ever reaches Git history.
Tools I rely on:
- git-secrets (AWS Labs) — blocks commits matching known secret patterns
- gitleaks — fast, configurable, and works well in CI too
- detect-secrets (Yelp) — good for baseline scanning on existing repos
Here’s how I set up gitleaks as a pre-commit hook:
# Install gitleaks
brew install gitleaks
# Add to .pre-commit-config.yaml
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.0
hooks:
- id: gitleaks
Once this is in place, any commit containing something that looks like a key or token gets rejected automatically before it leaves my machine.
Step 3: Enable Secret Scanning at the Platform Level
Pre-commit hooks only protect the machines that have them installed. To cover the whole team, I always enable server-side scanning too. GitHub, GitLab, and Bitbucket all offer this natively now.
If you’re on GitHub, I’ve written a full breakdown in GitHub Secret Scanning Explained that covers how push protection works and how to enable it for your organization.
Step 4: Scan Your Full Git History, Not Just New Commits
Most leaks I’ve fixed weren’t in the latest commit — they were buried three months deep in history. I always run a full history scan when auditing a repo for the first time.
gitleaks detect --source . --report-path gitleaks-report.json
If something turns up, deleting the file in a new commit isn’t enough. I have to rewrite history using git filter-repo or the BFG Repo-Cleaner, then force-push and coordinate with the team since it rewrites shared history.
bfg --delete-files id_rsa
git reflog expire --expire=now --all
git gc --prune=now --aggressive
Step 5: Rotate the Secret Immediately
This is the step people skip most often, and it’s the most important one. Deleting a leaked secret from Git doesn’t undo the exposure — anyone who cloned the repo, or any bot that scraped it, already has a copy. The only real fix is to revoke and rotate the credential. I go into the full process in Secret Rotation Best Practices, which pairs well with this post.
Common Mistakes I See Teams Make
- Assuming a private repo means secrets are safe — access still spreads through forks, CI logs, and former employees.
- Removing a secret from the latest commit but leaving it in history.
- Storing secrets in CI/CD YAML files instead of a secrets manager.
- Not rotating a key after “cleaning” the repository.
- Ignoring secrets inside build artifacts, Docker images, or log output.
Best Practices Checklist
| Practice | Why It Matters |
|---|---|
.gitignore secrets and env files | Stops accidental commits |
| Pre-commit secret scanning | Catches leaks before push |
| Server-side push protection | Covers the whole team |
| Regular full-history scans | Finds legacy exposures |
| Immediate rotation on leak | Neutralizes exposure |
| Centralized secrets manager | Removes secrets from code entirely |
For teams working in containerized environments, I’d also recommend reading about Docker security practices since secrets frequently leak through baked-in image layers too.
FAQs
Can I just delete a secret from my last commit and be safe? No. The secret remains in Git history unless you rewrite it, and even then, you should treat the credential as compromised and rotate it.
Do private repositories eliminate the risk? No. Private repos reduce exposure but don’t eliminate it — collaborators, forks, and integrations can still access history.
What’s the fastest way to check if my repo already has leaked secrets? Run gitleaks detect or trufflehog against the full history. Both are free and take minutes to run.
Should I use environment variables or a secrets manager? Environment variables are fine for local development, but for production, a secrets manager (Vault, AWS Secrets Manager, etc.) gives you rotation, auditing, and access control that plain env vars can’t.
Conclusion
Secret leaks in Git aren’t a matter of if — they’re a matter of when, unless you build prevention into your workflow. I treat it as a layered defense: keep secrets out of code, catch them with pre-commit hooks, scan at the platform level, audit history regularly, and rotate immediately when something slips through. Get these five steps right, and you’ll catch the vast majority of leaks before they ever become an incident.