Scan Git Repositories for Secrets with TruffleHog

Tested on Kali GNU/Linux Rolling 2026.2 (kali-rolling)
Package trufflehog 3.97.1
git
grep
Lab environment Pentest lab
Cert prep CompTIA PenTest+ PT0-003 — Domain 2 (Reconnaissance and Enumeration)
Privilege Normal user for scans; sudo to install TruffleHog under /usr/local/bin
Scope Local Git secret scanning with TruffleHog git and filesystem modes, git log -S history review, and grep fallbacks on a disposable lab repository. Does not cover live GitHub or GitLab API mining, CI pipeline setup, or post-finding credential abuse.
Related guides Web content discovery
SMTP and SMB enumeration
Short on time?Beta Get the gist of this 12-minute read in seconds, then ask follow-up questions

Developers paste cloud keys into config files, commit .env by mistake, and assume a later redaction removes the risk. TruffleHog helps you scan a Git repository for secrets and find secrets in Git history even when the latest tree looks clean. In penetration testing, that workflow is part of secrets enumeration — this walkthrough focuses on TruffleHog Git scanning on Kali.

Every secret in the examples below is fake lab data. Scan only repositories and paths covered by your rules of engagement.

IMPORTANT
Do not use discovered keys against production services. If you confirm a live credential during a client test, recommend revocation or rotation through the secure channel named in the RoE.

What is secret scanning in Git?

Git secret scanning hunts for API keys, cloud credentials, database connection strings, private keys, and password dumps left in source trees or commit history. The question that drives most searches is simple: “I deleted the key from the latest commit — does it still exist somewhere in the repository?”

Common leak locations:

  • Environment files — .env, .env.local, deployment YAML with inline passwords
  • Application config — JSON or XML blocks with aws_access_key_id and partner API tokens
  • Git history — secrets removed from HEAD but still reachable through older commits and object storage
  • Private keys — id_rsa, deploy keys, and PEM material committed beside application code
  • Password dumps — passwords.txt, CSV exports, and notes folders copied into the repo
  • Cloud and SaaS keys — AWS, Azure, GCP, Stripe, GitHub, Slack, and generic bearer tokens

Passive OSINT for pentesters can surface public GitHub leaks. This lesson focuses on active scanning after you legally possess a repository clone or filesystem export from the client.


TruffleHog quick start

Point TruffleHog git mode at a local repository clone. Replace the path with your authorized checkout:

bash
export REPO=/path/to/your/repo
trufflehog git "file://${REPO}" --no-verification
output
🐷🔑🐷  TruffleHog. Unearth your secrets. 🐷🔑🐷

Found unverified result 🐷🔑❓
Detector Type: Postgres
Decoder Type: PLAIN
Raw result: postgres://admin:SuperSecretLabPass99@db.internal:5432
Commit: 9a1ac9852086fad25676a8aaf6cf76cc7254fc79
Email: Lab User <lab@example.com>
File: config.json
Line: 2
Timestamp: 2026-08-31 05:57:33 +0000

The Commit and File lines tie the finding to an older revision — exactly what you need when HEAD shows REDACTED but history still contains the database URL.

TruffleHog labels each match verified or unverified:

  • Verified — TruffleHog contacted a provider or service and confirmed the credential works.
  • Unverified — the value matched a detector, but validity was not confirmed.
  • False positive / test fixture — the string resembles a secret but does not represent usable access.

Verification can make outbound requests to provider APIs, so confirm that traffic is permitted by the RoE before you enable it on client data.

The controlled lab repository below reproduces this output on Kali. Install TruffleHog first, then build $REPO under /tmp/secrets-lab/leaky-app.


Install TruffleHog on Kali Linux

Kali does not always ship TruffleHog in the default repositories. Use the official installation script from the Truffle Security project — it downloads the latest matching binary for your platform:

bash
curl -sSfL https://raw.githubusercontent.com/trufflesecurity/trufflehog/main/scripts/install.sh | sh -s -- -b /usr/local/bin

Confirm the build you installed:

bash
trufflehog --version
output
trufflehog 3.97.1

