Safeguard
DevSecOps

Secrets Scanning: Stop Leaking Credentials Before They Ship

A leaked API key in Git history is compromised the moment it is pushed — deleting the commit does not help. Here is how to build secrets scanning across your whole lifecycle, from pre-commit to Git history.

Priya Mehta
DevSecOps Lead
7 min read

The most important fact about a leaked secret is counterintuitive: once a credential is pushed to a repository, it is compromised, and deleting the commit does not un-compromise it. Git history is distributed and cached; the secret may already be in a fork, a clone, a CI log, or a scraper's database seconds after the push. GitGuardian's annual State of Secrets Sprawl research has repeatedly found millions of new hardcoded secrets exposed on public GitHub each year, and public repos are only the visible portion — private repos leak just as freely without enforcement. The only safe response to an exposed secret is to rotate it, and the only way to avoid that scramble is to catch the secret before it ever reaches the remote.

This guide covers secrets scanning as a layered system, because a single checkpoint always has a gap.

Why hardcoded secrets are uniquely dangerous

A vulnerability in your code requires an attacker to find and exploit it. A leaked credential requires nothing — it is a working key, ready to use, often granting exactly the access it was scoped for: a cloud account, a database, a payment provider, a signing key. Secrets are also irreversible once exposed. You cannot patch a leaked AWS access key the way you patch a SQL injection; you can only revoke and reissue it, and until you do, it is live. That combination — high value, zero exploitation effort, irreversible exposure — is why secrets scanning deserves its own dedicated control rather than being folded into general static analysis.

The layered detection model

Effective secrets scanning happens at four points, each catching what the previous one missed:

LayerWhen it runsCatchesCost of a miss here
Pre-commitBefore commit is createdSecret before it enters local historyNone — nothing leaked
CI / pull requestOn push, before mergeSecret that bypassed the hookLow — not yet on main
Git history scanScheduled / on-demandSecrets already committed historicallyMedium — already exposed
Push protectionServer-side on pushSecret at the platform boundaryLow — rejected at remote

The guiding principle: the earlier a secret is caught, the cheaper the response. A pre-commit catch costs a developer thirty seconds. A Git-history catch costs a rotation, an incident ticket, and an audit. Push everything as far left as you can, but do not rely on any single layer.

Layer 1 — Pre-commit hooks

The cheapest possible catch is before the commit exists. A pre-commit hook scans staged changes locally and refuses to create the commit if it finds a credential pattern. Tools like gitleaks and trufflehog run well in this position:

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/gitleaks/gitleaks
    rev: v8.18.0
    hooks:
      - id: gitleaks

The caveat: local hooks are opt-in and bypassable (git commit --no-verify). They are a developer convenience and a first line, never the enforcement boundary. Which is why you also need server-side scanning.

Layer 2 — CI / pull request scanning

CI scanning is the enforcement layer, because it runs on infrastructure the developer cannot bypass. Scan the diff on every pull request and fail the build on a detected secret. Because a secret pushed to a branch is already on the remote, treat any CI detection as a genuine exposure: block the merge and trigger rotation, do not just ask the developer to remove the line.

# ci: block PRs that introduce secrets
secrets-scan:
  stage: security
  script:
    - safeguard scan --secrets --diff origin/main --fail-on any
  # a hit here means the secret already reached the remote — rotate it

Layer 3 — Scanning existing Git history

New-secret detection does nothing about the credentials already sitting in your history. Run a full-history scan across every repository to find what leaked before you had scanning, then rotate everything it finds. Expect false positives from example values and test fixtures — tune with an allowlist and, critically, verify whether each finding is a live credential. This is where validation matters: a scanner that checks whether a detected key still authenticates lets you prioritize the ones that are actually dangerous over the thousands of expired or fake ones.

Layer 4 — Platform push protection

GitHub and GitLab both offer server-side secret push protection that rejects a push containing a recognized credential pattern at the boundary, before it lands in the repo at all. Enable it — it is a free backstop that catches what slips past hooks and complements, rather than replaces, your own scanning. Native platform detection is broad but generic; pair it with tooling that adds validation and reachability so you are not just told "a secret exists" but "this secret is live and here is what it opens."

After detection: the response runbook

Detection without a response plan just generates anxiety. Codify the steps so any engineer can execute them:

  1. Rotate first. Revoke and reissue the credential immediately. This is the only step that actually reduces risk; everything else is cleanup.
  2. Assess exposure. How long was it live? Public or private repo? Check access logs for use during the window.
  3. Remove from history if warranted. Rewriting history (git filter-repo) is worthwhile for hygiene but is not remediation — the rotation in step 1 is.
  4. Find the root cause. Why was it hardcoded? Usually the answer is "there was no easy way to inject it as an environment variable or pull it from a secrets manager." Fix that, or it recurs.

Prevention beats detection

The durable fix is architectural: developers hardcode secrets when doing the right thing is harder than doing the wrong thing. Make the secure path the easy path — a secrets manager (Vault, cloud KMS, or your platform's encrypted CI variables) with a one-line SDK call, and OIDC federation so pipelines mint short-lived cloud credentials at runtime instead of storing static keys. A 90-day static key that only needs to live for 90 seconds should not exist. When the easy path is also the secure path, scanning becomes a safety net rather than a daily battle.

The secrets scanning checklist

  • Pre-commit hooks installed for fast local feedback
  • CI scanning that blocks merges on new secrets (the real enforcement layer)
  • Full Git-history scan completed and findings rotated
  • Platform push protection enabled as a backstop
  • Detection findings validated for whether they are live
  • A written rotate-first response runbook
  • Secrets manager + OIDC making the secure path the easy path

How Safeguard helps

Safeguard runs secrets detection across every layer from one engine: pre-commit and IDE feedback, diff-aware CLI scanning that blocks merges in CI, and full Git-history sweeps. Findings are validated and correlated by Griffin AI, so you see which detected credentials are live and prioritized rather than a wall of test-fixture noise. Secrets sit alongside your SCA and SAST findings in one prioritized queue governed by the same policy-as-code gates, so "block any new secret" is a rule you write once and enforce everywhere.

Compare unified, validated secrets scanning to platform-native detection alone in our GitHub comparison, then get started free or read the documentation.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.