Credential Scanning
QDash runs three tools to keep secrets out of the repository: Lefthook orchestrates a pre-commit hook, Gitleaks blocks staged secrets locally before they enter git history, and Trufflehog scans git history in CI for verified leaks.
Tools
| Tool | Where it runs | What it scans | Configuration |
|---|---|---|---|
| Lefthook | Local pre-commit | Triggers Gitleaks against staged files | lefthook.yml |
| Gitleaks | Local pre-commit | Pattern match for secrets in working tree | .gitleaks.toml |
| Trufflehog | CI only (push, PR to main/develop) | Git history; only verified secrets are reported | .trufflehog-exclude-paths.txt |
CI definitions live in .github/workflows/secret-scan.yml. Installation instructions are in Setup.
Why two scanners
The two tools support different allowlist granularities, which is why they own different stages:
| Path allowlist | Literal-string allowlist | |
|---|---|---|
| Gitleaks | yes | yes (regex, in .gitleaks.toml) |
| Trufflehog | yes (.trufflehog-exclude-paths.txt) | no |
Because Gitleaks can allowlist specific values, it can be configured strictly: only the literals in .gitleaks.toml are exempt — anything else credential-shaped is rejected. That precision makes it the right gate at pre-commit, where it blocks credentials before they enter git history.
Trufflehog has no string-level allowlist, so the only way to suppress a known false positive is to drop the entire file. Compensating for that, it verifies findings by probing the upstream provider to check whether a credential is live, and walks full git history in CI on push and PR. Its role is ongoing detection of live secrets — running on remote CI infrastructure and reaching out to remote providers to confirm validity.
The result is a clear division of labor: Gitleaks is the strict, fast, syntactic gate at commit time; Trufflehog is the slower, semantic, live-credential check that runs in CI. Gitleaks is not run in CI because the gitleaks-action requires a paid license for organization repositories; the local pre-commit hook covers the same purpose, and any commit that slips past it is caught by Trufflehog's history scan.
Lefthook
lefthook.yml defines a single pre-commit command that calls gitleaks protect --staged. Running lefthook install once writes the git hook into .git/hooks/. If the Gitleaks binary is missing, Lefthook skips the step rather than failing the commit, so contributors on platforms without the binary are not blocked.
Gitleaks
Gitleaks scans content for known secret patterns (API keys, tokens, private keys). The repo config .gitleaks.toml sets [extend] useDefault = true to inherit all built-in detection rules (AWS, GitHub, GCP, Azure, Slack, Stripe, etc.) and layers two kinds of allowlist on top:
paths— files whose entire contents are excluded from scanning.regexes— literal strings that are ignored wherever they appear.
Allowlist entries and why they are safe
| Entry | Kind | Why allowlisting is safe |
|---|---|---|
ui/src/client/ | path | Orval-generated TypeScript API client. Regenerated from docs/oas/openapi.json by task generate; any hand edit is overwritten on the next run. |
ui/src/schemas/ | path | Orval-generated TypeScript schemas. Same generation lifecycle as ui/src/client/. |
ui/bun.lock | path | Bun lockfile. Contains package integrity values that look high-entropy but are public package digests. Regenerated by bun install. |
docs/oas/openapi.json | path | OpenAPI document exported from the FastAPI app via curl /openapi.json in task generate. Not hand-edited. |
mongodb://root:example@mongo:27017 | regex | Local-dev compose default. The hostname mongo resolves only inside the compose network and the password is literally the word example — it is not a secret in any environment, just an example value. |
The shared property of every path entry is generation-owned: the file is rewritten by a tool, not by a human. A developer pasting a real credential into one of these files would lose it on the next regeneration step, so the scanning blind spot has no practical attack surface. The single regex entry covers a fixed example value that is identical across every developer's machine.
Trufflehog
Trufflehog walks the git history and reports only verified findings — meaning it actively probed the upstream provider and confirmed the credential is live. False positives are rare, so a hit should be treated as a real leak.
Paths in .trufflehog-exclude-paths.txt are excluded from history scanning. Each line is a regex matched against the file path:
\.gitleaks\.toml
docs/design/api-testing-guidelines\.md
docs/development/api/testing\.md
docs/development/credential-scan\.md
poetry\.lock
scripts/migrate_user_tokens\.py
tests/conftest\.pyExclude entries and why they are safe
Because Trufflehog walks full git history, the exclude list must cover both currently-tracked files and files that only exist in older commits.
| Entry | Status | Why excluding is safe |
|---|---|---|
\.gitleaks\.toml | tracked | Contains the Gitleaks allowlist itself — the literal mongodb://root:example@mongo:27017 and placeholder tokens (ADMIN_TOKEN, YOUR_TOKEN) would otherwise be re-flagged as credentials. None are real secrets; see the Gitleaks allowlist table above. |
docs/design/api-testing-guidelines\.md | deleted (history only) | Removed in commit 50b0fd31. Old API testing guideline document that referenced example tokens. Excluded so historical commits do not trigger findings. |
docs/development/api/testing\.md | tracked | API testing guide. May reference token names and example fixtures in code samples. None are live credentials. |
docs/development/credential-scan\.md | tracked | This document itself. Contains example tool output with mock values such as AKIAIOSFODNN7EXAMPLE and fabricated commit hashes used to illustrate finding format. |
poetry\.lock | deleted (history only) | Removed in commit e840268b when the project moved to uv. Lockfile entries contain public package integrity digests that look high-entropy. |
scripts/migrate_user_tokens\.py | deleted (history only) | Removed in commit cca42d62. One-shot migration script that operated on user-token records; variable names and field-key strings match credential-detector patterns even though no live secret was ever embedded. |
tests/conftest\.py | tracked | Pytest fixtures set environment variables to obvious dummy values ("test-token", "test-openai-key", etc.) so the test client can boot without real credentials. None are real. |
Trufflehog only reports verified findings, so unverified pattern matches in these files would not have triggered an action anyway — but excluding them keeps scans quiet and prevents future verifiers (added by Trufflehog upstream) from probing fixture values.
Running Locally
The repository ships task targets that mirror the CI commands.
| Command | What it does |
|---|---|
task scan-leaks | Gitleaks against the full working tree |
task scan-leaks-staged | Gitleaks against staged files only (same as pre-commit) |
task scan-secrets | Trufflehog against git history, verified findings only |
task scan-secrets-all | Trufflehog against git history, including unverified hits |
task check (the standard pre-push gate) includes scan-leaks.
Reading Output
Gitleaks
A finding looks like:
Finding: AKIAIOSFODNN7EXAMPLE
Secret: AKIAIOSFODNN7EXAMPLE
RuleID: aws-access-token
Entropy: 3.95
File: src/qdash/api/config.py
Line: 42
Commit: (staged)
Author: <staged>
Date: (staged)
Fingerprint: src/qdash/api/config.py:aws-access-token:42Key fields when triaging:
- RuleID — which detector matched. Gives an immediate hint at the secret type.
- File / Line — where to look. For staged scans, the line number is in the working tree.
- Fingerprint — stable identifier for the finding; use it if you need to discuss a specific hit.
The exit code is non-zero on any finding, which is what blocks the commit.
Trufflehog
A verified finding looks like:
Found verified result 🐷🔑
Detector Type: AWS
Decoder Type: PLAIN
Raw result: AKIA****************
File: src/qdash/api/legacy.py
Line: 87
Commit: 3f2a1b0c9d8e7f6a5b4c3d2e1f0a9b8c7d6e5f4a
Repository: file://.
Email: dev@example.com
Timestamp: 2024-08-12 14:22:10 +0000Because Trufflehog only flags verified secrets, the credential is almost certainly active. Rotate it before doing anything else; the git history rewrite comes after.
Handling False Positives
Gitleaks — add the path or regex to .gitleaks.toml under [allowlist]:
[allowlist]
paths = [
'''path/to/safe/file\.json''',
]
regexes = [
'''dev-only-fixture-token-[a-z0-9]+''',
]Prefer narrow regexes over broad path allowlists — broad allowlists hide real future leaks in the same file.
Trufflehog — add the path regex to .trufflehog-exclude-paths.txt. Only do this for files that legitimately contain credential-shaped strings (test fixtures, documentation about credentials). If the finding is verified, do not allowlist; rotate.