The script installs the newest release by default. To pin a specific version, append the tag: sh -s -- -b /usr/local/bin v3.97.1.

Prefer a manual install? Download the Linux amd64 archive from the TruffleHog releases page, extract trufflehog, and move it to /usr/local/bin with sudo.


Build a leaky Git lab repository

Real engagements start from a client zip export or git clone. Here you simulate a common mistake: a database URL committed in config.json, then redacted in HEAD while the original commit remains in history.

Variable Purpose
LAB Scratch directory (/tmp/secrets-lab)
REPO Git root (/tmp/secrets-lab/leaky-app)

Export the paths once per shell session:

bash
export LAB=/tmp/secrets-lab
export REPO="${LAB}/leaky-app"

Create the parent directory:

bash
mkdir -p "$LAB"

Initialize the repository and set a local Git identity:

bash
mkdir -p "$REPO" && cd "$REPO"

Turn the directory into a Git repository:

bash
git init

Set a local commit identity so history looks realistic:

bash
git config user.email "lab@example.com"

Set the display name Git attaches to commits:

bash
git config user.name "Lab User"

Add a config file with a Postgres connection string in the first commit:

bash
printf '%s\n' '{' '  "database_url": "postgres://admin:SuperSecretLabPass99@db.internal:5432/app"' '}' > config.json

Stage and commit the first revision:

bash
git add config.json

Record the commit that introduces the database URL:

bash
git commit -m "add config with database url"

Redact the URL in the working tree and add token-shaped strings plus a password list:

bash
printf '%s\n' '{' '  "database_url": "REDACTED"' '}' > config.json

Add a .env file with SaaS token placeholders:

bash
printf '%s\n' 'GITHUB_TOKEN=ghp_abcdefghijklmnopqrstuvwxyz1234567890ABCD' 'STRIPE_KEY=sk_test_4eC39HqLyjWDarjtT1zdp7dc' > .env

Create a notes folder for the password dump:

bash
mkdir -p notes

Write colon-separated credentials into notes/passwords.txt:

bash
printf '%s\n' 'admin:Winter2024!' 'deploy:changeme123' 'backup:qwertyuiop' > notes/passwords.txt

Commit the second revision:

bash
git add . && git commit -m "redact database url; add env and notes"

Review the history you just created:

bash
git log --oneline
output
d74eb67 redact database url; add env and notes
9a1ac98 add config with database url

config.json at HEAD shows REDACTED, but the first commit still stores the full connection string for TruffleHog to find.


Choose TruffleHog git mode or filesystem mode

Mode What it is for
trufflehog git Git repositories and commit history; returns commit metadata
trufflehog filesystem Directories and files; may encounter loose Git objects under .git
trufflehog github GitHub repositories and organizations (requires tokens; out of scope here)

When you need to find secrets in Git history, prefer trufflehog git. TruffleHog filesystem mode scans files beneath the path you supply. If .git is included, it may also read loose objects under .git/objects, but that is not the same as a full Git-aware history walk — packed objects and complete history semantics differ.

Current TruffleHog releases clone local repositories into a temporary directory before scanning to reduce risk from malicious local Git configuration (CVE-2025-41390). Only use --trust-local-git-config when you trust the repository.


Scan Git history with TruffleHog git mode

Git mode walks commits and attributes each finding to the revision that introduced it:

bash
trufflehog git "file://${REPO}" --no-verification
output
Found unverified result 🐷🔑❓
Detector Type: Postgres
Decoder Type: PLAIN
Raw result: postgres://admin:SuperSecretLabPass99@db.internal:5432
Commit: 9a1ac9852086fad25676a8aaf6cf76cc7254fc79
Email: Lab User <lab@example.com>
File: config.json
Line: 2
Timestamp: 2026-08-31 05:57:33 +0000

Record the commit, file path, line, and timestamp as supporting evidence for the finding. The commit hash points at the introducing revision even though HEAD shows REDACTED — the core “deleted from the tree but still in history” scenario.

To scan commits for secrets introduced after a known baseline — a recent branch, retest window, or release tag — limit the history range:

