I Switched from Gitleaks to Betterleaks — Here’s What Changed
Why the original Gitleaks author built a replacement, and how you can use it to catch secrets before attackers do
I Switched from Gitleaks to Betterleaks — Here’s What Changed

Why the original Gitleaks author built a replacement, and how you can use it to catch secrets before attackers do
In my previous article, I walked through how to harden your Git workflow using Gitleaks. A few weeks after publishing it, I noticed something: the same engineer who created Gitleaks had quietly shipped a brand new tool and called it Betterleaks.
That name is pretty confident. I wanted to find out if it lives up to it.
Spoiler: it does — and by a significant margin. This article breaks down why, and walks you through a full hands-on setup from scratch.
Why Credentials in Code Are a Ticking Clock
Here’s something that does not get said enough: the moment a secret lands in a public repository, the countdown starts. Automated bots continuously index GitHub and GitLab looking for patterns that resemble API keys, database connection strings, and service tokens. In some documented incidents, compromised credentials have been weaponized in under five minutes.
The usual culprits are innocent-looking mistakes. A .env file that slipped past .gitignore. A token copy-pasted directly into a configuration file during a late-night debugging session. A base64-encoded credential buried three commits back that nobody thought to scrub.
The job of a secrets scanner is to catch all of these before an attacker does.
So What Exactly Is Betterleaks?
Betterleaks is a free, open-source CLI tool for detecting exposed credentials across Git repositories, directories, and streamed input. It was written by Zach Rice, who also authored the original Gitleaks — a tool that has been downloaded tens of millions of times and is deeply embedded in DevSecOps pipelines around the world.
The reason for building something new had to do with project ownership. Rice no longer held full administrative control over the Gitleaks repository, which created friction around the direction of the tool. Rather than work around that constraint indefinitely, he started fresh. The result, Betterleaks v1.0.0, shipped in early 2026 under MIT license and with backing from Aikido Security, a developer-focused security company.
The project is designed as a straight drop-in replacement. Your existing Gitleaks config files, ignore files, and CLI flags all carry over with no modification required.
What Actually Makes It Better
The Problem With Entropy-Based Detection
To understand why Betterleaks matters, you first need to understand how most secrets scanners — including Gitleaks — work under the hood.
The standard approach is Shannon Entropy: a mathematical measure of how “random” or unpredictable a string of characters is. The logic is intuitive — real secrets like API keys tend to look random, so high-entropy strings are flagged as suspicious.
The problem? Entropy is a blunt instrument. Certain natural language phrases, variable names, and identifiers also register as high-entropy. The result is a steady stream of false positives that erode trust in the tool. Teams start suppressing alerts. Actual secrets slip through.
Token Efficiency: A Smarter Signal
Betterleaks takes a different approach. Instead of measuring character-level randomness, it evaluates strings using Byte Pair Encoding (BPE) tokenization — the same compression technique that underpins large language models like GPT.
Here is the core insight: when a BPE tokenizer processes natural language, it maps familiar words and phrases to long, efficient tokens. When it processes a random-looking string like an API key, it cannot find those patterns and instead breaks the string into many tiny fragments. Betterleaks measures this compression ratio and uses it as a filter.
Strings that compress poorly (many short tokens) are likely secrets. Strings that compress efficiently (fewer, longer tokens) are probably human-readable text and get filtered out.
The practical impact of this on real-world detection:

