Docs / Local Vaults

Local vaults

mcpgate shows the model a token in place of each personal value. A local vault on the user’s computer gets the real value for a token directly from mcpgate, not through the model, and puts it into a local file or command.

The problem

A ticket system, a CRM or a mailbox answers with personal data. mcpgate replaces each value with a token such as ⟦EMAIL_1:ma•••@example.com⟧ before the model reads the answer. When the model puts the token into a tool call, mcpgate puts the real value back before the call reaches the service. That covers every call that goes through mcpgate.

A local file or a local command does not go through mcpgate. If the model writes a letter to a customer into a file, the file holds the token, not the address.

Model sees ⟦EMAIL_1⟧ YOUR COMPUTER AI client for example Claude Code mcpgate Services tickets, CRM tokens only call with ⟦EMAIL_1⟧ answer with ⟦EMAIL_1⟧ real value data Local file holds ⟦EMAIL_1⟧ Write with ⟦EMAIL_1⟧ the token, not the address
A call to a service goes through mcpgate, and the service gets the real value. A Write to a local file does not go through mcpgate. The file holds the token.

How a local vault closes the gap

A local vault is a hook in the AI client. It runs before a Write, Edit or Bash call. It sends each token in the call, with its key K, to mcpgate’s resolve endpoint and puts the real values into the call. The model and the transcript keep the token.

Model sees ⟦EMAIL_1⟧ YOUR COMPUTER AI client for example Claude Code mcpgate resolve endpoint Services tickets, CRM tokens only call with ⟦EMAIL_1⟧ answer with ⟦EMAIL_1⟧ real value data Local file holds the address Vault hook for example maisecrets Write with ⟦EMAIL_1⟧ value K and the token, over HTTPS, same origin only
The vault hook asks mcpgate for the value of a token and puts it into the local call. No line goes from the hook to the model: the model still sees only the token.

Connect maisecrets

maisecrets is a free plugin for Claude Code and Codex. Its vault speaks the protocol on this page, in Claude Code and in Codex. The first run from end to end passed on 17 October 2026: maisecrets put the values of five tokens from a help-desk ticket into a local file, and the model saw only the tokens. Four steps:

  1. Turn on the switch (operator). In the mcpgate admin, open Data sanitization, find the card “Local vaults”, turn on “Let local vaults read values” and save. The switch is off by default. When it is on, it applies to every user of the instance.
  2. Each user sets up maisecrets 0.6.36 or later: install it, add mcpgate as an MCP server of user scope with an https URL, use mcpgate once, reconnect, and activate the link. The steps, the states and what to do in each: Vault references.

maisecrets resolves a token only in a session that got the token from that mcpgate server, and only for 24 hours after the server first showed it. A token from another source, such as a file or a web page, stays a token. A token in the argument of an MCP call also stays a token: it goes back to the server that made it. To type a value on a test device, see Appium, Selenium and LambdaTest.

Limits of this first version: on Windows, a Bash command gets no server value. In Codex, vault references need Python 3.11 or later and are off on Windows. A value under 8 characters, or a value that reads as a word (a first name), can show again in later tool output, for example when the AI runs cat on the file it wrote. maisecrets does not look for such a value, because a search for it would change every text that holds it.

Build your own vault

The protocol is open. A vault of your own needs these parts. The reference today is section 4 of docs/PROTOCOL.md in the maisecrets repository. A separate, vendor-neutral specification, with conformance cases and the reason for each decision, is in preparation and not public yet. Client-Vault and client-vault are not registered at IANA. This is a proposed protocol, not a standard.

A wider question, how any MCP server could give the model a reference instead of a sensitive value, is explored in Sensitive References for MCP, an independent pre-SEP design.

1. The key and the header

Make a random 256-bit secret K per server origin and per client profile. Keep K on the user’s computer. It goes only to the resolve endpoint, never on the MCP connection. Add a static header to every request to the server:

Client-Vault: v=1, kid=:<standard base64 of SHA-256(K), with padding>:

The header is an RFC 9651 dictionary. It carries a hash, never K.

2. Discovery

Read the protected resource metadata of the MCP URL (RFC 9728):

GET https://mcp.example.com/.well-known/oauth-protected-resource/mcp

While the operator switch is on, the answer has the field client_vault_endpoint. Use it only if it is https and has the same origin as the MCP URL. If the field is missing from a 200 answer, the server does not support local vaults.

3. Resolve

POST <client_vault_endpoint>
Authorization: Bearer <K, base64url without padding: 43 characters>
Content-Type: application/json

{"now": "2026-10-16T09:00:00Z",
 "refs": [{"key": "EMAIL_1", "seen_at": "2026-10-16T08:58:12Z"}]}
  • key is the token’s key without its display part.
  • now is the client clock. seen_at is the time the vault first saw the key. Do not move it to a later sighting: if the server mints the key again for another person, a later time would get that person’s value.
  • At most 100 keys and 64 KB per request.
  • The endpoint refuses an OAuth token. Send K only.

4. The answer

The server answers with all values or with one error for the whole request. It never resolves a part.

StatusCodeMeaning
200—{"values": {"EMAIL_1": "…"}}: one string for each requested key. "refs": [] is a status check and answers {"values": {}}.
400bad_requestThe body is malformed, or a key is in the client range (a key the vault made itself).
401unknown_keyNo active binding for this K. This includes a revoked or a blocked binding.
403pendingThe binding waits. The body has activates_at.
403clock_skewThe client clock is more than 5 minutes off. The body has server_time.
404unknown_refA key is not in the user’s current mapping. The body names the key.
410expiredThe server minted the key again after the vault first saw it. The key can now name another person.
413too_large / too_many_refsThe body is above 64 KB, or it has more than 100 keys.
429rate_limitedMore than 100 values for this user today. Retry-After gives the seconds to midnight UTC.
503disabledThe operator switch is off.

Treat every other answer as a refusal: a redirect, another status, a body that is not JSON, a value that is not a string, or a key that is missing from a 200 answer.

Make your own MCP server speak it

The same contract, from the server side. mcpgate implements it as follows:

  • Read the header. Parse Client-Vault as an RFC 9651 dictionary. Accept v=1 and a kid of 32 bytes.
  • Bind at the sign-in. On the authorization-code exchange with PKCE, bind the kid to the user at once. This request needs the PKCE verifier of the sign-in that the user did, so a stolen access token cannot use this path.
  • Bind pending on an MCP request. A kid that first shows on an MCP request binds pending. mcpgate activates it after 24 hours, at the next browser sign-in, or when the user activates it on the connections page. The user gets a notice on a channel the agent does not control (Slack direct message, else email).
  • One kid, one user. If a kid shows with a second user, block it for both. mcpgate allows at most 5 vaults per user.
  • Publish the endpoint. Add client_vault_endpoint to the RFC 9728 metadata while the feature is on.
  • Resolve. Compare SHA-256 of the presented K with the stored kid. Store the kid only, never K. Answer with the codes in the table above.
  • Audit. Write one audit line per resolve: the user, the kid and the keys. Never the value, never K.

Security properties and limits

  • The model never gets the value. The vault puts it into the local call. The model and the transcript keep the token.
  • K never reaches another origin. The vault takes the endpoint only over HTTPS and only on the origin of the MCP URL.
  • A stolen access token can bind only pending: 24 hours, with a notice to the user, who can revoke it.
  • A cap: 100 values per user per day.
  • A key minted again for another person answers 410. An old token never gets the value of a new person.
  • The limit: an AI agent that runs as the user can read K from the vault’s store, as it can read any stored value. The cap, the audit line, the notice and the revoke on the server bound that. A value that the vault writes into a file is plain text there.