12.8M
secrets exposed on GitHub in 2023 -- up 28% year-over-year (GitGuardian State of Secrets Sprawl)
4 layers
of defense: pre-commit hook, GitHub push protection, CI scan, and runtime detection
90 min
time window to rotate credentials before a leaked secret is typically exploited (GitGuardian data)

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
Free daily briefing

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:

  1. Developer runs git push
  2. GitHub blocks the push and shows which file + line contains the secret
  3. 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 typeRevocation steps
AWS access keyIAM > Users > Security credentials > Deactivate key
GitHub PATSettings > Developer settings > Personal access tokens > Delete
Google Cloud keyIAM & Admin > Service accounts > Keys > Delete
Stripe keyDevelopers > API keys > Roll key
Slack tokenapi.slack.com/apps > Your app > OAuth tokens > Revoke
Database passwordALTER USER username PASSWORD 'newpassword';
.env / genericRotate 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 install in 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:

  1. Add the value to your .gitleaks.toml allowlist (preferred for recurring patterns)
  2. 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:

MetricSourceTarget
Open secret scanning alertsGitHub > Security > Secret scanningTrending to zero
Bypass rateGitHub API > bypassed alerts<5% of detections
Time to revoke after exposureIncident log<15 minutes
Repos with pre-commit hooks installedManual audit or GitHub Actions job100% of active repos
Secrets found in CI vs. pre-commitgitleaks reportsCI 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

  1. GitGuardian State of Secrets Sprawl
  2. gitleaks
  3. GitHub Secret Scanning Documentation
  4. MITRE ATT&CK: Unsecured Credentials (T1552)
  5. NIST SP 800-61 Computer Security Incident Handling Guide
  6. BFG Repo Cleaner

Free resources

25
Free download

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.

No spam. Unsubscribe anytime.

Free download

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.

No spam. Unsubscribe anytime.

Free newsletter

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.

Eric Bang
Author

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.

Free Brief

The Mythos Brief is free.

AI that finds 27-year-old zero-days. What it means for your security program.

Joins Decryption Digest. Unsubscribe anytime.

Daily Briefing

Get briefings like this every morning

Actionable threat intelligence for working practitioners. Free. No spam. Trusted by 50,000+ SOC analysts, CISOs, and security engineers.

Unsubscribe anytime.

Mythos Brief

Anthropic's AI finds zero-days your scanners miss.