Skip to content

TruffleHog: Verified Secret Detection

Lesson 7 of 8Intermediate12 min readGit Security & DevSecOps · Secret SecurityVerified: trufflehog 3.97.1 and git 2.43.0 on Ubuntu 24.04, September 2026

TruffleHog does something no pattern matcher can: it takes a candidate credential and asks the provider whether it works.

That single capability changes the shape of the output. A regex scanner produces a list of strings that look like credentials. TruffleHog produces a list separated into ones that are live and ones that are not — which is the only distinction that determines what you do next.

Terminal window
{/* A remote repository */}
trufflehog git https://github.com/OWNER/REPO
{/* A local repository note the file:// scheme */}
trufflehog git file:///path/to/repo
{/* A directory, ignoring Git */}
trufflehog filesystem /path/to/directory

Filter the output by verification status:

Terminal window
trufflehog git file:///path/to/repo --results=verified,unknown

And in CI, make a finding fail the job:

Terminal window
trufflehog git file://. --results=verified --fail

--fail exits with code 183 when results are found — a distinctive value chosen so it cannot be confused with an ordinary tool error.

Every other scanner in this cluster answers “does this look like a credential?” TruffleHog answers “is this credential real?”

It does so with detectorsover seven hundred of them — each of which knows both a credential’s shape and how to test it against its provider’s API. A candidate AWS key gets an authenticated call to AWS; a candidate GitHub token gets a call to GitHub.

The results are classified:

StatusMeaningWhat it means for you
verifiedThe provider confirmed the credential worksAn incident. Rotate now
unverifiedDetected, and verification said noProbably dead or fake. Still worth a look
unknownVerification could not be completed — an error, a network failureTreat as verified until you know better
filtered_unverifiedUnverified and would have been filtered outVisible only if you ask for it

The default output is verified and unknown, which is a sensible default: it shows you what is live plus what could not be checked, and hides the noise.

Real output, scanning a test repository containing a randomly generated, syntactically valid but entirely fake GitHub token. The Raw result value has been replaced here — the tool prints the credential in full, and publishing a well-formed token string is exactly what push protection is designed to stop:

Detector Type: Github
Decoder Type: PLAIN
Raw result: ghp_REPLACED_FOR_PUBLICATION_SEE_NOTE_BELOW
Rotation_guide: https://howtorotate.com/docs/tutorials/github/
Version: 2
Commit: ef62a6fa2d5d53bb80b9957019ce7eafdb356b11
Email: T <t@e.com>
File: secrets.cfg
Line: 1
Repository: file:///path/to/repo
Timestamp: 2026-09-01 07:01:01 +0000

Several things in that block are worth noticing.

Raw result prints the secret. TruffleHog does not redact by default. That is deliberate — you usually need the value to identify which credential it is — and it means the output is a file full of credentials. Do not attach it to a ticket, and be careful where CI logs go.

Rotation_guide links to instructions for revoking that specific credential type. A small touch that removes a real obstacle: knowing a key leaked and not knowing where to revoke it is a common reason remediation stalls.

Decoder Type: PLAIN means the value was found as-is. TruffleHog also decodes base64 and other encodings and re-scans the result, so this field tells you whether the secret was hiding.

Commit, file, line and author locate it precisely, which is what you need to scope a history rewrite.

Critically: this finding did not appear in the default output. The token is fake, verification failed, and it was classified unverified. Only asking for unverified results surfaced it:

Terminal window
trufflehog git file://$PWD/repo --results=verified,unknown,unverified

That is the tool working correctly, and it is the single most important behaviour to understand before using it — the default view is deliberately a short list of things that are real.

Every run ends with a structured summary:

finished scanning {"chunks": 4, "bytes": 233, "verified_secrets": 0,
"unverified_secrets": 0, "scan_duration": "317.403202ms",
"trufflehog_version": "3.97.1", "verification_caching": {...}}

verified_secrets is the number that matters. verification_caching reports how many verification calls were avoided by caching — relevant on a large scan, where the same credential appearing in fifty commits should not produce fifty API calls.

Human-readable output is fine for a person reading a terminal. Anything else — a CI gate, a report, a dashboard — should use --json, which emits one JSON object per finding.

The fields on each object, verified against version 3.97.1:

SourceMetadata SourceID SourceType SourceName
DetectorType DetectorName DetectorDescription DecoderName
Verified VerificationFromCache
Raw RawV2 Redacted
ExtraData StructuredData SecretParts

Three of those do most of the work:

Verified is a boolean, and it is the field every automation should branch on.

Redacted carries a partially-masked version of the value, which is what belongs in a report, a notification or anywhere a human might read it. Raw is the full credential — useful for identifying which key this is, and dangerous everywhere else.

