Safety, cost, and cache
Three pre-send guards — secret scanner, .commitbrief/ guard, cost preflight — plus the local response cache and how to manage it.
CommitBrief runs three always-on guards before any LLM call —
plus an opt-in token preflight — and keeps a local response cache
so re-running a review on an unchanged diff is essentially free.
You can also inspect the exact prompt before it leaves your machine
with --show-prompt.
Secret scanner
Pre-send guard that flags credential-shaped strings in the diff before the provider call. Prevents accidental upload of API keys, private keys, tokens, etc. to a third-party LLM.
What it scans
- The diff — only added lines (
+-prefixed, excluding the+++ b/pathheader). Removed and context lines are skipped — the goal is to catch new leaks, not re-flag history that is already on disk somewhere. - User-authored rules content — any non-default
COMMITBRIEF.mdorOUTPUT.md. These join the system prompt verbatim, so a credential pasted into either file would leak just as surely as one in the diff. Embedded defaults are presumed clean and skipped.
Patterns matched
| Name | Pattern (regex) |
|---|---|
| AWS Access Key | AKIA[0-9A-Z]{16} |
| GitHub Token | gh[pousr]_[A-Za-z0-9]{36,} |
| GitLab Token | glpat-[A-Za-z0-9_-]{20,} |
| Anthropic API Key | sk-ant-[A-Za-z0-9_-]{40,} |
| OpenAI API Key | sk-(?:proj-|live-)?[A-Za-z0-9]{40,} |
| JWT | eyJ[A-Za-z0-9_-]{8,}\.eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,} |
| Stripe Live Key | sk_live_[A-Za-z0-9]{24,} |
| PEM Private Key | -----BEGIN [A-Z ]*PRIVATE KEY----- |
Adding your own patterns (v1.7.0)
The eight built-ins cover the common public token formats, but
internal or company-specific credentials need their own regexes.
guard.secret_patterns lets you add them on top of the built-ins
(ADR-0024):
guard:
secret_patterns:
- name: Acme Internal Token
pattern: 'acme_[A-Za-z0-9]{32}'
- name: Internal Webhook Secret
pattern: 'whsec_[A-Za-z0-9]{40,}'
Your patterns are additive: the built-in eight always run and
cannot be disabled or shadowed by a user entry (to turn the scanner
off wholesale, use guard.secret_scan: false). An invalid regex
aborts the review before any provider call, naming the offending
entry — a typo can’t silently disable a credential check. As with the
built-ins, only the line number and pattern name are ever reported,
never the matched text.
This key is config-file only — commitbrief config set rejects it
(it takes a list of objects, not a scalar). Hand-edit the YAML, or
add it under init/setup’s generated config.
What the user sees
⚠ Possible secrets detected in diff (2 line(s)):
line 42: AWS Access Key
line 87: GitHub Token, OpenAI API Key
Send to LLM anyway? [y/N]:
The matched substring is never printed — only line numbers and pattern names. This keeps the secret out of stderr (and any CI log that captures it) even when the scanner fires.
Behavior matrix
| Context | Outcome |
|---|---|
TTY, no --allow-secrets, no --yes |
Prompt; default no → abort. |
TTY, --yes |
Prompt still shows. --yes does not bypass the scanner. |
Non-TTY, no --allow-secrets |
Abort with Aborted (non-interactive); pass --allow-secrets to override. |
Any context, --allow-secrets |
Skip the scanner entirely. |
guard.secret_scan: false in config |
Skip the scanner entirely. |
The matched-line list is still printed to stderr in the
--allow-secrets case — you opted in, but you still see what
was flagged.
Disabling
# Per-invocation
commitbrief --staged --allow-secrets
# Per-config (whole binary)
commitbrief config set guard.secret_scan false
The whole-binary opt-out is appropriate if you have a separate secrets-management layer (gitleaks, trufflehog, pre-commit hooks) doing the same job and the prompt is just noise.
Prompt-injection scan of your rules (v1.7.0)
When you use a custom COMMITBRIEF.md or
OUTPUT.md, its content joins the system prompt — so a stray
“ignore previous instructions” pasted into either file could hijack
the reviewer. CommitBrief scans these files for prompt-injection
phrasing (ignore previous instructions, you are now…,
system prompt, …) before they enter the prompt (ADR-0025).
On a match the CLI prints a non-blocking warning with the file
and line numbers and continues — it never aborts, because it is
your own file and you may have a legitimate reason for the wording.
The embedded default rules are trusted and skipped entirely. The
scan is on by default; toggle it with guard.injection_scan:
guard:
injection_scan: true # default; false silences the warning
commitbrief config set guard.injection_scan false
This complements the always-on immutability guard that wraps your rules in the prompt (see Review rules): the envelope tells the model to treat project rules as data, while this scan surfaces suspicious phrasing to you before the call is made.
Cost preflight
Pre-send guard that estimates the dollar cost of a review and prompts/aborts when it exceeds the configured threshold. Catches “oops, I pasted a 50k-line generated file” before tokens are actually spent.
When it runs
Right after the cache lookup, before the provider call. A cache hit skips the preflight entirely (no provider call, no cost).
How the estimate is computed
estimated_input_tokens = chars / 4 (well-known approximation)
estimated_output_tokens = clamp(input / 4, 200, 1500)
estimated_cost = provider.Pricing(model).Cost(...)
Output tokens are capped at 1500 and floored at 200. Underestimating output matters: a high-priced output model would otherwise systematically hide its actual bill.
provider.Pricing(model) reads a built-in $/1M-token snapshot
per model. If a rate has drifted or you have a negotiated price,
override it with
providers.<name>.pricing.<model> —
the override feeds this estimate, the verbose footer, and the
cached-cost figure (v1.2.0, OQ-09).
Threshold
cost:
warn_threshold_usd: 0.50 # default; <=0 disables
Bump it for scheduled jobs (a CI runner doing 100 reviews/day cares less about a single $1 review):
commitbrief config set cost.warn_threshold_usd 5.0
Behavior matrix
| Estimate | Context | Outcome |
|---|---|---|
<= threshold |
any | Silent. Review proceeds. |
> threshold |
TTY, no --no-cost-check, no --yes |
Prompt; default no → abort. |
> threshold |
TTY, --yes |
Prompt still shows. --yes does not bypass. |
> threshold |
Non-TTY, no --no-cost-check |
Abort. |
| any | --no-cost-check |
Skip the preflight entirely. |
Providers with zero pricing
Ollama, claude-cli, gemini-cli, and codex-cli return
Pricing{} (all zero). The preflight short-circuits silently — estimated cost
is always 0, never above the threshold. The verbose footer shows
— instead of a dollar figure.
Token preflight (opt-in)
Off by default. When enabled, CommitBrief estimates the prompt’s
token count and, if it exceeds the active model’s context window,
prompts (TTY) or aborts (non-TTY) before the paid call — a
friendly catch instead of a raw provider 400 context length exceeded. New in v1.4.0 (ADR-0003).
commitbrief config set guard.token_preflight true
guard:
token_preflight: false # default; true = confirm/abort on overflow
It’s opt-in because the estimate is the same chars / 4 heuristic
the cost preflight uses — a false positive shouldn’t block a review
nobody asked to guard. On a TTY you can still confirm and send
anyway; non-interactively it aborts with a message pointing at this
key. Set guard.token_preflight=false to turn it back off.
Inspecting the outgoing prompt
Want to know precisely what leaves your machine? --show-prompt
assembles the full system + user prompt that would be sent, prints
it, and exits — no provider call, no cache lookup, no cost. New
in v1.4.0.
commitbrief --staged --show-prompt
commitbrief diff main...feature --show-prompt --cli claude --with-context
commitbrief --staged --show-prompt --output prompt.txt # write to a file
It reflects every prompt-shaping flag — the scope, --with-context,
--cli / --provider, and --lang — because it prints the
already-assembled prompt. Use it to audit data egress, debug a
custom COMMITBRIEF.md, or copy the prompt into another tool. It’s
the full-text companion to dry-run,
which reports only metadata (file counts, token estimate, cost).
The .commitbrief/ guard
Any diff touching files under .commitbrief/ (excluding the root
COMMITBRIEF.md and .commitbriefignore) prompts before any LLM
call. The rationale: .commitbrief/ files are usually
user-specific (per-repo config, OUTPUT.md template) and committing
them may break other developers’ configurations or leak API keys.
| Context | Outcome |
|---|---|
| TTY | Prompt; n → abort, y → proceed. |
Non-TTY, no --yes |
Auto-abort. |
Any, --yes |
Proceed without prompting. |
This is the one guard --yes still bypasses (since v0.9.1
the secret scanner and cost preflight have dedicated bypass flags).
Cache
CommitBrief caches LLM responses on disk so re-running a review on an unchanged diff is essentially free.
Cache key
SHA-256 over the concatenation:
diff_text + system_prompt + provider_name + model + lang_code + schema_version
Change any one input and you get a fresh review:
- Editing the diff (staging more files, amending the commit, etc.).
- Editing
COMMITBRIEF.md(system prompt differs). - Switching providers (
--provider gemini). - Switching models (
--model gpt-4o-mini). - Switching locale (
--lang tr).
Location
<repo-root>/.commitbrief/cache/ — one JSON file per cached
review. Per-repo: commitbrief cache clear in one repo does not
touch another repo’s cache. Gitignored automatically.
TTL and disable
cache:
enabled: true # false skips reads + writes entirely
ttl_days: 7 # 0 → DefaultTTL (7 days); cannot be negative
max_size_mb: 0 # on-disk cap; 0 = unlimited; >0 evicts oldest first
Per-invocation: --no-cache to force a fresh provider call.
Management commands
# Wipe everything for this repo.
commitbrief cache clear
# Bounded cleanup with defaults (keep newest 500 + last 7 days).
commitbrief cache prune
# Trim aggressively.
commitbrief cache prune --keep-last 100 --older-than 1d
# Per-provider/model scope.
commitbrief cache prune --provider anthropic --model claude-opus-4-7
# Summarize the cache (read-only).
commitbrief cache stats
# Inspect one entry by its cache key (the SHA-256 from --verbose / dry-run).
commitbrief cache inspect 3f9a… --show-content
commitbrief cache prune accepts duration units d / w / m
(30 days) / y (365 days). Decimals, negatives, and stdlib
h/m/s shorthand all reject — so off-by-one surprises are
impossible.
Inspecting the cache (v1.3.0)
cache stats prints a read-only summary — entry count, total size,
oldest/newest timestamps, the configured size limit, and a
per-provider/model breakdown:
Cache: 142 entr(y/ies), 2.4 MB at /repo/.commitbrief/cache.
Oldest 2026-05-20T08:14:02Z · newest 2026-05-28T19:41:55Z.
Size limit: unlimited (set cache.max_size_mb to bound).
By provider/model:
anthropic claude-opus-4-7 128 2.2 MB
openai gpt-4o 14 220.0 KB
cache inspect <key> dumps one entry’s metadata (provider, model,
language, created-at, TTL + freshness, token counts, on-disk size). The
.json suffix is optional; the cached review body is shown only with
--show-content:
Key: 3f9a…
Provider: anthropic
Model: claude-opus-4-7
Lang: en
Created: 2026-05-26T01:23:45Z
TTL: 604800s (expires 2026-06-02T01:23:45Z, fresh)
Format: json
Size: 3.4 KB
Tokens: input=4231 output=1503 cached=0
Diff hash: sha256:abc…
Size-bounded eviction (v1.3.0)
Set cache.max_size_mb to an integer >0 to bound the on-disk cache.
After each write, if the directory exceeds the cap, the oldest entries
(by created-at) are evicted oldest-first until it fits; the entry just
written is never evicted, so a single entry larger than the cap survives
over-budget. The default 0 keeps the cache unlimited — cache prune
stays the manual, window-based stand-in.
commitbrief config set cache.max_size_mb 50 # bound the cache to ~50 MiB
On-disk format
{
"version": 1,
"created_at": "2026-05-27T18:29:13Z",
"ttl": 604800,
"key": {
"diff_hash": "sha256:abc123...",
"system_prompt_hash": "sha256:def456...",
"provider": "anthropic",
"model": "claude-opus-4-7",
"lang": "en"
},
"result": {
"content": "<LLM response>",
"format": "json",
"tokens": { "input": 2105, "output": 526, "cached": 0 }
}
}
result.format is json on the happy path, markdown-fallback
when the LLM produced unparseable JSON twice, or plain-text for
CLI-tool-backed providers.
Cache-hit reporting
On a hit, the verbose footer shows Saved: $X (the cost figure
that would have been spent) and tokens are marked as
(local cache hit). The pipeline still rebuilds the prompt (so
the dry-run cache-key matches) but skips the provider call.
See also
- Configuration —
guard.secret_scan,guard.secret_patterns,guard.injection_scan,cost.warn_threshold_usd,cache.*fields. - Global flags —
--allow-secrets,--no-cost-check,--no-cache,--yes. - Signal control — the baseline and inline
commitbrief-ignoreare true removals (they change--fail-onand--json), unlike the display-only--min-severity. - Troubleshooting — when guards fire unexpectedly.