Skip to content

Secret scanning for AI agents

Use ov scan --format agent when a coding agent needs to check for exposed credentials. This mode returns structured detection results with opaque file references. Source snippets, filenames, credential previews and raw error text are excluded from its JSON report.

Terminal window
ov scan --format agent .

Scanning needs no OV account. It runs the built-in detectors locally and does not verify credentials or store them. The CLI’s existing background update check still applies when run in a terminal.

Exit status What the agent should do
0 clean Report no findings within the declared scope. Check coverage before concluding the intended files were scanned.
1 findings Explain the findings and guide local review and remediation.
2 incomplete Explain why coverage is incomplete; retain any findings and resolve the missing scope before retrying.
2 error Correct the invocation and retry.

Require schema_version: 1, valid JSON and a matching exit/status pair. Missing, malformed or unfamiliar output is a failed check. CLI syntax errors and process termination may occur before a report is produced. A clean scan is detection evidence for its scope; it does not certify that a project contains no secrets.

The version 1 report fields are:

  • policy: agent-local-v1.
  • scope: working_tree, file, path_list, staged_index, or none for invocation failures.
  • coverage: scanned and skipped file counts, excluded directory count, scanned bytes, and an incomplete_reasons array.
  • limits: per-file bytes, total bytes, entries and finding limits.
  • findings: an array, including when empty. Each finding has id, file_id, detector, severity, numeric line and column, verification: not_checked, and a remediation identifier (openai_key or credential).
  • next_steps: fixed workflow identifiers, not repository-authored instructions.
  • error_code: empty for scans; a fixed code for invocation failures.

Finding and file references are local to one report. They are not persistent identities or handles for editing files. Do not use the numbers to correlate separate scans. To see locations, the user runs the companion format in their own terminal:

Terminal window
ov scan --format review .

Review uses the same policy and shows quoted paths and finding positions without source snippets. It includes repository-controlled filenames, so keep this output local instead of feeding it to the model.

Agent and review formats use a fixed policy. Repository configuration, baselines, ignore files and inline suppression comments do not configure this mode. The built-in detectors’ general heuristics still apply, including their limits and false positives. Hidden directories such as .claude, .cursor and .codex are included.

These exact directory names are excluded below the selected root: .git, .gitnexus, .worktrees, node_modules, vendor, dist, bin, .next, .astro, .venv, venv, and __pycache__. Their contents are outside the reported scope. A skipped directory counts once; the scanner does not count its unseen children. NUL-containing binary files are also outside a directory scan’s text scope and count as skipped files.

The limits are 10 MiB per file, 100 MiB of file reads, 10,000 entries, 1,000 reported findings and 64 nested directory levels. A read can consume one extra byte to detect a limit. These limits bound work, not the detector’s peak memory. Resource exhaustion, read failures, cancellation, changed files, and symlinks or special files encountered in scope produce incomplete. Explicitly targeting an excluded binary file also produces incomplete.

The selected root or selected file’s parent resolves normally, including OS directory aliases. Stable symlinks below that boundary are not followed. Use a quiescent checkout: the scan is not an atomic snapshot and does not defend against arbitrary concurrent same-user filesystem mutation.

You can restrict the mode to a list of working-tree files:

Terminal window
set -o pipefail
git diff --name-only -z --diff-filter=ACMR | ov scan --format agent --include-only-paths-from-stdin

Run this in Bash from the repository root. Require the path-producing command to succeed too: pipefail prevents a failed git diff followed by an empty scan from being accepted as a successful pipeline. Paths must be root-relative. NUL-delimited names are preserved exactly; newline-delimited input also supports CRLF. The input cap is 1 MiB and 10,000 nonempty paths. Missing paths, paths outside the root, directories and explicitly excluded paths are not silently accepted as clean. An empty list is a valid empty scope with files_scanned: 0.

Only --format, --staged, and --include-only-paths-from-stdin are supported with this policy. Other explicit options—including --verify, --fix, --fail-on, --history, configuration and baselines—return unsupported_option before scanning. Findings at any severity cause exit 1 when the scan is complete.

Terminal window
ov scan --format agent --staged
# The user reviews locations in their own terminal:
ov scan --format review --staged

