Skip to content
Go back

Pre-Commit Hooks That Catch Secrets

By KingPin 14 min read
Pre-Commit Hooks That Catch Secrets
Contents

You Pasted the Token Into config.js at 2 AM

It’s 2 AM. Your self-hosted Renovate runner refuses to authenticate. You have been staring at RENOVATE_TOKEN and RENOVATE_GITHUB_COM_TOKEN for an hour. You paste both values straight into config.js “just to test”, the runner springs to life, and you run git add -A && git commit -m "fix renovate" before your brain catches up.

Then you push. The CI runner clones the repo. A mirror syncs it. Someone’s fork pulls the new branch. A scraper that watches public commit feeds grabs it in under a minute. You notice at 9 AM and delete the file in a new commit, which feels like closing the barn door while admiring the horse’s new life elsewhere.

Renovate is not the villain here. Any tool that needs tokens invites this mistake, and a self-hosted bot needs two of them. The point is that a secret which reaches git push is already gone. The only place a leak is still free to fix is the pre-commit hook on your own machine. My default pick for that hook is gitleaks. TruffleHog is the upgrade when you want proof that a credential is live. detect-secrets is the third option for people already living in Python-land.

Full example: Clone the working files at github.com/KingPin/sumguy-examples/devops/pre-commit-secret-scanning

Why “I’ll delete it in the next commit” Does Not Work

Git keeps history. A later commit that removes the token leaves the old commit intact, and every clone carries every commit. Renovate opening PRs, CI runners printing logs, Dependabot, mirrors, and forks all take full copies.

GitHub’s server-side secret scanning fires alerts after the push lands. Push protection blocks at the push boundary, but only for the token patterns the provider supports. A hook on your machine runs before the commit object exists, so there is nothing to clean up.

If it already happened, rotate the credential. Do it now. Rewriting history with git filter-repo makes the repo look clean, but it does not un-leak anything a bot or fork already cloned. Rotate first, scrub later (if you scrub at all).

The 3-Minute Setup: pre-commit Framework

All three scanners plug into the pre-commit framework (v4.6.2 as of October 2026). Install it, then wire it into the repo.

Terminal window
pipx install pre-commit # or: pip install pre-commit
pre-commit install # writes .git/hooks/pre-commit
pre-commit autoupdate # bumps rev: pins to the latest tags
pre-commit run --all-files # scan the whole tree once

Here is a .pre-commit-config.yaml with gitleaks, TruffleHog, and a few basics from pre-commit-hooks v6.0.0. In practice you run one scanner, not both. I list both so you can compare them on your own repo.

.pre-commit-config.yaml
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- id: check-added-large-files
- id: detect-private-key
- id: end-of-file-fixer
- repo: https://github.com/gitleaks/gitleaks
rev: v8.30.1
hooks:
- id: gitleaks
- repo: https://github.com/trufflesecurity/trufflehog
rev: v3.99.0
hooks:
- id: trufflehog

detect-private-key is a cheap tripwire for PEM headers. It overlaps with the scanners, and that is fine. Cheap and redundant beats expensive and missing.

Gitleaks: The Fast Offline Gate

Gitleaks is a Go binary that matches your staged changes against a set of regex rules. No network calls, no accounts. Version 8.30.1 came out in March 2026.

The pre-commit repo ships three hook ids:

The hook runs gitleaks git --pre-commit --redact --staged --verbose with pass_filenames: false. It looks only at what you staged, and --redact keeps the secret out of your terminal scrollback. A failing run looks like this:

Detect hardcoded secrets.................................................Failed

Tuning gitleaks without breaking a sweat

False positives are the tax you pay for any scanner. Gitleaks gives you four ways to quiet them, from narrow to wide.

  1. Inline: add a gitleaks:allow comment on the line. Pass --ignore-gitleaks-allow if you want CI to ignore those comments and report everything.
  2. Fingerprint: list a finding in .gitleaksignore. A fingerprint looks like <commit>:<path>:<rule-id>:<line>, and for staged changes that have no commit yet it drops the first field (config.env:github-pat:1). The -i, --gitleaks-ignore-path flag (default .) points at the file.
  3. Config: a .gitleaks.toml that extends the defaults and adds an allowlist.
  4. Skip once: SKIP=gitleaks git commit -m "...". Use this rarely and on purpose.

Here is a config that keeps the built-in rules and quiets one noisy rule for docs and a marker string. Since v8.25.0 the global [allowlist] became [[allowlists]], and since v8.21.0 a rule’s [rules.allowlist] became [[rules.allowlists]].

.gitleaks.toml
[extend]
useDefault = true
[[rules]]
id = "generic-api-key"
[[rules.allowlists]]
regexTarget = "line"
regexes = ['''EXAMPLE_TOKEN_DO_NOT_USE''']
paths = ['''docs/.*\.md$''']

