Navigation
reference
Credential References
What this is
Phase 35 introduces an opt-in response format for the vault.read_credential MCP tool. Instead of returning the plaintext directly, vault can return a credential reference that the MCP client substitutes at downstream tool-call time. The plaintext never crosses the MCP boundary back into the LLM context.
This is Mechanism B in vault’s plaintext-out-of-the-chat-log design. Mechanism A is vault exec (CLI-side wrapper). Mechanism C is a long-term MCP spec proposal for a first-class secret content type.
How a reference looks on the wire
Call vault.read_credential with responseFormat: 'reference':
{
"credentialReference": {
"ref": "vault:lease:<leaseHandle>",
"preview": "****1234",
"metadata": { "format": "reference-v1", "length": 14 }
}
}
ref- opaque string the LLM sees. The leaseHandle is already single-use and HMAC-signed; carrying it back to the client is safe.preview- non-revealing fingerprint suitable for “you are about to use ****1234” confirmation UI. Last-4 for raw strings,<envelope>for typed credentials (login / ssh-key / etc., whose JSON shape can leak field names).metadata.format- version stamp. Future formats bump this.metadata.length- byte length, useful for client-side validation without revealing the bytes.
The substitution contract
A compliant MCP client implements the following at downstream tool-call time:
- The LLM emits a tool call (e.g.
Bash) whose arguments may contain a literalvault:lease:<handle>substring. - Before exec, the client walks the tool-call arguments, finds every
vault:lease:<handle>substring, and replaces it with the plaintext. The plaintext is read from the client’s in-process bag of{ ref -> plaintext }populated whenvault.read_credentialreturned the reference earlier in the session. - Substitution happens inside the client’s address space; the plaintext never appears in the conversation log, the LLM context window, or any persisted transcript.
- The bag entry is single-use per the underlying lease (vault enforces this on the server) and is cleared after one substitution.
If the client does NOT implement substitution, the tool call receives the literal vault:lease:<handle> string and the downstream tool (e.g. an SSH command) fails. This is by design: it’s safer to fail loudly than to silently leak.
When to use which mechanism
| Use case | Recommended mechanism |
|---|---|
| Run a shell command that takes a credential (SSH, AWS CLI, curl) | Mechanism A: vault exec - the wrapper is the plaintext path. |
| MCP client supports reference substitution AND the credential will be used in a tool call (Bash, file write) | Mechanism B: responseFormat: 'reference' - cleanest separation. |
| MCP client does NOT support reference substitution | default: responseFormat: 'plaintext' - plaintext returns; SECURITY note in the tool description warns the agent not to echo it. |
| LLM needs to reason about the value (rare; e.g. parse a JSON envelope, count chars) | default: responseFormat: 'plaintext' - reasoning requires the bytes. |
What this defends against
- Operator reviewing the chat transcript later sees only
vault:lease:<handle>strings + previews, never the plaintext. - Backups of the chat transcript (Anthropic-side, cross-device sync, leaked client config files) contain only references.
- Prompt-injection attacks that gain read access to the chat log via a malicious tool result earlier in the session find references, not credentials.
- Model-side bugs that echo earlier-turn content into a later visible response echo the reference, not the value.
What this does NOT defend against
- Compromise of the MCP client process while it holds plaintext in its substitution bag. The threat model assumes the user’s local machine is trusted.
- A malicious tool the user runs after substitution completes. Once the plaintext is in the child process’s argv / env / stdin, the tool can do whatever it wants with it.
- Network-layer attacks on the vault REST + MCP surface. Those are addressed by TLS + audience binding + the audit chain, not by this convention.
Implementation notes for MCP client authors
A reference implementation lives at @ghoststack/credential-reference (zero deps, plain TypeScript). The package exports:
import { substituteReferences } from '@ghoststack/credential-reference';
// On every downstream tool call:
const substitutedArgs = await substituteReferences(args, bag);
The bag is a Map<ref, plaintext> the client populates at vault.read_credential time. The helper handles deep walks, leaves non-string values alone, and supports nested references in JSON-shaped argument trees.
MCP spec evolution
Both mechanisms above are vault-side. The long-term goal is a first-class secret content type in the MCP spec itself, with explicit handling rules:
- Clients SHOULD NOT log content of type
secret. - Clients SHOULD NOT include content of type
secretin the LLM context window of subsequent turns. - Clients MAY render a preview / fingerprint to the user.
- Clients MUST honor the lease semantics (single-use, expiry).
The vault team is tracking the spec proposal at the MCP repository. Until that lands, the credential-reference convention is the canonical path for any MCP client that wants spec-compatible behavior today.
Related
vault exec(Mechanism A) - the CLI wrapper for shell-command scenarios.- Audit log - vault records every read regardless of which mechanism the client used.
- MCP server reference - the full tool surface vault exposes.