Skip to content

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.


Terminal window
ov scan [path] [flags]

path defaults to the current directory if omitted.


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.

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
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
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

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.

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.


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/quickstart

Previews 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-urlPOSTGRES_URL and stripe-keySTRIPE_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.
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.
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
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.


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.

Terminal window
ov scan --verify

Because of that, --verify asks for consent before making any network call:

Warning: --verify will send each detected credential to its vendor's live API
to 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.


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_CHECK is set to any non-empty value
  • CI is 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:

Terminal window
export OV_NO_UPDATE_CHECK=1

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 teaches the scanner your organization’s own credential formats using declarative fields — every field below is required:

version: 1
custom_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 | info

A 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.

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": verified

Overrides interact with --fail-on (default medium): downgrading a noisy detector below your fail threshold keeps it visible in output without failing CI.

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.


--fix walks you through storing every detected secret in OpaqueVault and then tells you exactly how to remove them from git history.

Terminal window
ov scan . --fix

You 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:

  1. Scan runs as normal and shows all findings
  2. 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-keySTRIPE_KEY. Press enter to accept the default, type a custom name, or type - to skip.
  3. You enter your master password once — it’s reused for all secrets in the session
  4. Each secret is encrypted locally and stored in OpaqueVault
  5. After storing, exact git filter-repo commands 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_ID
Value 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-fix

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).