Quick Answer
Deleting a secret from your code and pushing a new commit does not remove it from Git history. Every earlier commit still holds it, and anyone with access to the repository can read it. In 963 codebases VibeDoctor scanned between 31 July and 14 October 2026, 38% contained at least one string that looked like a secret. Among 336 projects connected to an account, about 1 in 18 had a real private key or secret key sitting in its current files. If you find one, rotate the key first, then scan your full history and rewrite it.
What We Find in Real Repositories
These numbers come from VibeDoctor's own scans. Every codebase scan runs Gitleaks over the files in the scanned commit. No project or owner is identified here.
| Codebases scanned, 31 July to 14 October 2026 | Projects |
|---|---|
| All scans, including free scans of public repositories | 963 |
| Flagged at least one possible secret (any severity) | 364 (37.8%) |
"Possible secret" includes test fixtures, placeholder values and publishable keys that are meant to be public. For a closer look we used the 336 projects connected to a VibeDoctor account, as of each project's most recent scan (May to October 2026):
| At the latest scan | Projects (of 336) |
|---|---|
| Any secret finding above info level | 49 (14.6%) |
| Critical: a private key or a live-format secret key | 19 (5.7%) |
Where the 19 critical secrets were (a project can appear in more than one row):
| Location | Projects |
|---|---|
| Source code | 15 |
| Config files (JSON, YAML, TOML) | 7 |
A committed .env file |
5 |
| Tests | 1 |
The Gitleaks rules behind them: generic-api-key with a secret-key prefix such as sk_ (12 projects), private-key (6), and 2 each for aws-access-token, gcp-api-key, stripe-access-token and vault-service-token.
Two things stand out. Most real secrets live in ordinary source files, not in .env files, so checking .gitignore is not enough. And every one of these keys is in a committed file, which means it is already in Git history. Removing the line in the next commit will not take it out.
Git History Is a Permanent Record
Once something is committed, Git keeps it in the object store until you explicitly rewrite history. That is what makes it safe to go back to any version of your code, and it is exactly why a committed secret is so hard to get rid of.
When a developer adds a key to a file, notices, removes it and commits again, the key still lives in the earlier commit. Anyone who runs git log -p or git show <commit>, or browses the commit list on GitHub, can read it in plain text. Every clone, fork and CI cache made in the meantime has a copy too.
How Secrets End Up in Git History
- Direct commit: a working key is written straight into a source or config file. In our data this is the most common case by far.
- Committed .env file: the
.envfile was committed before it was added to.gitignore. 5 of the 19 projects above had a real secret in one. - Debug code: a key is hardcoded "just for now" while debugging and never removed.
- AI tool generation: you paste a working key into a prompt, and Bolt, Lovable, Cursor or v0 puts it in the generated file.
- Copy-paste from documentation: a real key ends up where a placeholder should be.
Scanning Your Full History with Gitleaks
Gitleaks is a widely used open-source secret scanner. Pointed at a Git repository, it walks every commit and checks every change, not just the files you have today.
// BAD - secret committed in a past file, then deleted
// commit a3f89c2 (2 months ago)
// src/lib/openai.ts
import OpenAI from 'openai';
// Pasted in while testing - "I'll move this to env later"
const client = new OpenAI({
apiKey: 'sk-proj-EXAMPLE-NOT-REAL-KEY-abc123xyz'
});
// commit b7d12e1 (1 month ago) - "remove hardcoded key"
// The fix only removed the line. The key still lives in commit a3f89c2.
To scan the history yourself:
# Install (macOS)
brew install gitleaks
# Every commit on every branch (newer Gitleaks versions)
gitleaks git --log-opts="--all" -v .
# Same scan on older Gitleaks versions
gitleaks detect --source . --log-opts="--all" -v
# Only the files as they are now, without history
gitleaks dir -v .
Each finding in a history scan names the commit, author and date where the secret was introduced:
Finding: apiKey: 'sk-proj-EXAMPLE-NOT-REAL-KEY-abc123xyz'
Secret: sk-proj-EXAMPLE-NOT-REAL-KEY-abc123xyz
RuleID: openai-api-key
File: src/lib/openai.ts
Line: 5
Commit: a3f89c2d14f8a3b7e9c1d4f6a8b2e5c7d9f1a3b5
Author: dev@example.com
Date: 2026-01-15T10:23:41Z
Removing Secrets From Git History
Once you know which secret to remove, use git filter-repo (the tool the Git project recommends) or BFG Repo-Cleaner. Both rewrite every commit that contained the secret, which gives those commits new hashes.
This is destructive. After rewriting you must force-push, and everyone who has a clone needs to re-clone. Coordinate with your team first.
# 1. Install git-filter-repo
pip install git-filter-repo
# 2. Replace the secret with a placeholder in every commit
# replacements.txt contains one line per secret:
# sk-proj-EXAMPLE-NOT-REAL-KEY-abc123xyz==>REMOVED_SECRET
git filter-repo --replace-text replacements.txt
# 3. filter-repo removes the origin remote as a safety step; add it back
git remote add origin git@github.com:you/your-repo.git
# 4. Force push all branches and tags
git push origin --force --all
git push origin --force --tags
GitHub can still keep cached views and pull request references to the old commits. If the repository was ever public, ask GitHub Support to remove them.
Tools That Find Leaked Secrets
| Tool | Scans past commits | How it finds secrets |
|---|---|---|
| Gitleaks | Yes | Regex rules plus entropy; open source |
| TruffleHog | Yes | Provider detectors, and it can check whether a found key still works |
| GitHub secret scanning | Yes, once turned on | Provider partner patterns; free for public repositories |
| VibeDoctor | No, current files of the scanned commit | Gitleaks rules, with severity set by key type |
| grep / manual search | No | Only the patterns you think of |
How to Fix a Leaked Secret: The Complete Workflow
- Rotate immediately: in the provider's dashboard (AWS IAM, OpenAI, Stripe, GitHub), create a new credential and revoke the old one. Do this before rewriting history and assume the old key is already compromised.
- Update your environment: put the new credential in your hosting provider's secret settings (Vercel, Railway, AWS Secrets Manager) and your local
.envfile. - Rewrite history: use
git filter-repoto replace the old secret in every commit. - Force push: push the rewritten history and have the team re-clone.
- Add prevention: run Gitleaks as a pre-commit hook so the next secret is blocked before it is committed.
VibeDoctor's Vibe Check runs Gitleaks on every codebase scan and shows the file and line of each secret. Severity follows the key type: a private key or live secret key is critical, a publishable key is low, and a value in a test fixture or .env.example is only informational. It checks the files in the scanned commit, not past commits, so run the history scan above once and let VibeDoctor catch new secrets on later scans. Free to sign up.
A private repository does not make a committed key safe. In the October 2026 Zeabur breach, the attacker used admin keys that Zeabur had stored in plaintext in a private configuration repository.
Prevention: Pre-Commit Hooks and CI Checks
# .pre-commit-config.yaml (https://pre-commit.com)
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.30.1
hooks:
- id: gitleaks
# .github/workflows/secrets-scan.yml (steps)
- uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Scan for secrets
uses: gitleaks/gitleaks-action@v3
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }} # only for organization accounts
Use gitleaks-action@v3: v2 ran on Node 20, which GitHub removed from its hosted runners on 16 September 2026. A pre-commit hook stops the secret before it reaches history, which is far cheaper than the rotate-rewrite-force-push routine above. Setting up pre-commit secret scanning walks through the hook, Husky and the alternatives.
FAQ
What tools scan Git history for leaked credentials?
Gitleaks and TruffleHog are the two most used open-source options: both walk every commit, not just your current files. Gitleaks matches rules for known key formats; TruffleHog can also check whether a key it finds still works. GitHub secret scanning covers repositories hosted on GitHub once it is turned on. Run one of them with all branches included, for example gitleaks git --log-opts="--all" -v .
If I make my GitHub repo private, are old secrets safe?
No. Anyone who already cloned it has the full history locally, and automated scrapers may have read it while it was public. Private repositories are also shared with collaborators and connected services. Treat any exposed secret as compromised, whatever the repository's visibility is now.
Does GitHub's secret scanning protect me?
It helps. Secret scanning alerts on known provider patterns, and push protection blocks some secrets at push time. It is free for public repositories; private repositories need a paid GitHub plan. It will not catch custom or internal credentials that match no provider pattern.
Does VibeDoctor scan my Git history?
No. VibeDoctor runs Gitleaks on the files in the commit it scans. A secret that was deleted in an earlier commit will not show up, so run gitleaks git --log-opts="--all" . once on your repository to check the history.
What's the difference between Gitleaks and TruffleHog?
Both scan Git history. Gitleaks uses regex rules and entropy and is quick to run locally. TruffleHog has a larger set of provider detectors and can test a found credential against the provider's API to tell you whether it still works. For most small projects Gitleaks is enough.
Do I need to rewrite history if I rotate the secret immediately?
Rotating makes the exposed key useless, which is the urgent part. The old key still sits in history, though, and will keep showing up in scans and confusing reviewers. For public repositories, rewriting history and asking GitHub Support to purge cached views is the complete fix.