Allowlist fields you can use: description, condition (OR by default, or AND), commits, paths, regexes, regexTarget (secret by default, or match, or line), and stopwords, which target the extracted secret. Note the AND condition: with it, a finding must satisfy every filter in the block before it is ignored. With the default OR, any single match is enough.

The exit code is 1 on a finding by default. Change it with --exit-code if a pipeline needs something else.

Is gitleaks dead?

No. As of October 2026 the README carries a warning: gitleaks is feature complete, no new features will be merged, and future releases are security patches only. The author’s attention has moved to Betterleaks (github.com/betterleaks/betterleaks), created in February 2026, with v1.9.0 out on 2026-09-29 and about 2,100 GitHub stars.

That sounds like a reason to panic and is a reason to calendar a look in six months. A feature-complete regex scanner with security patches is a fine thing to have in a commit hook. Nobody needs a new feature in their hook at 2 AM. You need it to run fast and stay quiet. Pin the rev, let Renovate bump it (more on that below), and move when Betterleaks earns it.

TruffleHog: The One That Checks If the Key Works

TruffleHog v3 is a Go rewrite with 700+ credential detectors, and its party trick is active verification. When it finds something that looks like an AWS key, it calls the vendor API to check whether the credential is live. A match that works is a verified finding. A match that is a dead test key from 2019 is noise, and TruffleHog can drop it.

That means fewer false positives. It also means your commit makes outbound network calls.

The hook (v3.99.0 as of October 2026) runs this:

trufflehog git file://. --since-commit HEAD --results=verified --fail --trust-local-git-config

What each flag does:

Ignoring a line works with a trufflehog:ignore comment. --no-ignore-tag reports results even when the comment is present, which is handy in CI.

The honest trade-offs

Three gotchas to know before you adopt it.

  1. Offline commits report nothing. With --results=verified, no network means nothing can be verified, so nothing is reported. On a plane, your hook passes everything. That is the worst failure mode: a green check that means “I could not look”.
  2. Commits are slower. Verification calls take time. I will not give you a number, because the time scales with how many candidates you stage and how fast the vendors answer. Expect “slower than a regex”, not “instant”.
  3. Stage, then commit. TruffleHog’s pre-commit guide says to run git add and git commit as separate steps and to avoid git commit -am, because unstaged modifications can slip past the hook. Test with a staged fake secret before you trust it (the script below does exactly that).

There is also a privacy angle. A live verification call sends your candidate credential to the vendor’s API. For a real key, you are telling the vendor that a key is in your commit. Most teams accept that, and you should decide on purpose.

detect-secrets: The Baseline Workflow

Yelp’s detect-secrets (v1.5.0, last released 2024-05-06) works differently. It uses entropy checks and plugins, so it flags high-entropy strings that are not credentials. The answer to the noise is a baseline file: you scan once, audit the results, and the hook then only complains about new findings.

.pre-commit-config.yaml
repos:
- repo: https://github.com/Yelp/detect-secrets
rev: v1.5.0
hooks:
- id: detect-secrets
args: ['--baseline', '.secrets.baseline']

The workflow:

Terminal window
detect-secrets scan > .secrets.baseline
detect-secrets audit .secrets.baseline
git ls-files -z | xargs -0 detect-secrets-hook --baseline .secrets.baseline

The first command writes the baseline. The second walks you through each finding interactively, marking real secrets and false positives. The third scans every tracked file against the baseline. For a single line, add # pragma: allowlist secret.

Tuning flags exist: --base64-limit and --hex-limit (entropy thresholds), --disable-plugin, --exclude-files, --exclude-lines, --exclude-secrets, --word-list, and --list-all-plugins to see what is available.

The release cadence is slow. The last release was in May 2024, which is a long gap in a field where vendors keep changing token formats. I would pick it only if your shop already runs on Python tooling and you like the audit-and-baseline ritual.

The Comparison Table

gitleaksTruffleHogdetect-secrets
Works offlineYesRuns, but verified-only reports nothingYes
VerificationNo (regex rules)Yes, calls vendor APIsNo (entropy + plugins)
Inline allowgitleaks:allowtrufflehog:ignore# pragma: allowlist secret
Ignore mechanism.gitleaksignore fingerprints, .gitleaks.toml allowlistsInline tag, plus exclude flags.secrets.baseline
Maintenance (Oct 2026)v8.30.1, security patches onlyv3.99.0, released 2026-10-06v1.5.0, last release 2024-05-06
Pick whenYou want a fast, quiet gate on every commitYou want proof a key is live and accept network callsYou live in the Yelp/Python world and like baselines

Prove It Blocks: leak-test.sh

Never trust a hook you have not watched fail. This script makes a throwaway repo, copies in the configs, stages a fake token, and tries to commit. The token is fake. It matches the shape of a GitHub token and does not authenticate anywhere.

