Credential audit — leaks
commitbrief leaks scans every tracked file plus the added lines of your git history for committed credentials — the same pattern set the review path uses, with no provider call, no cache, and no cost. Exits 1 on a hit, so it gates CI out of the box.
commitbrief leaks audits the repository for committed credentials —
every tracked file in the working tree, plus the lines added by
historical commits. It is fully deterministic: the same pattern set the
secret scanner uses, no
provider call, no cache, no cost.
Why this is a separate command
The pre-send secret scanner is a gate. It runs before a review is sent and sees exactly one thing: the added lines of that diff. That is the right shape for stopping a leak in flight, and the wrong shape for two questions people actually ask:
- Is there a secret in my working tree right now? The gate never reads files — only a diff.
- Did anyone ever commit one? The gate reads the added lines of one diff. A key that was committed and later removed is still in the history — and still reachable by anyone who cloned or forked the repo before you scrubbed it.
leaks answers both.
Usage
commitbrief leaks # tracked files + last 200 commits
commitbrief leaks --no-history # working tree only, fast
commitbrief leaks main..HEAD --no-worktree # exactly that range
commitbrief leaks --author alice --start-date 2026-01-01
commitbrief leaks --max-commits 10000 # the most history one run can cover (slow)
commitbrief leaks --patterns # what it looks for, then exit
The two halves
Both run by default, and each has its own off-switch — so a positional range narrows the history half without silently disabling the tree.
| Half | What it reads |
|---|---|
| Working tree | Every tracked file, whole — not just added lines. |
| History | The added lines of each selected commit, attributed to its hash, author, and date. |
Untracked files are deliberately out of scope. An untracked, gitignored
.env is where a secret is supposed to live, and it cannot leak
through git — flagging it would be noise.
Added lines only, for history, is precisely how a removed-but-committed key is found. Scanning context lines instead would re-report the same key once per commit that happened to touch the file near it.
Flags
| Flag | Default | Notes |
|---|---|---|
--no-worktree | off | Skip the working-tree half. |
--no-history | off | Skip the commit-history half. Disabling both is an error. |
--patterns | off | Print the effective pattern set (built-ins + your guard.secret_patterns) and exit. |
--max-commits <N> | 200 | Bound the history half. 0 means the default of 200, not unlimited; pass a larger number (up to 10000) to scan more. Truncation is always stated in the human report. |
--fail-on <sev> | any | See Exit code — the default differs from every other command. |
--sarif | off | Write the findings as a SARIF 2.1.0 log for GitHub Code Scanning. Requires v1.21.0 or later. See SARIF output. |
Every filter applies. The
commit filters
(--author, --committer, --start-date, --end-date, --text)
narrow which commits the history half reads; the path filters
(--file, --dir, --exclude-file, --exclude-dir) narrow which
files either half reads.
Output
Scanned 613 file(s) across 50 commit(s).
Skipped 4 file(s): binary or over the size cap.
CRITICAL config/prod.yml:14 AWS Access Key
CRITICAL deploy/legacy.env:3 Stripe Live Key (a1b2c3d, alice)
2 possible credential(s) found. Rotate them — a key in git history stays reachable in every existing clone.
The trailing (hash, author) marks a history hit: the line may be
long gone from your working tree. Coverage is always stated — files
scanned, commits scanned, files skipped, and whether the walk was
truncated — because “no credentials found” means nothing without knowing
what was looked at. --quiet drops the coverage and skipped-files lines but
never the truncation warnings.
The commit walk itself stops at 10000 commits, a ceiling --max-commits
cannot lift. On a longer history the report adds a ⚠ line saying only
the newest 10000 commits were inspected and older history was not
scanned (Requires v1.21.0 or later).
What it never prints
The matched text. A finding carries a file, a line, and the pattern
names — nothing else. The scanner reads whole files, so its own report
must not become a second copy of the secret in your terminal scrollback,
your CI log, or the pastebin you were about to send it to. In --json,
snippet is always absent for the same reason.
Severity
Hits are critical, with two deliberate exceptions at high:
- JWT — expired and sample tokens are common in fixtures; it is the one built-in with a real false-positive rate.
- User patterns from
guard.secret_patterns— CommitBrief cannot judge how bad a hit on your house format is.
Exit code
leaks is the one command where --fail-on defaults to any: a hit
exits 1, so it gates CI with no extra configuration. The report is
printed first, then the run fails — you get the findings and the
non-zero exit.
commitbrief leaks # exit 1 if anything is found
commitbrief leaks --fail-on none # report only, always exit 0
commitbrief leaks --fail-on critical # ignore the `high` tier
Feeding the policy gate
--json emits the standard
schema v1 document, so
guard consumes it directly with no new
plumbing:
commitbrief leaks --json --fail-on none | commitbrief guard --from-json -
meta.provider is "builtin" and cost_usd is 0 — there was no model
call. When the commit walk hit its 10000-commit ceiling, meta.walk_truncated
is true; it is absent otherwise (Requires v1.21.0 or later). When
--max-commits cut the selected history short, meta.truncated is true;
it is absent when the window covered the whole history (Requires v1.21.1 or
later). The two are independent: a larger --max-commits lifts the second,
nothing lifts the first. That reuse is deliberate: a separate scan schema would have bought
nothing and broken this pipeline.
SARIF output
Requires v1.21.0 or later. --sarif writes the findings as a SARIF
2.1.0 log under the commitbrief/leaks/ category; use -o to write a
file. The human report moves to stderr and --fail-on still sets the
exit code. It does not combine with --json or --patterns.
commitbrief leaks --sarif -o leaks.sarif
Each result uses the rule id sec.hardcoded-secret and carries the
pattern names and, for a history hit, the short commit hash. It never
carries the matched text, a snippet or an author.
Code Scanning treats an alert missing from the upload as fixed, so a
partial scan would close alerts it never checked. --sarif therefore
writes a log only for a complete scan:
- Combining it with
--no-worktree,--no-history,--file,--dir,--exclude-file,--exclude-dir,--author,--committer,--start-date,--end-date,--textor a git range is a usage error. - If the history scan stops at the
--max-commitswindow, no file is written and the command exits 1. Re-run with a larger--max-commits N. - If the history is longer than the commit walk can cover, no file is
written and the command exits 1.
--max-commitscannot raise that limit.
Upload it with category: commitbrief/leaks. See
SARIF and Code Scanning.
Limits — read these before trusting a clean report
- The ignore layers apply.
leakshonors the built-in patterns and.commitbriefignoreexactly as a review does, so a key committed intovendor/**,dist/**, or a*.min.jsis not reported. This keeps.commitbriefignorethe single place a project says “not this”. Narrow with--dirif you need the wider sweep. - Regex only, no entropy scoring. A high-entropy blob with no recognizable prefix is invisible. The patterns are deliberately tight — length floors and fixed prefixes — which is what keeps false positives rare. This is a targeted check, not a general-purpose secret scanner.
- Binaries and large files are skipped (a NUL byte in the first 8 KiB, or over 5 MiB). Both are counted in the report.
- Untracked files are not scanned.
--max-commitsbounds history at 200 by default. Truncation is always stated in the human report,--quietincluded; under--jsonit is flagged asmeta.truncated, and the walk ceiling asmeta.walk_truncated. The walk itself stops at 10000 commits whatever--max-commitssays.
If it finds something
Rotate the credential at its provider. That is the fix.
Removing the line, amending the commit, or rewriting history does not reach clones, forks, CI caches, or anyone’s local checkout that already has the commit. Treat a committed key as compromised from the moment it was pushed.
See also
- Safety, cost, and cache — the pre-send gate this complements, and where you register your own patterns.
- Review scopes — the commit and path filters that narrow a scan.
- Policy gate — guard — the gate
--jsonfeeds. - Output formats — the
document
--jsonemits.