A jump from 70% to 98% recall is not an incremental improvement. It means catching nearly three times as many of the misses that entropy-based scanners routinely let through.
Other Capabilities Worth Knowing
Parallel Git history scanning. Betterleaks can process a repository’s entire commit log using multiple workers simultaneously via --git-workers. For projects with years of history, this cuts scan time dramatically compared to sequential processing.
Recursive decoding. Developers sometimes encode credentials to keep them out of plain sight — base64, hex, URL encoding, unicode escapes. Betterleaks can peel back these layers automatically. Set --max-decode-depth and it will decode, scan, decode again, and repeat until it hits the depth limit or runs out of encoded content. Supported formats include base64, hex, percent-encoding, and unicode escape sequences.
Compressed archive scanning. Secrets can hide inside .zip, .tar.gz, and other archive formats. With --max-archive-depth, Betterleaks will extract and scan nested archives down to whatever depth you configure.
Full backwards compatibility. Betterleaks reads .gitleaks.toml natively. It respects .gitleaksignore and .betterleaksignore files interchangeably. All existing Gitleaks CLI flags work without modification.
Hands-On: Building a Full Secret Scanning Setup
Let me walk through a complete end-to-end demo. We will install Betterleaks, create a realistic repository with intentionally leaked credentials, run several types of scans, generate reports, and wire everything into a CI/CD pipeline.
What You Need
- A Linux or macOS terminal (WSL works fine on Windows)
- Git
- Docker (optional — only needed for the container install path)
Installing Betterleaks
Pick whichever method suits your environment:
# Homebrew — easiest on macOS or Linux
brew install betterleaks
# Docker — useful for ephemeral CI environments
docker pull ghcr.io/betterleaks/betterleaks:latest
# Build from source
git clone https://github.com/betterleaks/betterleaks
cd betterleaks
make betterleaks
Confirm the install worked:
betterleaks version
Setting Up a Demo Repository
We need a repository that mimics real-world credential exposure. Let’s build one from scratch.
mkdir leaked-creds-demo && cd leaked-creds-demo
git init
Create an application config file with several types of fake-but-realistic credentials:
cat > app_config.py << 'EOF'
# ============================================================
# Application Configuration
# WARNING: This file should never be committed to version control
# (But let's pretend someone forgot that rule)
# ============================================================
# Cloud provider keys
AWS_ACCESS_KEY_ID = "AKIAIOSFODNN7EXAMPLE"
AWS_SECRET_ACCESS_KEY = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
# Version control platform token
GITHUB_TOKEN = "ghp_aBcDeFgHiJkLmNoPqRsTuVwXyZ123456"
# Payment processor
STRIPE_SECRET_KEY = "sk_live_4eC39HqLyjWDarjtT1zdp7dc"
# Primary database
DATABASE_URL = "postgres://admin:SuperSecret123!@prod-db.internal.company.com:5432/myapp_prod"
# Notification service
SLACK_WEBHOOK_URL = "https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX"
# Safe to commit — marked explicitly
PLACEHOLDER = "this-is-not-a-real-key" # betterleaks:allow
EOF
Add a .env file for good measure:
cat > .env << 'EOF'
OPENAI_API_KEY=sk-proj-aBcDeFgHiJkLmNoPqRsTuVwXyZ1234567890abcdefghijklmnopqrstuvwxyz
SENDGRID_API_KEY=SG.aBcDeFgHiJkLmNo.PqRsTuVwXyZ1234567890
JWT_SIGNING_SECRET=do-not-commit-this-signing-key-ever
EOF
Commit both files to simulate the accidental push:
git add .
git commit -m "feat: add application configuration"
Now add a second commit with an obfuscated credential to test the decoding feature:
cat > cache_config.txt << 'EOF'
# Cache layer configuration
# Credentials stored in encoded format for "security"
CACHE_BACKEND_CREDS=c2stbGl2ZV80ZUMzOUhxTHlqV0Rhcmp0VDF6ZHA3ZGMK
EOF
git add .
git commit -m "chore: add cache configuration"
We now have a two-commit repo with credentials scattered across both plaintext and encoded formats.
Scanning the Working Directory
Start with the simplest scan — just the files on disk:
betterleaks dir -v .
You should see output along these lines:
Finding: AWS_ACCESS_KEY_ID = "AKIAIOSFODNN7EXAMPLE",
Secret: AKIAIOSFODNN7EXAMPLE
RuleID: aws-access-key-id
Entropy: 3.45
File: app_config.py
Line: 8
Finding: STRIPE_SECRET_KEY = "sk_live_4eC39HqLyjWDarjtT1zdp7dc",
Secret: sk_live_4eC39HqLyjWDarjtT1zdp7dc
RuleID: stripe-secret-key
File: app_config.py
Line: 14
Finding: GITHUB_TOKEN = "ghp_aBcDeFgHiJkLmNoPqRsTuVwXyZ123456",
Secret: ghp_aBcDeFgHiJkLmNoPqRsTuVwXyZ123456
RuleID: github-pat
File: app_config.py
Line: 11
...
Notice that PLACEHOLDER is not reported — the inline # betterleaks:allow comment tells the scanner to skip that specific line.
Scanning the Full Git History
Deleting a file from the latest commit does not erase it from history. Anyone with access to the repository can still retrieve it with git log. That is exactly why scanning git history matters more than scanning the working tree alone.
betterleaks git -v .
Betterleaks walks every commit, every patch, and every diff. If a credential appears anywhere in the repository’s history, it gets flagged — even if the file was deleted ten commits ago.
Speed things up on large codebases:
betterleaks git -v --git-workers=4 .
Catching Encoded Secrets
Now let’s see the recursive decoding in action:
betterleaks dir -v --max-decode-depth=5 .
Betterleaks will decode the base64 value in cache_config.txt, inspect the decoded output for credential patterns, and report a finding if it matches any rule. No manual decoding step required.
Generating Structured Reports
Raw terminal output is great for local development. CI/CD pipelines need structured, machine-readable output. Betterleaks supports several formats:
# JSON — easy to parse in scripts and dashboards
betterleaks git --report-format json --report-path betterleaks-report.json .
# SARIF — integrates natively with GitHub Advanced Security
betterleaks git --report-format sarif --report-path betterleaks-report.sarif .
# CSV — for spreadsheet-based reporting workflows
betterleaks git --report-format csv --report-path betterleaks-report.csv .
The SARIF output is particularly valuable in GitHub workflows — findings appear directly inside pull requests as code annotations, with file and line references.
Working With a Baseline
When you first adopt Betterleaks on an existing codebase, there is a practical challenge: the repository likely has old secrets buried in its history that you cannot immediately remediate. Running a scan with no baseline will fail your CI pipeline on every build until every historical finding is addressed — which is usually impractical.
The baseline workflow handles this cleanly:
# Generate the baseline from the current state
betterleaks git --report-path betterleaks-baseline.json .
# Future scans flag only findings that didn't exist at baseline time
betterleaks git \
--baseline-path betterleaks-baseline.json \
--report-path new-findings.json \
.
The baseline file acts as a known-issues registry. Your pipeline passes on historical findings and only fails on newly introduced secrets.
Blocking Commits at the Source: Pre-commit Hooks
The cleanest place to stop a secret is before it is ever committed. Pre-commit hooks run Betterleaks automatically every time a developer attempts a git commit, and abort the commit if anything is detected.
First, install the pre-commit framework if you do not already have it:
pip install pre-commit
Create a .pre-commit-config.yaml at the repository root:
repos:
- repo: https://github.com/betterleaks/betterleaks
rev: v1.0.1
hooks:
- id: betterleaks
Install the hooks into the local Git configuration:
pre-commit install
From this point forward, every git commit in this repository will trigger a Betterleaks scan. If a credential is detected, the commit is rejected and the developer sees exactly which file and line caused the failure.
CI/CD Integration: GitHub Actions
Pre-commit hooks protect individual developer machines, but they can be bypassed or skipped. A pipeline-level check is the second, mandatory layer of defence.
Here is a complete GitHub Actions workflow that scans on every push and pull request, generates a SARIF report, and uploads it to GitHub’s Security tab:
name: Secret Scanning with Betterleaks
on:
push:
branches:
- main
- develop
pull_request:
branches:
- main
- develop
jobs:
secret-scan:
name: Detect Exposed Credentials
runs-on: ubuntu-latest
steps:
- name: Checkout full repository history
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Betterleaks
run: |
curl -L -o betterleaks.tar.gz https://github.com/betterleaks/betterleaks/releases/download/v1.1.1/betterleaks_1.1.1_linux_x64.tar.gz
tar -xzf betterleaks.tar.gz
chmod +x betterleaks
sudo mv betterleaks /usr/local/bin/
betterleaks --version
- name: Run credential scan
run: |
betterleaks git \
--report-format sarif \
--report-path betterleaks.sarif \
--exit-code 1 \
.
- name: Upload findings to GitHub Security
uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: betterleaks.sarif
Two things worth highlighting here:
fetch-depth: 0 is critical. GitHub Actions defaults to a shallow clone with only the latest commit. Without full history, the git scan only sees the most recent patch — exactly the kind of gap an attacker would exploit.
--exit-code 1 causes the pipeline step to exit with a non-zero code when credentials are detected, which marks the check as failed and blocks merging in branch protection rules.
GitHub Push Protection: The Hidden Safety Net
Even with pre-commit hooks in place, secrets can still slip through — particularly from commits made before the hook was installed. GitHub’s built-in Push Protection is a server-side layer that catches exactly this case.
When you attempt to push a branch, GitHub scans the entire push — including all commits in the history — for known secret patterns. If it finds one, the push is rejected entirely before any code lands on the remote.
What It Looks Like in Practice
Here is a real example. The pre-commit hook passed cleanly on the latest commit. But the push was blocked because secrets existed in an older commit:
$ git push origin -u main
remote: error: GH013: Repository rule violations found for refs/heads/main.
remote:
remote: - GITHUB PUSH PROTECTION
remote: -----------------------------------------------
remote: Resolve the following violations before pushing again
remote:
remote: -- Stripe API Key ---------------------------
remote: locations:
remote: - commit: 41560b219830e9d847cfd8ed34258a75c5c5527e
remote: path: app_confirg.py:15
remote:
remote: -- Slack Incoming Webhook URL ---------------
remote: locations:
remote: - commit: 41560b219830e9d847cfd8ed34258a75c5c5527e
remote: path: app_confirg.py:21
remote:
! [remote rejected] main -> main (push declined due to repository rule violations)
This is exactly what happened in our demo repository. The pre-commit hook only saw the staged files in the latest commit. GitHub Push Protection scanned the full history and blocked the push on secrets that were three commits old.
How to Resolve a Blocked Push
You have two options depending on your situation:
Option 1: Rewrite Git History (Recommended)
This permanently removes the secrets from all commits. Use git-filter-repo:
pip install git-filter-repo
# Remove the files containing secrets from all history
git filter-repo --path app_confirg.py --invert-paths
# Re-add the remote (filter-repo removes it for safety)
git remote add origin https://github.com/your-org/your-repo.git
# Force push the clean history
git push origin main --force
Warning: force pushing rewrites history. Coordinate with your team before doing this on a shared repository.
Option 2: Bypass After Rotating (Only If Keys Are Already Invalidated)
GitHub provides a bypass URL in the error output for each blocked secret. You can use this to allow the push — but only if you have already rotated the exposed credentials:
# GitHub provides a URL like:
# https://github.com/your-org/your-repo/security/secret-scanning/unblock-secret/XXXXX
# Visit the URL, confirm the secret is revoked, then push again
git push origin main
Never bypass without rotating first. The moment a secret appears in a public repo, assume it has been seen by automated scanners.
Why Pre-commit Alone Is Not Enough
This is the critical insight that the demo makes concrete:
Commit timeline:
ae09f17 ← cache_config.txt secret committed (NO hook existed)
41560b2 ← .env + app_confirg.py secrets committed (NO hook existed)
c23c3f9 ← .gitignore + .pre-commit-config.yaml (hook added HERE)
6c67a99 ← GitHub Actions workflow added
← git push → BLOCKED by GitHub Push Protection
The pre-commit hook only protects commits made after it was installed. Everything before that point is invisible to it. GitHub Push Protection sees all of it.
Enabling Secret Scanning on Your Repository
Push Protection is available on all public repositories and on private repositories with GitHub Advanced Security. To enable it:
• Go to your repository on GitHub
• Navigate to Settings → Security & analysis
• Enable Secret scanning
• Enable Push protection under Secret scanning
Once enabled, GitHub will also surface findings in the Security tab of your repository, giving your team a centralised view of all detected secrets across branches and history.
The Full Defence Stack
With all layers in place, your secret scanning coverage looks like this:
Developer runs git commit
│
▼
pre-commit hook (betterleaks) ← Layer 1: blocks secrets in staged files
│
▼
git push to GitHub
│
▼
GitHub Push Protection ← Layer 2: blocks secrets anywhere in push history
│
▼
GitHub Actions CI ← Layer 3: full audit on every PR
│
▼
GitHub Security Tab (SARIF) ← Layer 4: visibility and tracking for the team
Each layer catches what the previous one misses. None of them alone is sufficient. All four together make it genuinely difficult for a secret to reach production undetected.
Writing Custom Detection Rules
The built-in ruleset covers hundreds of credential types out of the box, but every organisation has internal token formats that generic rules will never catch. Betterleaks gives you a clean TOML-based configuration DSL for writing your own.
Create a .betterleaks.toml at the repo root:
title = "Acme Corp Secret Scanning Config"
# Extend the built-in ruleset rather than replacing it
[extend]
useDefault = true
disabledRules = ["generic-api-key"] # Disable rules that generate too much noise
# Custom rule: detect internal service tokens
[[rules]]
id = "acme-service-token"
description = "Detects Acme Corp internal service authentication tokens"
regex = '''ACME-[A-Za-z0-9]{8}-[A-Za-z0-9]{16}-[A-Za-z0-9]{8}'''
tokenEfficiency = true
keywords = ["ACME"]
tags = ["internal", "service-auth"]
# Allowlist: ignore tokens that look like test fixtures
[[rules.allowlists]]
description = "Skip known test and placeholder tokens"
stopwords = ["test", "example", "sample", "placeholder", "dummy"]
# Global allowlist: never scan generated test fixtures
[[allowlists]]
description = "Skip autogenerated test data directories"
paths = [
'''tests/fixtures/''',
'''testdata/''',
'''__mocks__/''',
]
Run it:
betterleaks git --config .betterleaks.toml -v .
The configuration supports composite rules with proximity matching (require two patterns to appear within N lines of each other), per-rule allowlists, global allowlists by path, commit hash, or stopword — giving you precise control over what gets reported.
Moving from Gitleaks: It Takes About 30 Seconds
If your team already uses Gitleaks, migration requires no planning. Every CLI flag, config file format, and ignore file convention carries over:
# Your existing Gitleaks command
gitleaks git --config .gitleaks.toml -v .
# Betterleaks — identical syntax, smarter engine
betterleaks git --config .gitleaks.toml -v .
Betterleaks reads .gitleaks.toml natively. It honours .gitleaksignore files alongside its own .betterleaksignore. You can migrate at your own pace — run both tools in parallel if you want to compare output before fully switching over.
Where AI Agents Fit In
One of the more interesting design decisions behind Betterleaks is its explicit consideration of AI coding agents as first-class users of the tool.
Tools like Claude Code, Cursor, and GitHub Copilot are increasingly generating, reviewing, and committing code autonomously. They interact with the shell the same way a developer does — by calling CLI tools and parsing their output. Betterleaks was designed with this in mind: structured output flags, minimal noise by default, and clean exit codes that automation can act on without needing to parse human-readable text.
As AI agents take on more of the code authoring workflow, the chances of a model accidentally including a credential in a generated snippet — or failing to recognise that a string it’s copying is sensitive — are real. Having a scanner that fits naturally into an agent’s tool-calling pattern is not just convenient; it closes a gap that most teams have not yet thought about.
Try It Yourself: The Demo Repository
Everything covered in this article — the pre-commit hook, the CI/CD pipeline, GitHub Push Protection, and the credential-laden Git history — is available in a fully working demo repository you can clone and run immediately.
What’s Inside the Repo
precommit-betterleaks-demo/
├── .github/
│ └── workflows/
│ └── secret-scan.yml # GitHub Actions CI pipeline
├── .env # Demo: accidental .env commit
├── .gitignore # Ignores report files
├── .pre-commit-config.yaml # Betterleaks pre-commit hook
├── app_confirg.py # Demo: hardcoded credentials
├── cache_config.txt # Demo: base64-encoded credential
└── README.md
What You Can Reproduce
• Clone the repo and install pre-commit hooks in under two minutes
• Trigger a blocked commit by staging a file with a fake credential
• Run betterleaks git . to see all historical findings across every commit
• Watch GitHub Push Protection block the push on secrets from old commits
- See the GitHub Actions pipeline fail with a SARIF report uploaded to the Security tab
Getting Started
git clone https://github.com/Learning-DevSecOps/precommit-betterleaks-demo.git
cd precommit-betterleaks-demo
pip install pre-commit
pre-commit install
# Try a manual full-history scan
betterleaks git -v --git-workers=4 .
Note: All credentials in the repository are fake and exist purely for demonstration. If you fork the repo, GitHub Push Protection will block your first push — this is intentional. It’s the feature working exactly as designed.
How It Stacks Up Against the Alternatives