DetectorName identifies the provider, which is how you route the finding to whoever owns that system. VerificationFromCache tells you whether the verdict came from a cached verification rather than a fresh call — worth knowing when a finding’s status looks stale.

A minimal gate that fails on live credentials without printing any of them:

Terminal window
trufflehog git file://. --results=verified --json --no-update > findings.json
count=$(jq -s 'length' findings.json)
if [ "$count" -gt 0 ]; then
jq -r '"\(.DetectorName): \(.Redacted)"' findings.json
echo "::error::${count} verified credential(s) found"
exit 1
fi

Note that it prints Redacted, never Raw. A gate that leaks the credentials it is protecting into a log everyone can read has made the situation worse, and this is the most common way that happens.

TruffleHog scans more than repositories, and the non-Git sources are frequently where credentials actually sit.

CommandScans
trufflehog gitA Git repository, local or remote, including history
trufflehog githubRepositories, organisations and users on GitHub
trufflehog filesystemFiles and directories
trufflehog dockerContainer images, layer by layer
trufflehog s3Object storage buckets

docker is the one worth adopting deliberately. Credentials baked into an image at build time are invisible to every Git-based scanner, survive in published layers, and are readable by anyone who can pull the image. See Preventing .env commits for how they get there.

A full history scan on every pull request is slow and reports findings the author did not introduce. Scope it to the commits under review:

Terminal window
trufflehog git file://. --since-commit "${BASE_SHA}" --branch "${HEAD_REF}"

What it doesScans only the commits between two revisions.

Why we run itA pull request check should report what this change introduced, not what the repository has always contained.

Expected resultFindings only from commits in the range, and a much shorter scan.

Pair it with a scheduled full scan. The pull request check keeps new credentials out; the scheduled scan covers everything the range-limited check never looked at.

The official action:

name: secret scan
on:
pull_request:
schedule:
- cron: "0 5 * * *"
jobs:
trufflehog:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- uses: trufflesecurity/trufflehog@v3
with:
extra_args: --results=verified,unknown

fetch-depth: 0 is required. A shallow clone has no history, so a history scan finds nothing and the check passes — a green tick that means nothing was looked at.

Running the binary directly gives you version control over the scanner itself:

- name: Install TruffleHog
run: |
curl -sSL "https://github.com/trufflesecurity/trufflehog/releases/download/v${VERSION}/trufflehog_${VERSION}_linux_amd64.tar.gz" \
| tar -xz trufflehog
sudo mv trufflehog /usr/local/bin/
env:
VERSION: 3.97.1
- name: Scan
run: trufflehog git file://. --results=verified --fail --no-update

--no-update stops the binary checking for a newer version at run time, which you want when the version is pinned deliberately.

Full-history verification on a large repository is slow, and most of the cost is verification calls rather than reading. Four flags control the scope.

FlagEffect
--since-commitStart from a commit rather than the beginning of history
--branchScan one branch instead of everything reachable
--max-depthLimit how many commits back to go
--bareScan a bare repository, useful for a mirror clone

And two that control which detectors run at all:

Terminal window
{/* Only the providers you actually use */}
trufflehog git file://. --include-detectors="github,aws,gcp,slack"
{/* Or everything except a noisy one */}
trufflehog git file://. --exclude-detectors="uri"

--include-detectors is worth considering for a blocking check. Running seven hundred detectors when your organisation uses six providers spends most of the scan on formats you will never issue, and the long tail of generic detectors is where unverified noise comes from.

The trade-off is real: excluding a detector means never finding that credential type, including one that arrives because somebody adopted a new service without telling you. The usual resolution is a narrow detector set for the fast pull request gate and the full set for the scheduled scan.

TruffleHog traverses archives, controlled by --archive-max-depth, --archive-max-size and --archive-timeout. That matters because a credential inside a .jar, a .tar.gz or a build artefact is invisible to a scanner that treats those files as opaque.

The container source is the same idea applied to images:

Terminal window
trufflehog docker --image=ghcr.io/YOUR_ORG/YOUR_IMAGE:TAG

This scans each layer, which finds credentials baked in at build time — a category no Git-based scanner can see, because the credential never entered the repository. It got in through a build context, a RUN step that fetched configuration, or a copied .env.

If you publish container images, this scan is worth running against a published tag rather than only against a locally built one. What you build and what you published can differ.

Three flags matter when the output is too long.

--results is the primary control. Start with verified alone for a blocking check, and widen to verified,unknown for a scheduled scan.

--filter-entropy filters unverified results below a Shannon entropy threshold, which removes low-randomness matches — the class that is usually a constant or an identifier rather than a credential.

--filter-unverified outputs only the first unverified result per chunk, which collapses the case where the same non-credential matches repeatedly.

