Skip to content

MCP interceptor

The MCP context interceptor is a middleware layer inside ov mcp serve. It scans the results that OpaqueVault’s MCP tools return and blocks any result containing a detected secret pattern before it reaches the AI model.

This is OpaqueVault’s second line of defense — it catches secrets that leak back through tool output the vault itself can’t control.


The vault prevents secrets from being requested by an AI. The interceptor catches the case where a secret ends up in a tool result anyway — most often in untrusted text that a tool embeds in its response, such as an error message relayed from the agent daemon.

It is the second of two layers, and the first one does the heavy lifting for vault_run:

  1. Structural omission (the primary defense). vault_run never returns subprocess output. The bridge deliberately does not read stdout or stderr into the response at all — the payload carries exit_code, redacted: true, and redacted_fields: ["stdout", "stderr"], and that key set is frozen by test. Even a daemon that still sent output could not get it into the agent-visible payload. Output length is treated as an exfiltration channel too, so no byte or line counts are returned either. You read the output on the daemon terminal or in the agent log.
  2. The interceptor (this page). A pattern-matching backstop over every result the MCP tools do return, so that a credential which slips into a payload some other way is caught before Claude sees it.

The same detection engine as ov scan:

Pattern Example
AWS access keys AKIA[0-9A-Z]{16}
AWS secret keys 40-char base64 secret in an AWS assignment context
GitHub tokens ghp_, github_pat_
Stripe keys sk_live_, sk_test_
Private keys PEM headers
JWT tokens eyJ... three-part base64url
Connection strings postgres://user:pass@host
High-entropy strings Shannon entropy above a length-banded threshold — 4.5 bits/char at 20–31 chars, rising to 4.8 at 64+

ov mcp serve
tool result ────────────▶ [interceptor scans] ────clean────▶ Claude
↓ match found
result dropped, Claude receives:
"⚠ OpaqueVault intercepted a potential secret
in this output (…). The value was blocked from
entering your context. Instead … use:
vault_create_secret · vault_run"

The interceptor covers two directions, and they do different things:

  • Outbound (the detection path). Every result OpaqueVault’s MCP tools return to Claude is scanned by the detection engine above. This is the path that catches secrets in command output.
  • Inbound (an exfiltration speed bump). vault_run calls only. The requested command is checked against a deny-list of obvious exfiltration patterns — printenv, env, nc, /proc/self/environ, bash -c "declare -p", redirecting an environment variable to a file. This is a deny-list, not a secret scan, and it is explicitly a speed bump rather than a boundary: a renamed binary or an alternate interpreter defeats it.

In block mode (the default) the entire result is scanned, whatever its size. In redact mode, results larger than 512 KB are scanned only in their first 512 KB — a secret buried past that offset in a single response is redacted only if the same value also appears in the scanned prefix.


Terminal window
OV_INTERCEPT_MODE=block ov mcp serve

The entire tool result is dropped and a fixed error string is returned to the MCP client in its place. Claude sees the error, never any part of the output. vault_status increments intercepted_count.

Terminal window
OV_INTERCEPT_MODE=redact ov mcp serve

Each detected value is replaced with [REDACTED:detector-name] and the surrounding content passes through, so Claude keeps the non-secret context of a command’s output.

Any unrecognized value of OV_INTERCEPT_MODE — including an empty one — falls back to BLOCK. There is no mode that disables the interceptor.


vault_status()
→ { "intercepted_count": 2, "intercept_mode": "block", ... }

If intercepted_count is non-zero, a secret pattern reached a tool result and was stopped. ov mcp serve also writes a diagnostic line naming the matched detectors to its own stderr — it never logs the matched value.

A non-zero count is worth investigating at the source: it means a credential reached a response payload, which the structural layer above is designed to make unnecessary.