The choice between these tools depends on what you optimise for. If raw detection accuracy on a self-managed setup matters most, Betterleaks is the clear answer. If your team needs a managed SaaS dashboard with team-level reporting, TruffleHog Enterprise or GitGuardian are worth evaluating. For most engineering teams building on top of an existing Gitleaks workflow, though, Betterleaks is simply a free upgrade.
Summary
Here is what I took away from spending time with this tool:
Secret scanning is one of those controls that feels optional right up until the moment it is not. Once a credential is exploited, you are dealing with an incident response situation — and those are expensive, disruptive, and entirely avoidable.
The jump in detection accuracy from entropy-based scanning to BPE token efficiency is the kind of improvement that changes outcomes, not just metrics. A scanner that catches 98% of secrets is fundamentally more useful than one that catches 70%, especially in a world where AI-generated code is adding more surface area every quarter.
If you are already using Gitleaks, switching costs nothing. If you are starting fresh, start here.
Useful Links
- GitHub: github.com/betterleaks/betterleaks
- Website: betterleaks.com
- Author’s blog on how detection works: Regex is (almost) all you need
- My previous Gitleaks article: Hardening Your Git Workflow
- Demo Repository: precommit-betterleaks-demo
If this was useful, give it a follow for more hands-on DevSecOps content. Questions about wiring Betterleaks into your specific setup? Drop them in the comments.
메타데이터
- post_id
- de286168bf9c
- slug
- i-switched-from-gitleaks-to-betterleaks-heres-what-changed-de286168bf9c
- url
- https://medium.com/@gautammakwana421/i-switched-from-gitleaks-to-betterleaks-heres-what-changed-de286168bf9c
- canonical_url
- https://medium.com/@gautammakwana421/i-switched-from-gitleaks-to-betterleaks-heres-what-changed-de286168bf9c
- author_url
- https://medium.com/@gautammakwana421
- status
- ok
- fetched_at
- 2026-07-11 03:16:26