Build log ·

Letting an AI assistant into an encrypted vault, and where that stops

An assistant can now list, search, organize, upload to and move files into a ShieldFive vault through a local MCP server, with a grant the server can check but not open. The post covers the key wrap, scopes, revocation, the verified move-in, one upload bug the first smoke test caught, and the limits: the model provider still sees what the assistant reads.

Written by Cho García

An AI assistant can now work inside a ShieldFive vault: list it, search it, find duplicates, read a text file, rename and move things, upload, and move local files in. It does this through @shieldfive/mcp, a Model Context Protocol server that runs on your machine. The ShieldFive server still never sees a file name or a byte of plaintext.

This post records how that works, what shipped between 2026-09-14 and 2026-09-21, and where the guarantee stops. The last part is the important one.

The problem

Every AI integration for cloud storage works the same way: the provider hands the assistant a token, the assistant calls an API, the API returns plaintext. That requires the provider to be able to decrypt. For an end-to-end encrypted vault it defeats the point, so the decryption has to happen somewhere the server is not.

What runs where

  • The browser creates the access, while the vault is unlocked. You pick folders, a scope and an expiry.
  • The MCP server runs locally over stdio, next to the assistant. It decrypts in memory and writes nothing decrypted to disk. One module makes network calls.
  • The ShieldFive server checks scope, expiry and revocation on every request, and serves ciphertext. It stores a SHA-256 of the access token, not the token.

What a grant is

A grant is a connection string:

sf-grant-v1:<grant_id>.<token>.<secret>.<pkfp>

token is the bearer credential the server checks. secret never reaches the server. pkfp pins the fingerprint of the owner's ML-KEM public key, so uploads are encrypted to the right key.

From the secret the browser derives a grant key:

GK = HKDF-SHA256(secret, salt = grant_id, info = "shieldfive/v1/agent-grant/wrap")

and wraps each chosen folder's key under it with AES-GCM, with the grant, object kind and object id in the associated data. A wrap made for one grant or one object does not open under another. Subfolders open through the vault's normal folder-key chain, so a grant holds one wrap per chosen folder, not one per file. A whole-vault grant wraps the root-level files individually; no grant ever holds the root key. The Bin, Media and incoming document requests are excluded unless you opt in.

These formats live in @shieldfive/crypto/vault (1.0.0-rc.5), so the browser and the MCP server share one reviewed implementation, checked against vectors from an independent implementation.

Scopes and revocation

A grant is read, organize or write. It expires after at most 90 days. Revoking it deletes its key rows and fails the next request with a 401; the MCP server re-fetches the grant on every call and caches nothing that would outlive a revocation.

Nothing a grant can reach deletes anything. vault_trash moves items into a per-connection folder in the Bin; there is no delete route in the API a grant can call.

Moving files in, without deleting

vault_move_in is the tool behind "free up space on this machine". For each file, up to 50 per call and 512 MB each: encrypt, upload, read the upload back, compare by SHA-256. Only after the comparison passes is the local original moved to .shieldfive-mcp-trash, with a manifest naming its vault copy. The first failure stops the batch. Nothing is deleted; the disk space comes back when you empty that folder yourself.

Release history

  • 2026-09-14, 0.1.0: nine local-only tools, no vault access.
  • 2026-09-16, 0.2.0: first version on npm.
  • 2026-09-18: grant design, schema, and the crypto vault module.
  • 2026-09-19, 0.3.0: vault tools through grants.
  • 2026-09-20, 0.4.0: authorize in the browser instead of pasting a connection string.
  • 2026-09-21, 0.5.0 to 0.6.x: write scope, vault_upload, vault_move_in.

One failure is worth recording. In 0.5.0 every real upload failed at the final step: the finalize request omitted the SHA-1 of the ciphertext and the server returned 400. Nothing was stored or charged. The first production smoke test caught it and 0.5.1 fixed it the same day. The tests had passed against a test double that did not enforce the rule; it enforces it now.

Where the guarantee stops

  • The model provider sees what the assistant reads. When vault_read_file returns a document to a hosted model, that document goes to the model's provider. Encryption protects the vault from ShieldFive, not from the assistant you chose to connect. A local model is the only way around this. This is the largest limit, and it is not one that encryption can fix.
  • Revoking does not un-read. Revocation stops new requests. A leaked secret together with ciphertext already downloaded stays decryptable, because revoking does not re-key the files.
  • The connection string is a live key. Anyone who copies it has the grant's access until it expires or is revoked. Kept in SHIELDFIVE_GRANT, it is readable by anything that can read the process environment.
  • Grant creation trusts the page. When the grant is created, a compromised server serving the web app could mislabel which folder you picked. Later requests are checked against the owner's keys.
  • Prompt injection still works inside the scope. Tool output is fenced as untrusted content, but that does not make a model ignore instructions in a file. What an injected instruction can do is bounded by the grant's scope and folders, and every change it can make is reversible.

Where to check it