Secret Scanning
Overview
Detect hardcoded secrets — API keys, tokens, passwords, PEM private keys, AWS access keys, GitHub PATs, JWTs, Slack tokens, OpenAI keys — before they land in commits, pushes, or production. This skill wraps three layered tools:
| Tool | Speed | False-positive rate | Live-verify? | Use case |
|---|---|---|---|---|
| gitleaks | Fast (regex-based) | Medium | No | Pre-commit / pre-push hooks; CI quick gate |
| trufflehog | Slower (verifies via API calls) | Low (--only-verified) |
Yes | CI deeper scan; reduce triage noise |
in-house scripts/secrets-scan.sh |
Fast (regex catalog of ~18 patterns) | Medium-high (curated allowlist) | No | Defense-in-depth; covers patterns gitleaks may miss; offline-safe; POSIX shells (Linux/macOS/Git Bash) |
in-house scripts/secrets-scan.py |
Fast (same catalog — stdlib-only Python port of the .sh, full parity) |
Medium-high (same curated allowlist) | No | Cross-platform (Linux/macOS/Windows, Python 3.10+); enterprise Windows where bash/PowerShell are blocked |
The recommended posture in 2026 is layered: gitleaks at pre-commit for speed, trufflehog with --only-verified in CI to eliminate triage burden, in-house regex as a backstop.
Companion skills:
dep-currency-check— dependency CVE check; closest local pattern for the gate-wrapper architecturepython-auth-security— credential storage patterns (where the secrets SHOULD live)docker-security— secrets-in-images (separate domain)llm-security— secrets in LLM prompts (don't put secrets in prompts; LLM07 / LLM06)
0. Secrets storage standard (host-wide, adopted 2026-07-25)
Detection is half the job. Where secrets LIVE is the other half — and the reason detection keeps having work to do. The standard:
~/.secrets/<project>.env -> loader -> process environment -> your code
storage (0600) delivery (ephemeral)
Storage and delivery are different questions. An environment variable is how a
process receives a secret, not where one is kept. Full spec, rationale, honest
caveats and the migration recipe: references/storage-standard.md.
| Layer | Rule |
|---|---|
| Storage | ~/.secrets/<project>.env (0600), dir 0700; shared values in common.env |
| Delivery | scripts/load-secrets.sh (bash) or scripts/load_secrets.py (python) |
| Precedence | real env > <project>.env > common.env |
| In repo | .env.example (names only), .gitignore rule, loader call — nothing else |
| Enforcement | pre-commit hook, blocking |
Enforcement — one pattern set, three call sites
The commit hook, the push hook and the gate all invoke the SAME scanner, so their rules cannot drift apart:
| When | Mechanism |
|---|---|
git commit |
hooks/pre-commit → secrets-scan.py --staged (reads the git index) |
git push |
.git/hooks/pre-push → secrets-scan.py (worktree) |
| Agent cycles | G_SECRETS_SCAN in _meta/gates.py |
bash ~/.claude/skills/secret-scanning/hooks/install.sh # per repo, commit-time
Two deliberate design choices, both learned the hard way:
install.shREFUSES to install if no scanner resolves. An inert hook is worse than no hook — it advertises protection that does not exist.- The hook fails CLOSED if the scanner disappears after install. Same reasoning.
git commit --no-verifyremains the explicit, visible escape hatch.
Commit-time is the one that matters most: push-time stops a secret leaving the machine, commit-time stops it entering history at all.
Honest limits — do not oversell
~/.secrets/is plaintext on disk. Fewer copies and one audit point, but it is NOT encryption and NOT a secret manager. Upgrade path:age+sopsorpass(none installed on this host today).- Env vars are not a security boundary — readable via
/proc/<pid>/environby the same user, inherited by every subprocess, visible inps. Fine for delivery. - Scrubbing is not rotation. A credential that was ever committed must be rotated at the provider; removing it from files does nothing for the exposure.
1. Tool selection — when to use which
| Scenario | Tool | Mode |
|---|---|---|
| Pre-commit hook (local dev, <2s budget) | gitleaks | regex |
| Pre-push hook (allow ~10s) | gitleaks OR in-house | regex |
| CI on every PR (allow ~60s) | gitleaks + trufflehog (--only-verified) | layered |
| CI nightly deep scan (allow several min) | trufflehog (--only-verified) + gitleaks | layered |
| Offline / air-gapped environments | gitleaks OR in-house | regex (no live verify) |
| Forge Step 1 advisory check on new project | in-house (always present) | advisory |
| bob WP-completion gate | G_SECRETS_SCAN strict mode | configurable scanner |
| alf sweep | G_SECRETS_SCAN advisory mode | configurable scanner |
| Historical git-history scan (whole repo back to root) | trufflehog git mode OR gitleaks git |
one-shot |
Why three tools instead of one: different tools catch different patterns. gitleaks is regex-heavy and fast; trufflehog adds live-verification (the secret is actually valid against its provider's API) which drastically reduces false-positives. The in-house catalog covers patterns specific to our local ecosystem (e.g. .bob-checkpoint.md token-shaped strings that aren't real but pattern-match) and runs offline.
2. Install Commands
RHEL 9 / AlmaLinux 9 / Rocky 9
# gitleaks — official binary release
curl -sSfL https://github.com/gitleaks/gitleaks/releases/latest/download/gitleaks_linux_x64.tar.gz | sudo tar -C /usr/local/bin -xzf - gitleaks
sudo chmod +x /usr/local/bin/gitleaks
gitleaks version
# trufflehog — official binary release
curl -sSfL https://raw.githubusercontent.com/trufflesecurity/trufflehog/main/scripts/install.sh | sudo sh -s -- -b /usr/local/bin
trufflehog --version
Debian 12 / Ubuntu 24.04
# gitleaks
curl -sSfL https://github.com/gitleaks/gitleaks/releases/latest/download/gitleaks_linux_x64.tar.gz | sudo tar -C /usr/local/bin -xzf - gitleaks
sudo chmod +x /usr/local/bin/gitleaks
# trufflehog
curl -sSfL https://raw.githubusercontent.com/trufflesecurity/trufflehog/main/scripts/install.sh | sudo sh -s -- -b /usr/local/bin
macOS
brew install gitleaks trufflesecurity/trufflehog/trufflehog
Windows 11
# gitleaks — via Scoop / Chocolatey or release ZIP
scoop install gitleaks
# OR
choco install gitleaks
# trufflehog — via release binary; no Scoop package in 2026
# Download from https://github.com/trufflesecurity/trufflehog/releases/latest
Verify availability via env-adoption probe:
bash ~/.claude/skills/env-adoption/scripts/probe.sh check --force
jq '.tools | {gitleaks, trufflehog}' ~/.claude/state/inventory.json
3. Canonical patterns
3.1 Local pre-push hook (bash for POSIX shells, Python for cross-platform)
Two installer/scanner pairs ship in your project root's scripts/ — pick by environment:
- POSIX shells (Linux / macOS / Git Bash):
scripts/install-pre-push-hook.shinstalls a hook that runssecrets-scan.sh. - Cross-platform / enterprise Windows (Execution Policy / AppLocker / Constrained Language Mode blocks PowerShell, but Python is present):
scripts/install-pre-push-hook.pywiresscripts/secrets-scan.py— stdlib-only, full parity with the bash pair (same patterns, allowlist, severity tiers, exit codes). Idempotent; backs up any unmanaged pre-push hook topre-push.bak;--uninstallremoves only the managed hook.
# Python pair (Linux, macOS, Windows)
python3 scripts/install-pre-push-hook.py # install into cwd
python3 scripts/install-pre-push-hook.py --target-repo PATH # install into a specific repo
python3 scripts/install-pre-push-hook.py --uninstall # remove the managed hook
To add gitleaks as a second pre-push pass, append to .git/hooks/pre-push (after the existing inhouse scan):
# gitleaks pre-push gate (additional layer)
if command -v gitleaks >/dev/null 2>&1; then
gitleaks dir . --no-banner --redact --exit-code 1 >&2 || {
echo "[gitleaks] BLOCKING — secrets detected. Override: git push --no-verify"
exit 1
}
fi
3.2 GitHub Actions CI workflow
# .github/workflows/secret-scan.yml
name: secret-scan
on: [pull_request, push]
jobs:
gitleaks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- uses: gitleaks/gitleaks-action@v2
env:
GITLEAKS_NOTIFY_USER_LIST: '@security-team'
trufflehog-verified:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- name: trufflehog (verified only)
uses: trufflesecurity/trufflehog@main
with:
extra_args: --only-verified --fail
3.3 G_SECRETS_SCAN gate (S038 Batch B)
The gate at _meta/gates.py wraps all three scanners. Auto-picks gitleaks > trufflehog > in-house.
# Advisory mode (never fails build; reports findings)
python3 ~/.claude/skills/_meta/gates.py G_SECRETS_SCAN /path/to/project \
--secrets-mode advisory
# Strict mode (fails on any finding)
python3 ~/.claude/skills/_meta/gates.py G_SECRETS_SCAN /path/to/project \
--secrets-mode strict --scanner gitleaks
Exit codes match the existing gate contract:
- 0 = pass (no findings OR advisory mode)
- 2 = fail (strict mode AND findings detected)
- 3 = environmental error (no scanner available)
3.4 gitleaks custom config (.gitleaks.toml at repo root)
The dep-currency-check skill has an established allowlist convention. Mirror that pattern for project-specific noise:
# .gitleaks.toml
title = "<project>-gitleaks-config"
[extend]
useDefault = true
[allowlist]
description = "project-specific known FPs"
paths = [
'''docs/.*tutorial.*\.md''',
'''.*\.test\.(js|ts|py)$''',
]
regexes = [
'''aws_access_key_id\s*=\s*AKIAIOSFODNN7EXAMPLE''', # AWS docs example
'''ghp_[A-Za-z0-9]{36}.*example.*''', # placeholder PATs
]
4. Defense-in-depth layering
Use ALL THREE tools — they catch different things:
| Pattern | gitleaks | trufflehog | in-house |
|---|---|---|---|
| GitHub PAT (classic) | ✓ | ✓ (verifies) | ✓ |
| AWS access key | ✓ | ✓ (verifies) | ✓ |
| Slack bot token | ✓ | ✓ (verifies) | ✓ |
| OpenAI sk- key | ✓ | ✓ (verifies) | ✓ |
| Generic high-entropy string | ✓ | ✓ | ✗ (would FP-storm) |
| PEM private key header | ✓ | ✓ | ✓ |
| JWT in source | ✓ | ✗ | ✓ |
Inline password=... assignment |
partial | partial | ✓ (in-house has this specifically) |
| Internal hostname leak | ✗ | ✗ | ✓ (*.internal, *.corp) |
| Live-credential verification (the secret is actually valid) | ✗ | ✓ | ✗ |
Trufflehog's --only-verified flag is the headline 2026 improvement — by hitting each candidate against its real provider API, it eliminates almost all false-positives. The trade-off is speed (each verify is an HTTP round-trip) and the assumption that you trust your CI runner to make those API calls (in air-gapped environments, skip --only-verified).
5. Security Hardening
.gitignore.env*and*.pembefore adding the scan — the scan is the safety net, not the policy.- Pre-commit AND pre-push, not just one — pre-commit catches local mistakes; pre-push catches anything that bypassed it (rebase, cherry-pick, force-push to a new branch).
- CI gate on PR + push — local hooks can be bypassed with
--no-verify; CI cannot. - Custom config per project — every project accumulates legitimate FP patterns over time. Maintain a
.gitleaks.toml(or equivalent) with documented justifications. - Rotate on detection, don't just delete — if a real secret was committed, even briefly, treat it as leaked. Rotate the secret with the provider.
git filter-repoto scrub history is theatre if the secret was visible for any minute via cloning/pushing. - Audit log scanner runs — when, who, what findings. The G_SECRETS_SCAN gate emits stdout; capture it in CI artifacts.
- Don't ignore
--no-verifyoverrides — log who used the override and investigate. If--no-verifyis routine in your team, the hook is too noisy and needs allowlist tuning. - Tune the in-house allowlist with care — every entry is a documented exception. Review the allowlist quarterly; remove entries whose context no longer applies.
- Scan generated artifacts too — build outputs, Docker images (
trivy fsoverlaps here), test fixtures with mock secrets. A "test-only" secret committed by accident is still a secret if it works against any real service. - Keep scanner versions current via
dep-currency-check— gitleaks and trufflehog ship pattern updates regularly. A stale scanner misses new providers' secret formats.
6. Anti-patterns
| Anti-pattern | Why it fails | Correct approach |
|---|---|---|
--no-verify becomes routine |
Defeats the safety net; team normalises bypass | Tune the allowlist OR raise the false-positive rate as a real bug |
| One tool only | Each tool has blind spots; no single catalog is complete | Layer at least two of {gitleaks, trufflehog, in-house} |
| Scan only pre-commit | Force-push / rebase / cherry-pick / branch creation bypass commit-time hooks | Add pre-push AND CI |
Trust --only-verified to find everything |
Many tokens are not live-verifiable (proprietary providers, locked-down APIs) | Pair with regex scanner; treat live-verify as drift-reducer, not gospel |
| Treat detection as the whole defense | Detection finds what's already there; you also need policy (env vars, vault, .gitignore) | Detection is a backstop, not a strategy |
| Auto-install hooks across all user repos | Corrupts repos; surprises contributors; impossible to roll back cleanly | Opt-in per repo via bootstrap-environment.py prompt |
| Embed allowlist in scanner config without justification comment | Six months later nobody knows why X is allowlisted | Every allowlist entry MUST carry a one-line comment with the documented reason |
| Scan-and-delete without rotation | The secret was visible for at least one commit window; treat as leaked | Rotate at the provider; THEN scrub history (optional) |
7. Selection Cheatsheet
- Need a pre-commit hook right now →
gitleaks protect --staged - Need a CI gate with zero false positives → trufflehog
--only-verified --fail - Need a one-shot scan of an existing repo's full history →
trufflehog git file://. --no-update - Need a defense-in-depth backstop offline → in-house
scripts/secrets-scan.sh(always present in this ecosystem) - Need a programmatic gate inside bob WP-completion →
gates.py G_SECRETS_SCAN <root> --secrets-mode strict - Need a forge Step 1 advisory check →
gates.py G_SECRETS_SCAN <root> --secrets-mode advisory
8. Gotchas
| Gotcha | Detail |
|---|---|
| gitleaks pre-commit treats new files only | Use gitleaks dir . for full-repo scan; gitleaks protect --staged for staged-only |
trufflehog --only-verified needs network |
In air-gapped CI, drop the flag and accept the FP burden, OR pre-allowlist known FPs |
| In-house scanner allowlist is centralized | One scripts/secrets-scan.sh, one allowlist; cross-project FP-tuning is shared |
.gitleaks.toml is git-tracked |
Allowlist additions are visible to anyone who clones — don't write secrets INTO the allowlist itself (use regex patterns that match the shape) |
git filter-repo history scrub is destructive |
Coordinate with anyone holding the repo; force-push afterwards; rotate the secret regardless |
| Some legacy regex catalogs (in-house pre-2026) flagged tutorial placeholders | The in-house catalog has a curated allowlist for this — see scripts/secrets-scan.sh source |
| Docker image scans are NOT covered here | Use trivy image <ref> for image-mode scans (see docker-security skill) |
| Encrypted secrets at rest are not in scope | If you encrypt secrets in source (sops, git-crypt, age), the scanner sees the ciphertext and won't flag it — but that's intended. Decrypted files MUST be .gitignored. |
9. Update triggers (alf scans these)
- gitleaks major version bump (currently 8.x as of 2026-05)
- trufflehog major version bump (currently 3.x)
- New provider added to gitleaks default config (auto-detected via
gitleaks versionoutput) - New secret-shape provider widely deployed (e.g. a new cloud's tokens become common in the wild)
- In-house
scripts/secrets-scan.shpattern catalog update - Annual review on 2027-05-24
10. See Also
| Need | Skill |
|---|---|
| Dependency CVE scanning (parallel pattern) | dep-currency-check |
| Credential storage patterns (env / vault / keyring) | python-auth-security |
| LLM prompts must not contain secrets | llm-security (LLM06, LLM07) |
| Docker image secret scans | docker-security |
| Pre-commit hook install pattern (POSIX + Windows hardened PS1) | dep-currency-check (precedent) |
| The G_SECRETS_SCAN gate it wraps | ~/.claude/skills/_meta/gates.py |
| Bootstrap-environment hook offer | ~/.claude/skills/_meta/ + installer/bootstrap-environment.py |