bash
trufflehog git "file://${REPO}" --since-commit HEAD~1 --no-verification

On this lab fixture, that pass reports no Postgres findings because the database URL was introduced in the parent commit, not in the latest revision alone. Scan the full repository when you need complete history coverage; use --since-commit when you only want changes after a specific commit.

Export JSON when you need machine-readable output for reporting pipelines:

bash
trufflehog git "file://${REPO}" --no-verification --json 2>/dev/null | grep '^{' | head -1

The useful fields are condensed below — the live command emits one long JSON line per finding:

output
{
  "DetectorName": "Postgres",
  "Verified": false,
  "SourceMetadata": {
    "Data": {
      "Git": {
        "commit": "9a1ac9852086fad25676a8aaf6cf76cc7254fc79",
        "file": "config.json",
        "line": 2
      }
    }
  }
}

"Verified": false is expected for lab fixtures and offline hosts.


Scan the working tree with TruffleHog filesystem mode

Filesystem mode is useful when you have a directory export without an intact .git directory, or when you want to scan config trees outside version control:

bash
trufflehog filesystem "$REPO" --no-verification
output
Found unverified result 🐷🔑❓
Detector Type: Postgres
Decoder Type: PLAIN
Raw result: postgres://admin:SuperSecretLabPass99@db.internal:5432
File: /tmp/secrets-lab/leaky-app/.git/objects/e7/665e282904bd0bd7094d5fe070d2cdbb120dd9
Line: 1

TruffleHog surfaced the connection string inside a loose Git object, not in the current config.json working tree. Prefer git mode when you need commit metadata and a deliberate history walk.


Find deleted secrets still stored in Git history

When HEAD shows REDACTED, older commits may still expose live material. git log -S (“pickaxe”) lists commits where the number of occurrences of a string changed — useful for locating when a secret was introduced or removed:

bash
git -C "$REPO" log -S 'SuperSecretLabPass99' --all --oneline
output
d74eb67 redact database url; add env and notes
9a1ac98 add config with database url

Both commits appear because the database URL was added in the first revision and removed in the second.

Inspect the revision that still contains the connection string:

bash
git -C "$REPO" show 9a1ac98:config.json
output
{
  "database_url": "postgres://admin:SuperSecretLabPass99@db.internal:5432/app"
}

TruffleHog git mode already reported the same material from config.json in commit 9a1ac98. Pickaxe queries give you a second, Git-native path when you need to prove exactly when a value entered or left the tree.


Find API keys and credentials with grep

Recursive grep is a useful fallback for organization-specific naming conventions or quick lab checks, but handcrafted regexes should not replace maintained TruffleHog detectors. Token formats change over time; use grep to find API keys in Git working trees as a lead, not as a substitute for scanner detectors.

Search for a few common indicators in the lab tree:

bash
grep -RInE 'ghp_|sk_(live|test)_' "$REPO"
output
/tmp/secrets-lab/leaky-app/.env:1:GITHUB_TOKEN=ghp_abcdefghijklmnopqrstuvwxyz1234567890ABCD
/tmp/secrets-lab/leaky-app/.env:2:STRIPE_KEY=sk_test_4eC39HqLyjWDarjtT1zdp7dc

GitHub- and Stripe-shaped strings appear in .env at HEAD even though the historical Postgres URL lives only in an older commit. Combine both approaches on engagements, but treat grep hits as leads until you confirm scope.

Search password-style dumps separately:

bash
grep -RInE '^[a-zA-Z0-9_.-]+:[^[:space:]]+$' "$REPO/notes"
output
/tmp/secrets-lab/leaky-app/notes/passwords.txt:1:admin:Winter2024!
/tmp/secrets-lab/leaky-app/notes/passwords.txt:2:deploy:changeme123
/tmp/secrets-lab/leaky-app/notes/passwords.txt:3:backup:qwertyuiop

Colon-separated user:password lines are weak credential stores and frequent lateral movement seeds.


Validate and report findings

Treat every TruffleHog and grep hit as unconfirmed until you record context and scope:

Step Action
Triage Note detector name, path, commit hash, and line number
Redact Replace live secret bytes with placeholders in your report
Verify On authorized repos with outbound access, rerun with --results=verified or --results=verified,unknown
Impact Map the secret to services — cloud consoles, payment APIs, databases
Handoff Deliver through the RoE secure channel; recommend rotation before history rewrite

For the highest-confidence output, use --results=verified. When you also want potentially valid secrets that TruffleHog cannot verify automatically, add unknown:

bash
trufflehog git "file://${REPO}" --results=verified,unknown

On the lab fixture, that pass still surfaces the unverified Postgres match. A --results=verified-only run returns nothing here — expected for synthetic strings.

If a real secret was committed, revoke or rotate it first. Rewriting Git history removes the exposed value from normal repository history but does not make a credential safe again — existing clones, forks, caches, logs, or earlier copies may still contain it.

For preventive scanning before secrets reach repository history, use TruffleHog in pre-commit or CI workflows — a separate DevSecOps topic outside this enumeration lesson.


Git secret scanning troubleshooting

Symptom Likely cause Fix
command not found: trufflehog Binary not on PATH Rerun the install script with -b /usr/local/bin
Zero findings on obvious .env Scanning wrong directory Export REPO to the Git root that contains .git
--results=verified prints nothing No live credentials in scope Expected for lab fixtures; try --results=verified,unknown
--since-commit misses an old leak Secret introduced before the baseline commit Run a full git scan or move the baseline earlier
Duplicate Postgres lines in filesystem mode Same secret in file and loose object Deduplicate in notes; prefer git mode for commit attribution
Huge JSON noise on CI runners Entire workspace scanned Point TruffleHog at the repository root only

Summary

Git secret scanning answers whether credentials survive after someone “removed” them from the latest commit. You installed TruffleHog 3.97.1 on Kali, ran git mode against a disposable repository, and watched it find secrets in Git history — a Postgres URL in config.json at commit 9a1ac98 while HEAD showed REDACTED. Filesystem mode and grep supplemented the scan; git log -S confirmed when the value was introduced and removed.

The main pitfall is trusting a single tool pass. Pair TruffleHog with pickaxe history review and manual triage, then redact before you write the report. Rotate confirmed live credentials before recommending history rewrites.

On authorized client work, run git mode against each in-scope Git repository and filesystem mode against non-Git source or config exports. Use --results=verified,unknown when the network allows verification. For broader passive collection before you possess a clone, continue with OSINT tools for pentesters.


References


Frequently Asked Questions

What is secrets enumeration in penetration testing?

Secrets enumeration hunts for API keys, cloud credentials, database URLs, private keys, and password dumps left in source trees, configuration files, or Git history. Git secret scanning with TruffleHog is one active technique you use after you legally possess a repository clone.

What does TruffleHog detect?

TruffleHog ships hundreds of detectors for cloud provider keys, SaaS tokens, database connection strings, and private keys. It can verify some findings against provider APIs when outbound access is allowed. Offline lab scans still surface unverified matches you must validate manually.

Should I use git mode or filesystem mode?

Use trufflehog git when you need commit-aware history scanning and metadata such as author and timestamp. Use trufflehog filesystem for directory exports or config trees. When .git is present and history matters, git mode is the recommended path.

What is the difference between verified and unverified results?

Verified means TruffleHog confirmed the credential against a provider or service. Unverified means the value matched a detector but was not confirmed. Use --results=verified for the strictest filter, or --results=verified,unknown when you also want matches that cannot be verified automatically.

Why did TruffleHog miss a token that grep found in the lab?

Synthetic test strings may match grep patterns but fail TruffleHog detector heuristics. Combine TruffleHog with git log -S and pattern searches, then treat every hit as a lead until you confirm scope and impact.

Repository and filesystem scanning belongs inside written authorization for the engagement. Cloning public repos without permission, exfiltrating secrets you find, or using discovered credentials outside the test scope violates most rules of engagement and many laws.

Finished reading?Beta Get the key takeaways in seconds, then ask follow-up questions
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with 15+ years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)