Pre-Commit Hooks for Secret Detection: Setup in 10 Minutes

Written by the Rafter Team

A pre-commit hook runs a script every time you run git commit. If the script fails—because it found a credential pattern in your staged changes—the commit is blocked. The secret never enters git history. No force-push cleanup, no history rewriting, no incident response.
This is the cheapest point in the credential security lifecycle to catch a leaked API key. Once a secret enters git history, removing it requires git filter-repo, force pushes, and the assumption that any forks or caches have already exposed it. A pre-commit hook prevents all of that by catching the secret before it exists in the repository at all.
GitGuardian's 2024 report found that 12.8 million secrets were exposed on GitHub in a single year. The majority were committed by developers who knew better but didn't have a safety net. Pre-commit hooks are that safety net.
This guide covers three tools: gitleaks (recommended for most teams), detect-secrets (best for large existing codebases), and TruffleHog (best for verified detection). For a deeper comparison of these tools, see Secret Scanning in CI/CD: detect-secrets vs gitleaks vs TruffleHog.
How Pre-Commit Hooks Work
Git supports hooks—scripts that run at specific points in the Git workflow. The pre-commit hook fires after git commit is invoked but before the commit is created. If the hook script exits with a non-zero status, the commit is aborted.
git commit -m "add payment integration"
│
▼
[pre-commit hook runs]
│
├── Secret found? → Commit blocked, error message shown
│
└── No secret found? → Commit proceeds normally
There are two ways to set up pre-commit hooks for secret detection:
- Native git hooks — Write a shell script directly in
.git/hooks/pre-commit - The
pre-commitframework — A Python tool that manages hooks declaratively via.pre-commit-config.yaml
The pre-commit framework is the standard approach because it handles tool versioning, installation, and team-wide consistency.
Option 1: gitleaks (Recommended)
gitleaks is the best default choice for most teams. It's a single Go binary with 150+ built-in secret detection rules, sub-second scan times, and no external dependencies.
Installation
# macOS
brew install gitleaks
# Linux (download binary)
curl -sSL https://github.com/gitleaks/gitleaks/releases/latest/download/gitleaks_linux_amd64.tar.gz | tar xz
sudo mv gitleaks /usr/local/bin/
# Or via Go
go install github.com/gitleaks/gitleaks/v8@latest
Setup with pre-commit Framework
# Install pre-commit (if not already installed)
pip install pre-commit
Create .pre-commit-config.yaml in your repository root:
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.21.0
hooks:
- id: gitleaks
Install the hook:
pre-commit install
That's it. Every git commit now runs gitleaks against your staged changes.
What It Catches
gitleaks ships with rules for 150+ secret types:
# Test by staging a file with a fake secret
echo 'STRIPE_KEY=sk_live_4eC39HqLyjWDarjtT1zdp7dc' > test-secret.txt
git add test-secret.txt
git commit -m "test"
# Output:
# Finding: STRIPE_KEY=sk_live_4eC39HqLyjWDarjtT1zdp7dc
# Secret: sk_live_4eC39HqLyjWDarjtT1zdp7dc
# RuleID: stripe-access-token
# File: test-secret.txt
# Line: 1
The commit is blocked. Remove the secret and try again.
Custom Rules
Add a .gitleaks.toml file to extend or override default rules:
# .gitleaks.toml
# Extend the default config (don't replace it)
[extend]
useDefault = true
# Add a custom rule for internal tokens
[[rules]]
id = "internal-api-token"
description = "Internal API service token"
regex = '''svc_tok_[A-Za-z0-9]{32,}'''
keywords = ["svc_tok_"]
# Allow-list for test fixtures and documentation
[allowlist]
paths = [
'''test/fixtures/.*''',
'''docs/examples/.*''',
]
regexes = [
'''sk_test_[A-Za-z0-9]+''', # Allow Stripe test keys
'''EXAMPLE_.*''',
]
Allow-Listing False Positives
When gitleaks flags a string that isn't actually a secret (a test fixture, a documentation example, a hash), add it to the allow-list rather than disabling the rule:
# .gitleaks.toml
[allowlist]
# Allow specific commits (useful for historical false positives)
commits = ["abc123def456"]
# Allow paths
paths = [
'''.*_test\.go''',
'''testdata/.*''',
]
# Allow specific regex patterns
regexes = [
'''PLACEHOLDER_.*''',
'''example-[a-z]+-key''',
]
Option 2: detect-secrets (Yelp)
detect-secrets is the best choice for teams with large existing codebases where a "block everything" approach would produce too many false positives on day one.
Installation
pip install detect-secrets
Setup
# Generate initial baseline (scans everything, records existing secrets)
detect-secrets scan > .secrets.baseline
# Audit the baseline — mark known non-secrets as false positives
detect-secrets audit .secrets.baseline
Create .pre-commit-config.yaml:
repos:
- repo: https://github.com/Yelp/detect-secrets
rev: v1.5.0
hooks:
- id: detect-secrets
args: ['--baseline', '.secrets.baseline']
pre-commit install
How the Baseline Model Works
The baseline file (.secrets.baseline) records every detected secret in the current codebase. The pre-commit hook only alerts on new detections—secrets not in the baseline.
Initial scan: 47 potential secrets found
│
▼
Audit: 12 are real secrets (need remediation)
35 are false positives (marked in baseline)
│
▼
Commit baseline to repo
│
▼
Pre-commit hook: only alerts on NEW secrets not in baseline
This approach lets you deploy scanning immediately on a codebase with existing secrets, without drowning in alerts. Fix the 12 real secrets at your own pace while blocking new leaks from day one.
The discipline requirement: The baseline model fails if developers add new detections to the baseline without auditing them. Treat baseline updates as code review—require PR approval for changes to .secrets.baseline.
Tuning Entropy Thresholds
detect-secrets uses Shannon entropy to find random-looking strings. The default thresholds work for most cases, but you can tune them:
# Lower entropy threshold = more detections, more false positives
detect-secrets scan --base64-limit 3.5 --hex-limit 2.5 > .secrets.baseline
# Higher entropy threshold = fewer detections, fewer false positives
detect-secrets scan --base64-limit 5.0 --hex-limit 4.0 > .secrets.baseline
Option 3: TruffleHog
TruffleHog's pre-commit hook adds credential verification—it tests whether detected secrets are actually active. This eliminates false positives on expired or test credentials.
Installation
# macOS
brew install trufflehog
# Linux
curl -sSL https://raw.githubusercontent.com/trufflesecurity/trufflehog/main/scripts/install.sh | sh
Setup
# .pre-commit-config.yaml
repos:
- repo: https://github.com/trufflesecurity/trufflehog
rev: v3.88.0
hooks:
- id: trufflehog
args:
- "git"
- "file://."
- "--since-commit"
- "HEAD"
- "--only-verified"
- "--fail"
pre-commit install
The --only-verified Trade-Off
The --only-verified flag means the hook only blocks on confirmed active credentials. Unverified detections (strings that look like secrets but couldn't be validated) pass through silently.
This dramatically reduces false positives—you'll never be blocked by a test fixture or documentation example. But it means unverifiable secrets (custom internal tokens, database passwords, encryption keys) won't be caught by the hook.
Recommendation: If you use --only-verified in the pre-commit hook, pair it with a gitleaks hook for pattern-based coverage of unverifiable secret types:
# .pre-commit-config.yaml — layered approach
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.21.0
hooks:
- id: gitleaks
- repo: https://github.com/trufflesecurity/trufflehog
rev: v3.88.0
hooks:
- id: trufflehog
args: ["git", "file://.", "--since-commit", "HEAD", "--only-verified"]
stages: [push] # Run on push instead of commit (slower due to verification)
This gives you fast gitleaks blocking on every commit, plus TruffleHog verification on push.
Native Git Hooks (Without the Framework)
If you prefer not to use the pre-commit framework, you can install hooks directly:
#!/bin/bash
# .git/hooks/pre-commit
# Make executable: chmod +x .git/hooks/pre-commit
# Run gitleaks on staged changes
gitleaks protect --staged --verbose
exit_code=$?
if [ $exit_code -ne 0 ]; then
echo ""
echo "Secret detected in staged changes. Commit blocked."
echo "Remove the secret and try again."
echo ""
exit 1
fi
Limitation: Native hooks live in .git/hooks/, which isn't committed to the repository. Each team member must install them manually. The pre-commit framework solves this by reading from the committed .pre-commit-config.yaml.
To share native hooks across a team, use a hooks directory:
# Create a shared hooks directory
mkdir -p .githooks
cp .git/hooks/pre-commit .githooks/pre-commit
# Configure git to use it
git config core.hooksPath .githooks
Commit .githooks/ to the repository. New clones need to run the git config command, but the hook scripts themselves are version-controlled.
Team Rollout: Making It Stick
The hardest part of pre-commit hooks isn't the setup—it's adoption. Here's how to make it stick across a team.
Step 1: Add to Repository Setup Documentation
Include hook installation in your README or contributing guide:
## Development Setup
1. Clone the repository
2. Install dependencies: `npm install` / `pip install -r requirements.txt`
3. Install pre-commit hooks:
```bash
pip install pre-commit
pre-commit install
### Step 2: Enforce in CI
Pre-commit hooks are local—a developer can skip them with `git commit --no-verify`. Add a CI step that runs the same scan to catch bypasses:
```yaml
# .github/workflows/secrets.yml
name: Secret Scanning
on: [push, pull_request]
jobs:
gitleaks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Step 3: Handle False Positives Gracefully
Nothing kills adoption faster than a tool that cries wolf. When developers encounter false positives:
- Add the pattern to
.gitleaks.tomlallow-list (for gitleaks) or.secrets.baseline(for detect-secrets) - Commit the updated configuration
- Share the pattern with the team so others don't hit the same issue
Performance Impact
Pre-commit hooks add latency to every commit. Here's what to expect:
| Tool | Typical Scan Time | Impact |
|---|---|---|
| gitleaks | 0.1-0.5 seconds | Negligible |
| detect-secrets | 0.5-2 seconds | Minor |
| TruffleHog (no verification) | 1-3 seconds | Noticeable |
| TruffleHog (with verification) | 5-30 seconds | Significant |
For TruffleHog with verification, consider running it on push instead of commit to avoid disrupting the commit flow.
Troubleshooting Common Issues
"I committed a secret before installing the hook"
The hook only scans new commits. Run a manual scan to find existing secrets:
# Scan current state
gitleaks detect --source . -v
# Scan full git history
gitleaks detect --source . --log-opts="--all" -v
If you find secrets in history, follow the emergency response guide.
"The hook is too slow"
If using TruffleHog, switch to --only-verified or move verification to push/CI. If using detect-secrets, reduce the number of active plugins:
detect-secrets scan --list-all-plugins
detect-secrets scan --disable-plugin HexHighEntropyString > .secrets.baseline
"The hook blocks on test fixtures"
Add test directories to the allow-list:
# .gitleaks.toml
[allowlist]
paths = [
'''test/.*''',
'''__tests__/.*''',
'''fixtures/.*''',
'''.*\.test\.(js|ts|py)''',
]
"Team members aren't installing the hook"
Add a CI check that fails if secrets are detected. This catches commits from developers who skip the local hook—GitHub's built-in secret scanning provides an additional safety net after push. Eventually, developers will install the hook to avoid waiting for CI feedback.
Conclusion
Pre-commit hooks are the simplest, highest-impact security improvement you can make to your development workflow. They catch leaked API keys at the cheapest possible point—before the secret enters git history—and add less than a second to each commit.
The setup takes 10 minutes:
pip install pre-commit- Create
.pre-commit-config.yamlwith gitleaks pre-commit install- Add a CI step to catch bypasses
- Configure allow-lists for known false positives
The alternative—finding a leaked secret after it's been pushed, rewriting git history, rotating credentials, auditing for exploitation—takes hours and carries real risk. Ten minutes of prevention eliminates that entire category of incident.
Rafter integrates gitleaks-based credential scanning into broader code security analysis. Start a scan at rafter.so.
Related Resources
- Secrets and Credential Security: The Complete Developer Guide
- Secret Scanning in CI/CD: detect-secrets vs gitleaks vs TruffleHog
- You Leaked an API Key—Now What? Emergency Response Guide
- GitHub Secret Scanning: What It Catches and What It Misses
- Top 10 Tools for Detecting API Key Leaks
- Exposed API Keys: The Silent Killer of Projects
- CI/CD Security Best Practices