--staged scans the full staged blobs changed relative to HEAD. On a branch without a first commit, it scans staged additions. Unchanged tracked files, untracked files, intent-to-add entries and unstaged edits are outside this scope. Deletions have no staged content to scan. Renamed destinations are scanned as additions. This is a full-blob scan, not an added-lines-only scan.

The optional positional directory selects a repository; running from a subdirectory still scans changes across that repository. Linked worktrees use their own index. Inherited GIT_DIR, GIT_INDEX_FILE and injected Git config do not select another index. Do not combine --staged with stdin paths. The flag requires agent or review format; legacy formats retain their existing working-tree and history behavior.

The JSON shape and agent-local-v1 detection policy are unchanged, with the new scope value staged_index. Consumers that enumerate scopes must recognize it before accepting the result. Blob IDs are retained internally during a scan; they are not included in the report. Local review positions refer to staged content and repository-root-relative paths, so they may differ from editor positions in the working tree. After removing a credential locally, stage the removal and rescan staged content too.

Conflicts, symlinks, submodules, missing objects, excluded staged paths or binary blobs, and exhausted limits produce incomplete/2. Findings gathered before a failure are retained. Empty and deletion-only scopes report zero scanned files. The existing file, byte, entry and finding limits apply; Git enumeration also has a 4 MiB metadata cap and staged scanning has a two-minute deadline. Cancellation is checked between detector calls; this is not a hard CPU sandbox. Additional reason codes are git_error, git_metadata_limit and unmerged_index; expiration reports cancelled.

The scanner sets GIT_NO_LAZY_FETCH=1 and blocks network protocols independently, including on older Git versions. It also disables external diff/textconv, replacement objects and fsmonitor. It invokes Git without shell interpolation or repository hooks. Missing partial-clone objects must be fetched separately by the user before retrying. Use a quiescent repository: blob IDs pin the enumerated bytes, but concurrent changes during enumeration are not an atomic snapshot guarantee.

Guided example: replace an exposed OpenAI API key

Section titled “Guided example: replace an exposed OpenAI API key”

The goal is to give the application a replacement credential while keeping the credential out of the agent’s conversation. The user handles credential entry; the agent handles explanations, secret names and the scan report.

  1. Find the exposure. The agent runs ov scan --format agent . and explains an openai-key finding. The user runs ov scan --format review . locally to locate it. Do not paste the file, key or review output into chat.

  2. Replace and revoke at the provider. In the provider console, create a replacement with the needed permissions and revoke the exposed key. Coordinate application cutover if necessary; until revoked, the old key remains exposed. The scanner does not perform or attest to this step.

  3. Store the replacement directly in OV. Sign in with ov auth login if needed. Choose the intended app and environment, then run this yourself:

    Terminal window
    ov secret set OPENAI_API_KEY --app my-app --env development

    Replace the example app/environment names with your existing OV context. Enter the replacement at OV’s hidden local value prompt and unlock locally. Do not put the value in a command argument, .env file, or AI chat. The existing OV storage path encrypts the value client-side.

  4. Remove the literal locally. Replace hardcoded credentials or plaintext agent configuration with your application’s supported runtime secret interface. Keep only the name OPENAI_API_KEY in shared configuration. Use your framework/SDK’s existing environment support with OV injection. The agent should not open the credential-bearing file to perform this step.

  5. Check the application through OV. For an existing Python application, the user can run its normal health check or test command this way:

    Terminal window
    ov run --app my-app --env development --secrets OPENAI_API_KEY -- python app.py

    Use your application’s actual command in place of python app.py. For an agent-driven check, prefer the connected MCP vault_check_context and vault_run tools, which provide execution metadata without returning child stdout or stderr to the model. Treat a successful process exit as that command’s evidence, not proof of overall application health.

  6. Rescan the same scope. Run ov scan --format agent . again. Require a complete report and review its findings. If the credential was committed, coordinate a separate local history review and cleanup; the working-tree result says nothing about historical copies.

Track the evidence separately: provider revocation, OV storage, plaintext removal, application operation, and the final scan. ov scan --fix stores credentials interactively and prints guidance; it does not establish all five.

For a citizen developer, the same flow is: locate the exposed connection, replace its credential at the service, store the replacement in OV, connect the tool through its supported secret interface, then test and rescan. If a tool only accepts pasted plaintext credentials, OV injection needs a supported adapter before this workflow can cover it.