Skip to main content

Blog / AppSec basics

Secret scanning: how to find and fix leaked keys in your Git repositories

Leaked credentials are one of the fastest ways from a repository to a breach. Here is how secrets end up in Git, why removing the line does not remove the risk, how to scan code and history, and a response plan for when you find one.

Key takeaways

  • Secrets leak into Git through example env files, test fixtures, CI configuration, notebooks, debug code and, increasingly, AI assistants that inline a key to make code run.
  • Deleting a secret in a new commit does not remove it. It stays in history, in clones and forks, and in any cache that saw it. Once pushed, treat it as exposed.
  • The response order is: revoke or rotate first, then check for use, then clean history if you must. Never the other way round.
  • Scan in three places: before commit, on every pull request, and across the full history of every repository, including the old ones.
  • The lasting fix is fewer long-lived secrets: a secrets manager for applications and short-lived OIDC credentials for CI.

What secret scanning is

Secret scanning searches source code, configuration and version-control history for credentials that should not be there: API keys, access tokens, passwords, private keys and connection strings with embedded passwords. Scanners combine patterns for known credential formats (many providers use recognisable prefixes) with entropy checks and context to find generic secrets, then filter out placeholders and examples.

It matters because a leaked credential needs no exploit. Whoever finds it simply uses it, with whatever access it grants: your cloud account, your payment provider, your database, your AI provider's billing. Automated tools watch public repositories continuously for exactly this, so on a public repository the window between push and abuse can be very short.

How secrets end up in Git

  • Example and local env files. A .env committed before .gitignore was set up, or an .env.example filled with real values "just for now".
  • Test fixtures and scripts. A real token pasted into a test or a one-off migration script that was never meant to be committed.
  • CI and infrastructure files. Credentials written directly into pipeline YAML, Terraform variables or Kubernetes manifests instead of a secret store.
  • Notebooks and logs. Jupyter notebooks keep cell output, and debug logs committed by mistake often contain headers and tokens.
  • Browser-exposed configuration. Keys placed in front-end environment variables (such as Next.js NEXT_PUBLIC_ or Vite VITE_) end up in the JavaScript every visitor downloads.
  • AI coding assistants. Asked to make something work, an assistant may inline a key from context. Read generated code before accepting it; our vibe coding security checklist covers this.

Why deleting the line is not enough

Git stores every version of every file. Removing a key in a new commit hides it from the current files, but anyone with the repository can still read it from history. It also survives in every clone, in forks, in pull request diffs on your Git host, in CI logs and caches, and in any mirror or backup.

Anyone with a clone can search history for a removed key
# Every commit, on every branch, that added or removed the string
git log --all -p -S 'AKIA'

# Every past version of a file that has since been deleted
git log --all --full-history -- config/.env

So the rule is simple: once a secret has been pushed, treat it as compromised. Rewriting history is cleanup, not remediation.

What to do when you find a leaked secret

Work in this order. Most of the damage in real incidents comes from doing step 4 first and step 1 late.

  1. Revoke or rotate the credential now. Issue a new one, deploy it, and disable the old one at the provider. This is the only step that actually closes the exposure.
  2. Check whether it was used. Review the provider's access logs, billing and audit trail for activity you do not recognise between the first push and the revocation.
  3. Find every copy. Search other repositories, branches, CI variables and logs for the same value; one key is rarely in only one place.
  4. Clean history if you need to, for example in a public repository or before open-sourcing. Do it only after rotation, coordinate with everyone who has a clone, and ask your Git host to purge cached views if needed.
  5. Fix the cause. Move the secret to a secrets manager or environment injection, add the file to .gitignore, and add scanning so it cannot happen silently again.
Removing a value from history after rotation, with git filter-repo
# replacements.txt holds one literal per line, for example:
#   OLD-SECRET-VALUE==>REMOVED
git filter-repo --replace-text replacements.txt

# Rewritten history must be force-pushed, and every collaborator must re-clone.
git push --force --all && git push --force --tags

Where to scan, and what each layer catches

Secret scanning layers
LayerCatchesMisses
Pre-commit hook on the developer machineSecrets before they ever reach a shared remoteAnything committed by someone without the hook installed
Push protection on the Git hostRecognised token formats at push timeGeneric passwords and formats the host does not know
Pull request scanningNew secrets in the change under review, visible to reviewersSecrets already in history before scanning started
Full history scan of every repositoryOld leaks in branches and commits nobody looks at any moreNothing in Git, but it needs to run regularly as repositories change
Scanning CI logs and build artifactsSecrets printed by scripts or baked into imagesUsually needs separate tooling

Git hosts offer built-in options too. GitHub, for example, provides secret scanning and push protection; on public repositories it is free, while on private repositories it is a paid add-on. Built-in host scanning focuses on recognised partner token formats, so a dedicated scanner that also looks for generic secrets and scans every provider you use is a useful second layer.

Prevent leaks with fewer long-lived secrets

Scanning finds leaks. Reducing the number of long-lived secrets makes each leak matter less.

  • Use a secrets manager for application credentials and inject them at runtime, rather than storing them in files that sit next to code.
  • Use short-lived credentials in CI. Most CI systems can exchange an OIDC identity token for temporary cloud credentials, so there is no stored cloud key to leak.
  • Scope every credential narrowly (one service, the minimum permissions, an expiry where the provider supports it).
  • Keep .env files out of Git with .gitignore, and commit an .env.example with obvious placeholders only.
GitHub Actions: temporary AWS credentials through OIDC instead of a stored access key
permissions:
  id-token: write   # allow the job to request an OIDC token
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/deploy-from-ci
          aws-region: us-east-1
      - run: ./deploy.sh

Secret scanning with CodeStax

CodeStax runs secret detection on every repository scan, on every plan including Free, alongside SAST, SCA, IaC, container and AI attack-surface analysis. It looks for API keys, passwords, OAuth tokens and personal access tokens, JWTs, private keys and database URLs with embedded credentials, in the code and in git history, and filters out placeholders, test files and example files to cut noise. Secrets in a change also surface in AI pull request review, so reviewers see them before merge. It works across GitHub, GitLab and Bitbucket.

Common questions

What is secret scanning?

Secret scanning searches code, configuration and version-control history for credentials such as API keys, tokens, passwords and private keys, so they can be revoked and removed before someone misuses them.

If I delete a leaked key from my repository, am I safe?

No. The key remains in Git history, in every clone and fork, and possibly in CI logs and caches. Revoke or rotate it at the provider first; cleaning history afterwards is optional cleanup, not a fix.

How do I find secrets in Git history?

Run a secret scanner over the full history of the repository rather than only the current files. For a quick manual check, git log --all -p -S with a known prefix shows every commit that added or removed that string.

Does GitHub scan for secrets?

Yes. GitHub offers secret scanning and push protection, free on public repositories and a paid add-on for private ones. It focuses on recognised token formats from partner providers, so many teams add a scanner that also finds generic secrets and covers other Git hosts.

How do I stop secrets leaking in the first place?

Use a secrets manager, short-lived OIDC credentials in CI, narrowly scoped keys, .gitignore for env files, and scanning before commit and on every pull request.

Your next step

Scan a repository and its history for leaked secrets.

Connect a repository on the Free plan. Secret detection runs on every scan, alongside code and dependency checks.

Get started free