ov scan
ov scan finds secrets committed or staged in your codebase. No file contents, line context, file paths, or repository data ever leave your machine — not to OpaqueVault, not to anyone. Detection is entirely local, and there is no telemetry and no account required for a plain scan.
Three things do make network calls, and all three are worth knowing about up front:
| What | When | What is sent |
|---|---|---|
--verify |
Only with the explicit --verify flag, after a y/N consent prompt |
The credential value itself, as an auth header, to that credential’s own vendor (GitHub, Stripe, …). Never to OpaqueVault. |
--fix |
Only with the explicit --fix flag |
The credential value you type at the prompt, encrypted client-side, stored via the OpaqueVault API. Requires login. |
| Update check | Every ov command, at most once per 24h, TTY only, never in CI |
Nothing about your scan — a plain GET for the latest published version string. |
The separate ov scan-diff-base CI helper may fetch from your repo’s own origin remote to resolve a merge base for diff-aware scanning — that’s standard git history retrieval, not ov scan itself, and it never transmits file contents or findings.
ov scan [path] [flags]path defaults to the current directory if omitted.
What it detects
Section titled “What it detects”26 detectors ship built in. This is the complete list — the ID in the first column is the name that appears in scan output, in severity_overrides, and as the basis for --fix secret names.
Cloud and platform credentials
Section titled “Cloud and platform credentials”| Detector ID | Severity | What it matches |
|---|---|---|
aws-access-key |
Critical | AKIA + exactly 16 uppercase alphanumerics, word-bounded |
aws-secret-key |
Critical | A 40-char [A-Za-z0-9/+=] value preceded on the same line by AWS keyword context — see below |
github-token |
Critical | ghp_/ghs_/gho_/ghr_ + 36 or more alphanumerics, or github_pat_ + 59 or more |
gitlab-token |
Critical | glpat- + 20 or more chars |
stripe-key |
Critical (sk_test_ → High) |
sk_live_, sk_test_, or rk_live_ + 24 or more alphanumerics |
stripe-webhook-secret |
Critical | whsec_ + 20 or more alphanumerics |
slack-token |
Critical | xoxb-/xoxp-/xoxa-/xoxr-/xapp- + 20 or more chars |
notion-token |
Critical | ntn_ + exactly 46, or secret_ + exactly 43 alphanumerics |
linear-token |
Critical | lin_api_ + exactly 40 hex chars |
npm-token |
Critical | npm_ + exactly 36 alphanumerics |
discord-bot-token |
Critical | 17–19 digit snowflake . 6 chars . 27 chars |
vercel-token |
Critical | vercel_ or vc_ + 24 or more alphanumerics |
supabase-token |
Critical | sbp_ + exactly 40 hex chars |
opaquevault-key |
Critical | ov_live_ or ov_test_ + 32 or more chars |
AI provider keys
Section titled “AI provider keys”| Detector ID | Severity | What it matches |
|---|---|---|
anthropic-key |
Critical | sk-ant-api<digit>- + 20 or more chars |
openai-key |
Critical | sk-proj- + 20 or more chars, or legacy sk- + 48 or more alphanumerics |
google-ai-key |
Critical | AIza + exactly 35 chars |
groq-key |
Critical | gsk_ + exactly 52 alphanumerics |
huggingface-token |
Critical | hf_ + 34 or more alphanumerics |
replicate-token |
Critical | r8_ + exactly 40 alphanumerics |
Generic and structural
Section titled “Generic and structural”| Detector ID | Severity | What it matches |
|---|---|---|
private-key |
Critical | A PEM header line -----BEGIN [TYPE ]PRIVATE KEY[ BLOCK]----- |
jwt |
High | Three dot-separated base64url segments starting eyJ, each 10+ chars |
postgres-url |
High | postgres:// or postgresql:// URL with a non-empty user:password@host segment |
url-with-password |
Medium | Any scheme://user:password@host URL with a 4+ char password, except postgres:// (covered above) |
high-entropy |
Low | 20+ char runs passing an adaptive entropy threshold — see below |
base64 |
inherits | A 100+ char base64 blob that decodes to printable text; the decoded text is rescanned with all 25 other detectors |
About the aws-secret-key detector
Section titled “About the aws-secret-key detector”This detector does no entropy analysis and has no adjacency-to-an-AWS-key-ID logic. It is a same-line keyword match. All of the following must appear on one physical line, case-insensitively:
aws …≤20 chars… secret …≤20 chars… (key|access) …≤10 chars… (=|:) ["']? <40-char value>The value is exactly 40 characters from [A-Za-z0-9/+=] — not “40 or more”. Two practical consequences: a bare 40-char string with no aws…secret…key preamble is not flagged by this detector (it may still trip high-entropy at Low), and a .env file with the name on one line and the value on the next will not match, because the pattern cannot span a newline.
About entropy detection
Section titled “About entropy detection”high-entropy applies a length-banded Shannon threshold (AdaptiveEntropyThreshold), not a flat 4.5:
| Value length | Threshold (bits/char) |
|---|---|
| 20–31 | 4.5 |
| 32–47 | 4.6 |
| 48–63 | 4.7 |
| 64 and up | 4.8 |
A value is considered when its length is 20 or more, and it is flagged only when entropy is strictly greater than the band threshold — exactly 4.5 does not flag. UUIDs and path-like strings are excluded. A value below the Shannon threshold can still be flagged by a per-character-class check, which is what catches restricted-alphabet hex keys.
The same detection engine powers the MCP context interceptor — patterns that catch committed secrets also guard the results OpaqueVault’s MCP tools return to Claude.
Output
Section titled “Output”FOUND 3 leaked secret(s) in your repo.
scripts/deploy.sh:31 aws-access-key AKIA************MPLE config/database.yml:14 postgres-url postgres://admin:p*****@prod... .env.backup:3 stripe-key sk_live_********************
Store them safely: ov secret set AWS_ACCESS_KEY ov secret set POSTGRES_URL ov secret set STRIPE_KEY
→ https://opaquevault.com/docs/getting-started/quickstartPreviews are redacted — the full value is never written to output or logs. It leaves your machine only on the two explicit opt-in paths described above (--verify and --fix).
The suggested secret names are derived mechanically from the detector ID: uppercase it and replace - with _. So postgres-url → POSTGRES_URL and stripe-key → STRIPE_KEY.
All 18 flags, with their defaults.
| Flag | Default | Description |
|---|---|---|
--fix |
false |
Interactively store found secrets in OpaqueVault and print git history cleanup instructions. Requires login. |
--fail-on |
medium |
Lowest severity tier that causes exit 1: verified, critical, high, medium, low, info, none |
--format |
auto |
Output format: human, json, sarif, github (auto = github when $GITHUB_ACTIONS=true, else human) |
--config |
.ovscan.yaml |
Path to the ov scan config file |
--include-only-paths-from-stdin |
false |
Scan only the newline- or NUL-delimited paths read from stdin (diff-aware / pre-commit; auto-detects git diff -z). Paths resolve from the current directory, so run it from the repo root. |
Verification
Section titled “Verification”| Flag | Default | Description |
|---|---|---|
--verify |
false |
Verify each detected credential against its vendor’s live API. Sends credentials to third parties — see below. |
--yes, -y |
false |
Skip the verification consent prompt (for CI / non-interactive use; also OV_VERIFY_NO_PROMPT=1) |
--verify-quiet |
false |
Accepted but not yet functional. Reserved for suppressing non-verified-live findings. |
--verify-rotated |
false |
Accepted but not yet functional. Reserved for re-verifying baseline findings to surface rotation status. |
Git history
Section titled “Git history”| Flag | Default | Description |
|---|---|---|
--history |
false |
Also scan full git history (all branches, stashes, dangling blobs, git notes). Cannot be combined with --fix. |
--since |
— | Limit history scan to commits after this date (YYYY-MM-DD or RFC3339); requires --history |
--max-blob-size |
10485760 (10 MB) |
Per-blob size cap for the history scan, in bytes; requires --history |
--time-budget |
0 (unbounded) |
History-mode safety valve, e.g. 10m |
Baseline
Section titled “Baseline”| Flag | Default | Description |
|---|---|---|
--baseline |
— | Baseline snapshot file; only flag findings not in it (defaults to .ovscan-baseline.json if that file exists) |
--baseline-init |
false |
Write current findings to the baseline and exit 0 |
--baseline-refresh |
false |
Re-walk the working tree and add new findings to an existing baseline (preserves the HMAC key) |
--include-verified |
false |
With --baseline-init in local mode: allow baselining of verified-live findings |
--allow-ci-baseline |
false |
Required to run --baseline-init in CI; intentionally awkward to discourage CI-time use |
Mutually exclusive combinations that hard-error: --include-only-paths-from-stdin with a positional path or with --history; --baseline-init with --baseline-refresh; --history with --fix. Using --since or --max-blob-size without --history warns on stderr but continues.
Verifying findings are live (--verify)
Section titled “Verifying findings are live (--verify)”By default ov scan tells you a credential looks like a Stripe key. --verify tells you whether it still works — which is the difference between a stale test key and an active incident.
ov scan --verifyBecause of that, --verify asks for consent before making any network call:
Warning: --verify will send each detected credential to its vendor's live APIto confirm it is active. These calls appear in vendor activity logs.Continue? [y/N]Only y proceeds. Anything else prints Verification skipped. and the scan continues without verification. In CI, pass --yes or set OV_VERIFY_NO_PROMPT=1 (any non-empty value suppresses the prompt).
13 of the 26 detectors are verifiable. Each uses a read-only “who am I” endpoint chosen to be as low-impact as possible — Stripe uses /v1/balance rather than a charges list, and the Google AI key goes in a header rather than a query string so it stays out of URL access logs.
| Verifiable | Endpoint |
|---|---|
github-token |
api.github.com/user |
gitlab-token |
gitlab.com/api/v4/user |
stripe-key |
api.stripe.com/v1/balance |
slack-token |
slack.com/api/auth.test |
linear-token |
api.linear.app/graphql |
npm-token |
registry.npmjs.org/-/whoami |
vercel-token |
api.vercel.com/v2/user |
anthropic-key |
api.anthropic.com/v1/models |
openai-key |
api.openai.com/v1/models |
google-ai-key |
generativelanguage.googleapis.com/v1/models |
groq-key |
api.groq.com/openai/v1/models |
huggingface-token |
huggingface.co/api/whoami |
replicate-token |
api.replicate.com/v1/account |
The remaining detectors have no verifier and always render as ? unverified: aws-access-key and aws-secret-key (SigV4 needs the paired secret), plus high-entropy, jwt, postgres-url, url-with-password, private-key, notion-token, discord-bot-token, supabase-token, opaquevault-key, and stripe-webhook-secret.
Results map as: HTTP 2xx → ✓ ACTIVE; 401/403 → ✗ inactive; anything else, including timeouts, 5xx, and rate-limit 429s → ? unverified. Verification runs at most 5 concurrent requests with a 5-second timeout each, and there is no retry or backoff.
Background update check
Section titled “Background update check”Independently of scanning, every ov command — including ov scan — may fetch https://releases.opaquevault.com/latest.txt in the background to tell you a newer version exists. It sends nothing about your repository or your scan; it is a plain GET for a short version string, capped at 64 bytes with a 3-second timeout.
It is skipped entirely — no network call at all — when any of these hold:
OV_NO_UPDATE_CHECKis set to any non-empty valueCIis set to any non-empty value (so it never runs in a pipeline)- stderr is not a TTY (redirected or piped output)
- the binary is a dev build
- a check already succeeded within the last 24 hours
To disable it permanently:
export OV_NO_UPDATE_CHECK=1Configuration file (.ovscan.yaml)
Section titled “Configuration file (.ovscan.yaml)”ov scan reads an optional .ovscan.yaml from the scanned directory (override with --config). It works with no config at all — the file exists to tune the scanner per repository.
Custom patterns
Section titled “Custom patterns”custom_patterns teaches the scanner your organization’s own credential formats using declarative fields — every field below is required:
version: 1custom_patterns: - id: acme-api-key # your name for the pattern; letters, digits, dashes prefix: "acme_" # literal prefix the credential starts with length: 40 # total token length charset: alnum # alnum | hex | base64 | base64url entropy_min: 4.0 # minimum Shannon entropy (bits/char) after the prefix severity: high # verified | critical | high | medium | low | infoA regex: form is intentionally not supported. A regex supplied via a file that arrives in pull requests is a denial-of-service surface (catastrophic backtracking against every scanned blob), so the scanner rejects it rather than accepting it silently — the declarative fields cover prefix-style credentials without that risk. If a config contains regex:, ov scan refuses to run and says so.
Severity overrides
Section titled “Severity overrides”Tune the severity of any detector — built-in detectors by their id, custom patterns with a custom: prefix:
severity_overrides: google-api-key: medium "custom:acme-api-key": verifiedOverrides interact with --fail-on (default medium): downgrading a noisy detector below your fail threshold keeps it visible in output without failing CI.
Other keys
Section titled “Other keys”ignore_paths (glob patterns to skip), ignore_blob_content (regexes that suppress matching lines — useful for fixture files), verify (liveness-check behavior in CI), and baseline (path + refresh policy for the accepted-findings baseline). All optional.
ov scan –fix
Section titled “ov scan –fix”--fix walks you through storing every detected secret in OpaqueVault and then tells you exactly how to remove them from git history.
ov scan . --fixYou must be logged in (ov auth login) before running --fix. If you’re not, it will print a link to register and exit.
The flow:
- Scan runs as normal and shows all findings
- For each unique detector found — not each finding, so 40 leaked Stripe keys produce one prompt — you’re prompted for a name. It defaults to the detector ID uppercased with
-replaced by_, e.g.stripe-key→STRIPE_KEY. Press enter to accept the default, type a custom name, or type-to skip. - You enter your master password once — it’s reused for all secrets in the session
- Each secret is encrypted locally and stored in OpaqueVault
- After storing, exact
git filter-repocommands are printed for each affected file, along with force-push instructions
Example session:
FOUND 2 leaked secret(s) in your repo.
.env.backup:3 stripe-key sk_live_******************** scripts/ci.sh:8 aws-access-key AKIA************MPLE
Found 2 secret type(s). Let's store them in OpaqueVault.App: my-saas
Master password: ••••••••••••[stripe-key] Secret name (default: STRIPE_KEY, enter to skip):Value for "STRIPE_KEY": •••••••••••••••••••••••••••• ✓ "STRIPE_KEY" stored[aws-access-key] Secret name (default: AWS_ACCESS_KEY, enter to skip): AWS_ACCESS_KEY_IDValue for "AWS_ACCESS_KEY_ID": •••••••••••••••••••• ✓ "AWS_ACCESS_KEY_ID" stored
✓ 2 secret(s) stored in OpaqueVault.
Next: remove them from your git history.If you don't have git-filter-repo installed: pip install git-filter-repo
For each file containing leaked secrets: git filter-repo --path .env.backup --invert-paths git filter-repo --path scripts/ci.sh --invert-paths
Then force-push to update the remote: git push --force-with-lease
Note: notify any collaborators to re-clone after a force-push.
→ https://opaquevault.com/docs/getting-started/scan-fixFalse positives
Section titled “False positives”High-entropy detection may occasionally flag random-looking strings that aren’t secrets (e.g., hashed IDs, UUIDs, base64-encoded config). These are skipped automatically when running --fix — just enter - when prompted for the name.
How it’s different from GitGuardian / truffleHog
Section titled “How it’s different from GitGuardian / truffleHog”ov scan is not trying to replace dedicated secret scanning platforms. It’s a local, instant, zero-friction tool designed for one purpose: get developers to discover they have a problem, and immediately give them a path to fix it with OpaqueVault.
No SaaS account is required to scan, and no file contents, line context, file paths, or repository data are ever transmitted anywhere. The only values that can leave your machine are individual credential strings, and only on the two explicit opt-in paths described above: --verify (to the credential’s own vendor) and --fix (encrypted, to OpaqueVault).