
How to Prevent Secret Leaks in Git (Before They Reach GitHub)
28.65 million secrets were leaked on GitHub in 2025 alone. Learn how to set up pre-commit scanning with gitleaks, use GitHub Push Protection, and respond when a key slips through.
In March 2026, GitGuardian published their annual State of Secrets Sprawl report. The headline number: 28.65 million new secrets detected in public GitHub commits during 2025. API keys, database passwords, cloud credentials, private keys. All sitting in git history, indexed and searchable.
That number has grown every year for the past five years. And it only counts public repositories. The private repo picture is worse: GitGuardian found that 80% of secrets detected on private repos are still valid at time of detection, meaning nobody rotated them. The average time to remediate a leaked secret? 27 days.
This article covers the tools and practices that actually prevent these leaks, the real incidents that show why it matters, and what to do when a secret gets through.
Why Secrets End Up in Git
The typical leak follows one of these patterns:
Accidental staging. You run git add . and your .env file gets included. You commit, push, and now your production database password is on GitHub. Even if you delete it in the next commit, it is still in the git history. An academic study on "oops commits" found that over 10% of leaked secrets found by GitGuardian were "fixed" this way: deleted in a follow-up commit but still fully visible in history.
Hardcoded values during development. You paste a real API key into your code to test something. You mean to replace it with an environment variable later. You forget.
AI-generated code. Your AI coding assistant reads your .env file or terminal environment, then includes the actual value in a code suggestion. You accept the suggestion without reviewing every line. The secret is now in your source code.
Configuration files. Not all config files are in .gitignore by default. Files like docker-compose.yml, .env.production, Terraform .tfvars, or custom config formats often contain secrets and get committed without anyone noticing.
Real Incidents
These are not hypothetical scenarios.
Uber (2016). Engineers committed AWS credentials to a private GitHub repository. Attackers found them, accessed an S3 bucket containing data on 57 million riders and drivers, and held the data for ransom. Uber paid $148 million in settlements and was fined by regulators in multiple countries. A single committed credential turned into one of the largest data breaches of the decade.
EleKtra-Leak (2023-2024). Security researchers discovered an automated attack campaign that monitored GitHub in real-time for exposed AWS IAM credentials. When a developer pushed a commit containing an AWS key, attackers cloned the repo within minutes and spun up EC2 instances for cryptomining. The entire attack chain, from push to exploitation, took less than five minutes. Automated scanners do not sleep.
tj-actions/changed-files (March 2025). A supply chain attack compromised a popular GitHub Action used by over 23,000 repositories. The attackers injected malicious code that extracted CI/CD secrets from build environments and dumped them into build logs. Any secret available to the CI pipeline was exposed. This was not a developer mistake. It was the infrastructure itself leaking secrets.
Layer 1: .gitignore
The first defense is making sure sensitive files never get tracked. Your .gitignore should include at minimum:
.env
.env.*
.env.local
.env.production
*.pem
*.key
*.tfvars
.securecoderc
Check your .gitignore right now. If .env is not in there, add it before doing anything else.
One thing .gitignore cannot do: protect secrets that are hardcoded in source files. For that, you need scanning.
Layer 2: Pre-Commit Scanning
A pre-commit hook runs automatically before every commit. If it finds a secret, the commit is blocked. You fix the issue and try again.
Gitleaks (recommended)
Gitleaks is the most widely used open-source secret scanner, with over 700 regex patterns for known secret formats. It is actively maintained and has a strong community.
# Install gitleaks
brew install gitleaks # macOS
# or download from GitHub releases for any platform
# Set up pre-commit hook
gitleaks protect --install
Now every commit is scanned automatically. Gitleaks checks for patterns like AWS keys (AKIA...), Stripe keys (sk_live_...), GitHub tokens (ghp_...), and hundreds of other known formats.
If you use the pre-commit framework, add gitleaks as a hook:
# .pre-commit-config.yaml
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.21.0
hooks:
- id: gitleaks
TruffleHog (deep audits)
TruffleHog by Trufflecurity goes beyond pattern matching. It can verify whether a detected secret is actually active by testing it against the service provider's API. This is valuable for auditing existing repositories where you need to know which leaked secrets are still exploitable.
# Scan a repo for verified secrets
trufflehog git file://./your-repo --only-verified
Use TruffleHog when you need to audit a repository's full history or verify which leaked secrets need immediate rotation.
A note on git-secrets
You may see git-secrets recommended in older articles. It was built by AWS Labs and focused on catching AWS credentials. However, it is effectively unmaintained: the last release was in 2019, and it has not seen meaningful development in years. Use gitleaks or TruffleHog instead.
Betterleaks
Betterleaks is a newer fork that builds on the gitleaks pattern set with additional improvements. Worth watching if you want an alternative to gitleaks.
Layer 3: CI Pipeline Scanning
Pre-commit hooks protect your local machine. But what about commits pushed from other machines, direct pushes to GitHub, or developers who skip hooks with --no-verify?
Add a scanning step to your CI pipeline:
# .github/workflows/secrets-scan.yml
name: Secret Scanning
on: [pull_request, push]
jobs:
gitleaks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Scan for secrets
uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
This runs on every pull request and push, blocking merges if secrets are detected. It is your safety net for anything that bypasses local hooks.
Layer 4: GitHub Push Protection
GitHub Push Protection blocks pushes that contain recognized secret patterns before they reach the repository. It runs server-side, so it cannot be bypassed by skipping local hooks.
Key facts:
- Free for all public repositories since February 2024.
- For private repositories, it requires a GitHub Advanced Security license.
- Reports a 75% precision rate, meaning relatively few false positives.
- Partners with over 200 service providers (AWS, Google Cloud, Stripe, etc.) to automatically revoke secrets when they are detected in public repos.
Enable it in your repository settings under Security > Code security > Secret scanning > Push protection.
Even with Push Protection enabled, keep your pre-commit hooks. Defense in depth means not relying on a single layer.
What to Do When a Secret Leaks
If a secret has already been committed, deleting it in a new commit is not enough. The value remains in your git history, and automated scanners check history. Here is the response procedure:
1. Rotate the secret immediately
Generate a new API key, change the password, revoke the token. Do this first. Before cleaning history, before investigating, before anything else. Every minute the old credential remains valid is a window for attackers.
2. Check access logs
Review the service provider's access logs for the leaked credential. Look for requests from unfamiliar IPs or unusual patterns. If the key was used by someone else, you have a larger incident to handle.
3. Remove from git history
Use git-filter-repo to rewrite history. It is the modern replacement for BFG Repo-Cleaner (which is no longer actively maintained) and is recommended by the Git project itself.
# Install git-filter-repo
pip install git-filter-repo
# Replace a leaked secret in all history
git filter-repo --replace-text expressions.txt
Where expressions.txt contains the secret to replace:
sk_live_abc123def456==>REDACTED
After rewriting history, coordinate a force-push with your team, since everyone will need to re-clone or reset their local copies.
4. Review how it happened
Was it a missing .gitignore entry? A hardcoded value? An AI suggestion? Fix the root cause so it does not happen again.
The AI Angle
AI coding assistants compound the secret leak problem. When Claude Code, Cursor, or Copilot reads your .env file or terminal environment, it can include real secret values in code suggestions, commit messages, or shell commands. One accepted autocomplete and the secret is in your source code.
Pre-commit hooks catch these leaks before they reach your repository. But the better approach is preventing the AI from ever seeing secret values in the first place.
SecureCode's zero-knowledge inject mode writes secret values to a temporary, short-lived file and gives the AI only the file path. The AI can reference the secret in commands but never sees the actual value. No value in the context means no value in code suggestions, and no accidental commits.
For the full setup, read how to manage secrets safely with Claude Code.
Quick Setup Checklist
-
.envand.env.*are in.gitignore - Gitleaks is installed as a pre-commit hook
- CI pipeline has a secret scanning step (gitleaks-action)
- GitHub Push Protection is enabled
- Team knows the rotation procedure for leaked secrets
- AI assistant uses zero-knowledge injection instead of reading
.envfiles directly - Existing repo has been audited with TruffleHog for historical leaks
Further Reading
- Why .env files are dangerous with AI agents explains how AI assistants leak secrets through context
- AI agent security guide covers broader security practices for teams using AI tools
- GitGuardian State of Secrets Sprawl 2026 for the full report
- Try SecureCode free. Zero-knowledge secrets for AI-assisted development