Stop Secrets Leaking to GitHub: Pre-commit Hooks, Push Protection, and What to Do When It Happens Anyway
Telling developers not to commit secrets does not work. GitGuardian found 12.8 million secrets exposed on GitHub in 2023 -- up 28% year-over-year. The answer is making secret leakage technically difficult rather than relying on discipline. This guide covers four layers: a gitleaks pre-commit hook that runs before a commit is created, GitHub push protection that blocks at the network layer, a CI scan that catches anything that slips through, and a 90-minute IR playbook for when a secret reaches the remote repository anyway.
Layer 1: Pre-commit Hook with gitleaks
A pre-commit hook runs locally before the commit is finalized. The developer never sees a rejected push -- the secret is caught before it enters Git history.
Install pre-commit and gitleaks:
# macOS
brew install pre-commit gitleaks
# Linux
pip install pre-commit
# gitleaks: download binary from https://github.com/gitleaks/gitleaks/releases
curl -sSL https://github.com/gitleaks/gitleaks/releases/download/v8.18.4/gitleaks_8.18.4_linux_x64.tar.gz | tar -xz
sudo mv gitleaks /usr/local/bin/
Create .pre-commit-config.yaml in your repo root:
# .pre-commit-config.yaml
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.4
hooks:
- id: gitleaks
name: Detect hardcoded secrets
description: Detect secrets using gitleaks
entry: gitleaks protect --verbose --redact --staged
language: golang
pass_filenames: false
Install hooks in every developer's local repo:
pre-commit install
Add to your project README and onboarding script:
# setup.sh
pip install pre-commit # or: brew install pre-commit
pre-commit install
echo "pre-commit hooks installed"
Test it works:
echo 'AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY' > test-secret.txt
git add test-secret.txt
git commit -m 'test'
# Should output: WARN[0000] leaks found: 1
# And block the commit
git restore test-secret.txt
rm test-secret.txt
Customizing gitleaks Rules for Internal Secrets
gitleaks ships with patterns for common secrets (AWS, GCP, Stripe, GitHub, Slack, etc.). For internal secrets -- your own API tokens, internal service credentials -- add a custom rules file.
Create .gitleaks.toml in your repo root:
# .gitleaks.toml
title = "Custom gitleaks config"
[extend]
# Use the default rules as a base
useDefault = true
[[rules]]
id = "internal-api-token"
description = "Internal API token (format: ipt_[32 alphanumeric chars])"
regex = '''ipt_[a-zA-Z0-9]{32}'''
tags = ["internal", "api"]
[[rules]]
id = "internal-db-password"
description = "Database connection string with password"
regex = '''(postgresql|mysql|mongodb)://[^:]+:[^@]{8,}@'''
tags = ["database"]
# Allow specific patterns that are false positives in your codebase
[[allowlists]]
description = "Allowlisted paths"
paths = [
'''.gitleaks.toml''', # the config file itself
'''tests/fixtures/.*''', # test fixtures with example values
]
[[allowlists]]
description = "Allowlisted patterns (test/example values)"
regexes = [
'''EXAMPLE_KEY_DO_NOT_USE''',
'''your-api-key-here''',
]
Update the hook to use this config:
- id: gitleaks
entry: gitleaks protect --verbose --redact --staged --config=.gitleaks.toml
Briefings like this, every morning before 9am.
Threat intel, active CVEs, and campaign alerts, distilled for practitioners. 50,000+ subscribers. No noise.
Layer 2: GitHub Push Protection
Push protection intercepts secrets at the remote. Even if a developer bypasses or hasn't installed the local hook, push protection blocks the push.
Enable organization-wide (preferred):
GitHub Organization > Settings > Code security and analysis
> Secret scanning > Push protection: Enable for all repositories
Enable per-repository:
Repository > Settings > Security > Code security and analysis
> Secret scanning > Push protection: Enable
What happens when a secret is detected:
- Developer runs
git push - GitHub blocks the push and shows which file + line contains the secret
- Developer must either: remove the secret and re-commit, or click through a bypass reason
Monitor bypass events (weekly):
Organization > Security > Secret scanning > Bypassed alerts
Or use the GitHub API:
curl -H "Authorization: Bearer $GH_TOKEN" \
"https://api.github.com/orgs/YOUR_ORG/secret-scanning/alerts?state=open&resolution=used_in_tests,false_positive,wont_fix" \
| jq '.[] | {repo: .repository.name, secret_type: .secret_type, bypassed_at: .updated_at, url: .html_url}'
Set up a GitHub Actions workflow to notify your security channel when a bypass occurs:
# .github/workflows/secret-bypass-alert.yml
name: Alert on Secret Scanning Bypass
on:
schedule:
- cron: '0 9 * * 1' # Every Monday 9am UTC
jobs:
check-bypasses:
runs-on: ubuntu-latest
steps:
- name: Check for bypassed secret alerts
run: |
count=$(curl -s -H "Authorization: Bearer ${{ secrets.GH_TOKEN }}" \
"https://api.github.com/orgs/${{ vars.ORG_NAME }}/secret-scanning/alerts?state=open" \
| jq 'length')
echo "Open secret scanning alerts: $count"
# Integrate with Slack/Teams webhook for notification
Layer 3: gitleaks in CI (Catch What Slips Through)
The CI scan runs on every PR and catches any secret that made it past the local hook and push protection (e.g., in a repository where hooks weren't installed, or in a file type push protection doesn't scan).
GitHub Actions workflow:
# .github/workflows/secrets-scan.yml
name: Secret Scanning
on:
pull_request:
push:
branches: [main, master]
jobs:
gitleaks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Full history required for gitleaks
- name: Run gitleaks
uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
args: detect --source . --config .gitleaks.toml --verbose
For PRs, scan only the changed commits:
- name: Run gitleaks on PR changes only
if: github.event_name == 'pull_request'
uses: gitleaks/gitleaks-action@v2
with:
args: detect --source . --log-opts "${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}" --verbose
Scan results as PR comments: The gitleaks-action automatically comments on PRs when secrets are found. Developers see the specific file, line, and rule that triggered -- actionable, not just a failed job.
Layer 4: When a Secret Reaches the Remote -- 90-Minute Incident Response
The moment a secret is pushed to a remote repository, assume it has been seen. GitHub repositories are scraped by automated tools within minutes of a push -- even private repositories if the token is exposed.
Minute 0-5: Confirm and contain
# Confirm the secret type (what can it access?)
git log --all -p | grep -A2 -B2 'SECRET_VALUE'
# Check if the commit is public or in a private repo
gh api repos/ORG/REPO --jq '.private'
Minute 5-15: Revoke the credential immediately
Do this BEFORE cleaning history. A revoked credential is harmless even if someone has already copied it.
| Credential type | Revocation steps |
|---|---|
| AWS access key | IAM > Users > Security credentials > Deactivate key |
| GitHub PAT | Settings > Developer settings > Personal access tokens > Delete |
| Google Cloud key | IAM & Admin > Service accounts > Keys > Delete |
| Stripe key | Developers > API keys > Roll key |
| Slack token | api.slack.com/apps > Your app > OAuth tokens > Revoke |
| Database password | ALTER USER username PASSWORD 'newpassword'; |
| .env / generic | Rotate in your secrets manager, update all services |
Minute 15-30: Check for exploitation
# AWS: check CloudTrail for activity since the secret was committed
aws cloudtrail lookup-events \
--start-time $(date -d '24 hours ago' --iso-8601=seconds) \
--lookup-attributes AttributeKey=Username,AttributeValue=COMPROMISED_USER \
| jq '.Events[] | {time: .EventTime, action: .EventName, ip: .CloudTrailEvent | fromjson | .sourceIPAddress}'
# GitHub: check the token's audit log before revocation
gh api /orgs/ORG/audit-log?phrase=actor:USERNAME --jq '.[] | {action, created_at, actor}'
Minute 30-60: Remove from Git history
# BFG Repo Cleaner (faster than git filter-branch)
brew install bfg
# Create a file with the secret value to replace
echo 'ACTUAL_SECRET_VALUE' > secrets-to-remove.txt
# Clone a fresh copy (BFG requires a clean clone)
git clone --mirror https://github.com/ORG/REPO.git
cd REPO.git
# Replace the secret with REMOVED in all history
bfg --replace-text ../secrets-to-remove.txt
# Clean and force push
git reflog expire --expire=now --all
git gc --prune=now --aggressive
git push --force
Minute 60-90: Verify and document
# Confirm secret is gone from all branches
git log --all -p | grep -c 'ACTUAL_SECRET_VALUE'
# Should output: 0
# Verify on GitHub
gh api /repos/ORG/REPO/git/refs --jq '.[] | .ref'
# Check main + all branches
Document in your incident log: what the secret was, when it was committed, when revoked, whether exploitation evidence was found, and what remediation was applied.
Developer Communication Template
When rolling out these controls, send this to engineering leads:
Subject: New secret scanning controls rolling out [DATE]
We're adding automated secret scanning to prevent accidental credential exposure. Here's what's changing and what you need to do:
What's new:
- GitHub push protection is now enabled organization-wide. If you push a file containing an AWS key, API token, or other credential pattern, the push will be blocked with an explanation.
- Pre-commit hooks with gitleaks are now part of project setup. Run:
pre-commit installin any repository where you're working.
If you get a false positive: If you're intentionally pushing example/test values and get blocked, you have two options:
- Add the value to your
.gitleaks.tomlallowlist (preferred for recurring patterns) - Click 'Bypass' in the GitHub UI and select the appropriate reason
All bypasses are reviewed weekly by the security team. If you have a legitimate reason, that's fine -- we just want visibility.
Questions? [security-team Slack channel]
Metrics to Track Progress
Set these up in your first week and review monthly:
| Metric | Source | Target |
|---|---|---|
| Open secret scanning alerts | GitHub > Security > Secret scanning | Trending to zero |
| Bypass rate | GitHub API > bypassed alerts | <5% of detections |
| Time to revoke after exposure | Incident log | <15 minutes |
| Repos with pre-commit hooks installed | Manual audit or GitHub Actions job | 100% of active repos |
| Secrets found in CI vs. pre-commit | gitleaks reports | CI findings trending toward zero (pre-commit catching earlier) |
Monthly audit query (list repos without push protection):
gh api /orgs/ORG/repos --paginate \
| jq '.[] | select(.security_and_analysis.secret_scanning_push_protection.status != "enabled") | .full_name'
The bottom line
Secrets in Git is a solvable problem with layered technical controls -- not a training problem. Pre-commit hooks catch secrets before commit, push protection catches them before remote, CI scanning catches anything that slips through, and the IR playbook limits damage when something reaches the remote. The 90-minute window between exposure and exploitation is real: build the revocation habit before you need it.
Frequently asked questions
Does GitHub push protection catch all secrets?
No. GitHub push protection detects secrets from a fixed list of high-confidence patterns (AWS keys, GitHub tokens, Stripe keys, and ~100 others). It misses: custom API keys with non-standard formats, internal service credentials, private keys without recognizable headers, and secrets embedded in binary or encoded content. This is why pre-commit hooks (which you configure) and CI scanning (which you tune) are essential complements.
What is the fastest way to set up secret scanning without changing developer workflow?
Enable GitHub push protection at the repository or organization level -- it requires no developer tooling changes. Go to: Settings > Code security and analysis > Secret scanning > Push protection: Enable. This blocks commits containing known secret patterns at the push layer without any local setup. Follow up with pre-commit hooks for defense in depth.
Can developers bypass GitHub push protection?
Yes. Push protection shows developers a warning and requires them to confirm they have a reason to push (e.g., 'used in tests', 'false positive', 'fix will be applied later'). All bypasses are logged and visible to security teams at: Security > Secret scanning > Bypassed alerts. Set up a weekly review of bypass events to catch misuse.
How do I remove a secret from Git history after it has been committed?
Use BFG Repo Cleaner (faster and safer than git filter-branch). Install: brew install bfg. Create a passwords.txt file with the secret. Run: bfg --replace-text passwords.txt. Then: git reflog expire --expire=now --all && git gc --prune=now --aggressive. Force push: git push --force. Immediately rotate the credential -- assume it was seen the moment it was pushed, regardless of how quickly you removed it.
What secrets should I scan for beyond API keys and passwords?
Scan for: private keys (RSA, EC, PGP), JWT signing secrets, OAuth client secrets, database connection strings (including embedded username/password), cloud provider credentials (AWS, GCP, Azure), internal service tokens, certificate private keys, .env files (which often contain multiple secrets), and Kubernetes kubeconfig files. gitleaks's default ruleset covers most of these; review the rules.toml to understand what's included.
What's the right way to store secrets in a repository for local development?
Use a .env.example file with placeholder values committed to the repo. Each developer copies it to .env (which is in .gitignore) and populates it from a secrets manager like 1Password CLI, AWS Secrets Manager, HashiCorp Vault, or Doppler. Never commit .env. Use a tool like direnv to automatically load environment variables from .env when you cd into the project directory.
How do I roll out pre-commit hooks to a team that resists tooling changes?
Frame it as saving them from an incident, not adding friction. Show a real example of a leaked secret incident (intern accidentally commits AWS key, S3 bucket scraped within 4 minutes -- this is a documented real pattern). Make installation a one-command setup in the README. Use pre-commit install as part of your project onboarding script. Start with detect-secrets in audit mode (no blocking) so developers see findings without being blocked, then move to blocking after 2 weeks.
Sources & references
Free resources
Critical CVE Reference Card 2025–2026
25 actively exploited vulnerabilities with CVSS scores, exploit status, and patch availability. Print it, pin it, share it with your SOC team.
Ransomware Incident Response Playbook
Step-by-step 24-hour IR checklist covering detection, containment, eradication, and recovery. Built for SOC teams, IR leads, and CISOs.
Get threat intel before your inbox does.
50,000+ security professionals read Decryption Digest for early warnings on zero-days, ransomware, and nation-state campaigns. Free, weekly, no spam.
Unsubscribe anytime. We never sell your data.

Founder & Cybersecurity Evangelist, Decryption Digest
Cybersecurity professional with expertise in threat intelligence, vulnerability research, and enterprise security. Covers zero-days, ransomware, and nation-state operations for 50,000+ security professionals weekly.
The Mythos Brief is free.
AI that finds 27-year-old zero-days. What it means for your security program.
