Problem: You Just Pushed API Keys to GitHub
You committed .env with your OpenAI API key. GitHub immediately sent a security alert. Your key is now in git history forever, even if you delete the file.
You'll learn:
- How to block secrets before they're committed
- Automated scanning with pre-commit hooks
- How to remove keys from git history
- Prevention strategies that actually work
Time: 12 min | Level: Beginner
Why This Happens
Developers copy API keys into code for testing. IDEs autocomplete file paths. .env files get staged with git add . without thinking. By the time you notice, it's already pushed.
Common symptoms:
- GitHub security alert emails
- Revoked API keys from providers
- Exposed credentials in public repos
- Keys buried in commit history
Solution
Step 1: Install git-secrets
This AWS tool scans commits for common secret patterns before they're added to history.
# macOS
brew install git-secrets
# Linux
git clone https://github.com/awslabs/git-secrets
cd git-secrets
sudo make install
# Windows (Git Bash)
git clone https://github.com/awslabs/git-secrets
cd git-secrets
./install.ps1
Expected: Command completes without errors.
Step 2: Configure git-secrets for Your Repo
cd your-project
# Initialize git-secrets
git secrets --install
# Add patterns for common API keys
git secrets --register-aws # AWS keys
# Custom patterns for other providers
git secrets --add '[A-Za-z0-9_]{32,}' # Generic 32+ char tokens
git secrets --add 'sk-[A-Za-z0-9]{48}' # OpenAI keys
git secrets --add 'ghp_[A-Za-z0-9]{36}' # GitHub PATs
git secrets --add 'AIza[0-9A-Za-z_-]{35}' # Google API keys
Why this works: Git-secrets runs on every commit, scanning staged files for patterns. If found, the commit is rejected before it enters history.
If it fails:
- Error: "not a git repository": Run
git initfirst - Permission denied: Add
sudoto install command on Linux
Step 3: Set Up Pre-commit Hooks
Use the pre-commit framework for more comprehensive checks.
# Install pre-commit
pip install pre-commit --break-system-packages
# Create config file
cat > .pre-commit-config.yaml << 'EOF'
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: detect-private-key # SSH keys, certificates
- id: check-added-large-files
args: ['--maxkb=1000']
- repo: https://github.com/Yelp/detect-secrets
rev: v1.4.0
hooks:
- id: detect-secrets
args: ['--baseline', '.secrets.baseline']
EOF
# Install hooks
pre-commit install
# Create baseline (scans existing files, creates allowlist)
detect-secrets scan > .secrets.baseline
Expected: Hooks install successfully. .secrets.baseline file created.
Step 4: Add Proper .gitignore Patterns
cat >> .gitignore << 'EOF'
# Environment files
.env
.env.*
!.env.example
# API keys and credentials
*.key
*.pem
*.p12
*.pfx
credentials.json
secrets.yaml
config/secrets/
# Common secret locations
.secret
.secrets/
private/
EOF
Why this works: Prevents accidental staging of known secret file patterns. The !.env.example line allows example files through.
Step 5: Test Your Protection
# Create a fake secret
echo "OPENAI_API_KEY=sk-1234567890abcdefghijklmnopqrstuvwxyz123456" > .env
# Try to commit it
git add .env
git commit -m "test: adding env file"
You should see: Commit rejected with error message showing the blocked secret pattern.
[ERROR] Detected secrets in .env
OPENAI_API_KEY=sk-************************************
Commit rejected.
If You Already Committed Secrets
Remove from History (Nuclear Option)
# WARNING: This rewrites git history
# Coordinate with your team first
# Install BFG Repo Cleaner
brew install bfg # macOS
# Or download from: https://rtyley.github.io/bfg-repo-cleaner/
# Remove secrets
bfg --replace-text secrets.txt # File with one secret per line
git reflog expire --expire=now --all
git gc --prune=now --aggressive
# Force push (only if you control the repo)
git push --force
Alternative (Surgical Approach):
# Remove specific file from history
git filter-branch --force --index-filter \
"git rm --cached --ignore-unmatch .env" \
--prune-empty --tag-name-filter cat -- --all
Critical: After removing secrets from history:
- Revoke the exposed keys at provider dashboard
- Generate new keys
- Update deployment configs
- Notify team members to re-clone the repo
Verification
# Test that hooks work
echo "test_key=sk-abcd1234" > test.txt
git add test.txt
git commit -m "test"
# Should be rejected
You should see: Commit blocked with secret detection message.
# Clean up test
git restore --staged test.txt
rm test.txt
What You Learned
- git-secrets blocks commits with API key patterns
- pre-commit hooks add multiple layers of protection
- .gitignore prevents accidental staging
- History rewriting is last resort, always revoke keys first
Limitations:
- Pattern matching isn't perfect (false positives/negatives)
- Doesn't scan files already in history
- Won't catch obfuscated secrets
When NOT to use this:
- Secrets already in history (revoke first, then clean)
- Shared repos where you can't rewrite history (coordinate cleanup)
Production Checklist
- git-secrets installed on all team machines
- Pre-commit hooks in project README
- .env.example file with fake values as template
- CI/CD secret scanning enabled
- Key rotation schedule documented
- Security policy in SECURITY.md
Advanced: Organization-Wide Setup
For teams, enforce this globally:
# Enable for ALL repos
git secrets --install ~/.git-templates/git-secrets
git config --global init.templateDir ~/.git-templates/git-secrets
# Every new repo now has git-secrets enabled
Add to team onboarding:
# .bashrc or .zshrc
alias git-init='git init && git secrets --install && pre-commit install'
Tested on Git 2.43+, git-secrets 1.3.0, pre-commit 3.6.0 Platforms: macOS 14, Ubuntu 24.04, Windows 11 (Git Bash)
Found this helpful? Share with your team before the next security incident.