leak-test.sh
#!/usr/bin/env bash
set -euo pipefail
here="$(cd "$(dirname "$0")" && pwd)"
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
cd "$tmp"
git init -q
git config user.name "Leak Test"
git config user.email "[email protected]"
cp "$here/.pre-commit-config.yaml" "$here/.gitleaks.toml" .
pre-commit install
# Fake token: right shape, not a real credential.
echo 'RENOVATE_GITHUB_COM_TOKEN=ghp_aB3dE5gH7jK9mN1pQ3sT5vW7yZ9bC1dE3fG5' > config.env
git add config.env
if git commit -m "add config" ; then
echo "FAIL: the commit went through, no hook caught the fake token"
exit 1
else
echo "OK: the hook blocked the commit"
fi

Run it with bash leak-test.sh. You should see the Failed line from gitleaks and the final “OK” message. If you see “FAIL”, your hook is not wired up, and you just saved yourself a very bad morning.

Heads up: with the --results=verified default, TruffleHog does not flag this fake token. In my run it reported Passed while gitleaks reported Failed on rule github-pat, which is the verification working as designed. Also expect the first run to be slow: pre-commit builds both scanners from source before the hooks fire.

The Gotcha: Hooks Only Run Where You Installed Them

pre-commit install writes a file into your local .git/hooks. Hooks do not travel with the repo. A teammate who clones and never runs pre-commit install has no protection, and neither does a bot that commits through an API.

The backstop is CI. Run the same scan in the pipeline so a missed install still gets caught before merge. A generic job needs only the binary and one command:

Terminal window
# Option 1: reuse the exact same hooks
pre-commit run --all-files
# Option 2: scan the history range of the pull request
gitleaks git --redact --verbose --log-opts="$BASE_SHA..HEAD"
trufflehog git file://. --since-commit "$BASE_SHA" --results=verified --fail

CI is the net under the trapeze. It catches the fall, but the secret has already been pushed to a branch by then. It limits who can see it, and it does not replace the hook. Rotate anything CI catches.

Close the Loop With Renovate

Back to the bot from the war story. Renovate can manage the rev: pins in your .pre-commit-config.yaml through its pre-commit manager. Renovate’s docs mark the manager as beta and it is disabled by default, so turn it on in renovate.json:

renovate.json
{
"pre-commit": {
"enabled": true
}
}

Now the bot that nearly leaked your token keeps your secret scanner on the latest release (Renovate is at 44.145.0 as of October 2026). Keep the Renovate tokens in the runner’s environment or a secrets store, never in config.js or a compose file you commit. For a compose file, use an env_file that is git-ignored, or Docker secrets.

The SumGuy Take

Run gitleaks. Pin it, add the .gitleaks.toml above, and keep detect-private-key next to it. It works on a plane, it scanned the 67-byte test file below in 21 ms, and it blocks the commit before there is a commit. Feature complete is a fine state for a regex gate.

Reach for TruffleHog when false positives are costing you more than slower commits, or when you want to know whether a leaked key was alive. Treat the network dependency as a real cost, not a footnote. Pick detect-secrets if your team already runs on Yelp-style Python tooling and wants the audit workflow.

Add the CI backstop either way. Then rotate anything that ever escaped, because history rewrites do not un-leak. Your 2 AM self will thank you.

Common Questions

What should I do after I already pushed a secret to GitHub?

Rotate the credential immediately, then check the provider’s logs for use. Rewriting history with git filter-repo does not help against anything that cloned the repo already, including bots, mirrors, and forks. Treat the secret as public. Scrub history afterward only to stop new readers from seeing a dead token.

Does gitleaks work offline?

Yes, gitleaks works fully offline. Gitleaks matches rules against your staged changes locally and never calls an external service. The gitleaks hook id builds from source on first use, so pre-commit needs network access once to fetch and build it. After that, every commit scans without a connection.

Does GitHub push protection make a local pre-commit hook redundant?

No, a local hook still earns its place. Push protection acts at the push boundary and only covers the token patterns the provider supports. A pre-commit hook blocks the secret before a commit object exists, works on any host, and can use your own custom rules.

Is gitleaks still safe to use now that it is feature complete?

Yes. As of October 2026, gitleaks v8.30.1 still receives security patches, and the README says future releases are security patches only. The author now focuses on Betterleaks. Gitleaks has no new features coming, so pin a version and plan a review of Betterleaks rather than an urgent migration.

Will the hook protect teammates who never ran pre-commit install?

No. The pre-commit framework writes the hook into each clone’s local .git/hooks, so a teammate who skips pre-commit install commits unscanned. Add a CI job that runs pre-commit run --all-files or the scanner directly, so a missed install gets caught before merge.


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it'll show up above once verified.


Previous Post
Sync Game Saves Without a Cloud Account
Next Post
zstd vs lz4 vs gzip vs xz: Measured

Discussion

Powered by Garrul . Sign in with GitHub or Google, or post anonymously.

Related Posts