// docs · v1.x

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

  1. The diff — only added lines (+-prefixed, excluding the +++ b/path header). Removed and context lines are skipped — the goal is to catch new leaks, not re-flag history that is already on disk somewhere.
  2. User-authored rules content — any non-default COMMITBRIEF.md or OUTPUT.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

  • Configurationguard.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-ignore are true removals (they change --fail-on and --json), unlike the display-only --min-severity.
  • Troubleshooting — when guards fire unexpectedly.