The order to reach for them: fix the --results filter first, because it is a statement about what you care about. Reach for entropy filtering only when a genuinely noisy unverified stream is worth keeping rather than discarding.

Neither is universally better. They fail differently, which is the argument for running both.

GitLeaksTruffleHog
MechanismRegex rules plus entropyDetectors plus live verification
Key question“Does this look like a credential?”“Is this credential working?”
SpeedFast, entirely localSlower — verification makes network calls
OfflineYesVerification requires network access
False positivesHigherMuch lower when filtered to verified
False negativesFormats with no ruleProviders with no detector
Custom formatsStraightforward TOML rulesMore involved
Output safety--redact built inPrints raw secrets by default
Non-Git sourcesFiles and stdinContainers, object storage, GitHub organisations

A split that works well in practice:

GitLeaks in the pull request gate and the pre-commit hook. Fast, offline, no network calls, easy to extend with your organisation’s own token format.

TruffleHog on a schedule, and for incident work. When there is a finding and you need to know whether it is live, verification answers in seconds what a person would spend an afternoon establishing.

The redundancy is the point. A credential format GitLeaks has a rule for and TruffleHog has no detector for is caught by one; a base64-encoded credential TruffleHog decodes and verifies is caught by the other.

The situation where TruffleHog earns its place decisively is triage after a finding, and it is worth walking through because the workflow is different from routine scanning.

You have an alert — from GitHub secret scanning, from GitLeaks, or from somebody noticing. The urgent question is not “where is it” but “does it still work?”, because the answer determines whether this is a rotation or a cleanup.

  1. Scan the affected repository including unverified results, so you see everything rather than just the live set:

    Terminal window
    trufflehog git file://. --results=verified,unknown,unverified --no-update --json > findings.json

    Use --json, and write to a file rather than to a terminal somebody is sharing.

  2. Extract the verified findings. Those are the incidents:

    Terminal window
    jq -r 'select(.Verified == true) | .DetectorName' findings.json | sort | uniq -c
  3. Treat unknown as verified. A credential whose verification errored is not a credential you have cleared.

  4. Rotate everything in categories 2 and 3, following rotating exposed credentials.

  5. Re-scan after rotation. Previously verified findings should now come back unverified. That is the confirmation that the rotation took effect — a better check than reading a settings page.

Step 5 is the one that turns this from a scanner into a verification tool. Nothing else in this cluster will tell you, positively, that the credential you rotated is now dead.

Assuming the default output is everything. Unverified results are hidden by default. A fake or revoked credential in your repository will not appear unless you ask.

Treating unknown as safe. It means verification failed to complete — a network error, a rate limit, a provider outage. It is closer to verified than to unverified.

Letting raw output reach a CI log. The value is printed unredacted, and the log is visible and retained.

Shallow clones in CI. No history to scan, and a passing check.

Using @main because the README does. It is a moving reference executing in a job with your credentials.

Running verification against an environment that alerts on unfamiliar authentication. Tell the people who watch those alerts before you scan.

Using it as the only scanner. No detector means no finding, and detectors cover the providers somebody implemented.

TruffleHog is a detector plus a phone call. The detector finds candidates the way any scanner does; the phone call to the provider is what turns “this looks like a credential” into “this credential works” — and that is the fact that determines whether you have an incident.

  • Verification distinguishes live credentials from dead strings, which is the triage split that matters
  • Results are classified verified, unverified, unknown and filtered_unverified; the default shows verified and unknown
  • unknown means verification could not complete and should be treated as live
  • Verification authenticates with the candidate credential, which the provider sees and may log
  • Output prints the raw secret by default, so CI logs need care
  • --fail exits with code 183, distinct from ordinary tool errors
  • file:// is required for a local repository path
  • It scans containers, object storage and whole GitHub organisations, not only Git
  • The project’s README shows @main; pin to a tag or SHA instead
  • GitLeaks and TruffleHog fail differently, which is why running both is not redundant

Use a disposable repository and a randomly generated fake token.

  1. Commit a file containing a ghp_ prefix followed by 36 random alphanumeric characters.

  2. Run trufflehog git file://$PWD --no-update. Predict: does it report anything?

  3. Run it again with --results=verified,unknown,unverified. Predict: what changes, and why?

  4. Read the finding. Note that Raw result shows the full value and that a rotation guide is linked.

  5. Run with --no-verification. Predict: how does the classification change, and how does the scan duration compare?

  6. Add --fail and check echo $?. Predict: what exit code?

  7. Scan the same repository with GitLeaks. Predict: do the two agree on this finding? The difference is the lesson.

  8. Delete the repository.

GitHub Actions Security ChecklistToken permissions, fork pull requests, script injection and supply chain — with the attack each item prevents.

The secrets management checklist and least-privilege token guide are in the Professional Toolkit.