Skip to content

Explanation

Privacy and data sovereignty

What runs locally, the real egress map (including MCP reads into a cloud LLM's context), the opt-in secret scrubbing layer, and how the gap-tracking system enforces consent before storing query text.

Quaid is local-first by construction. This page makes the privacy posture explicit: what runs locally, the real ways content can leave your machine, the opt-in secret-scrubbing layer that masks machine-shaped secrets on outbound reads, and how the knowledge-gap system handles the one place where consent matters.

The dominant egress path is the MCP client, not the network. quaid itself contacts almost nothing. But the whole point of the MCP server is that a client reads your memory — and the common clients are cloud-hosted LLMs (Claude Code, Cursor, etc.). When such a client calls memory_get/memory_query/memory_search, the returned page content enters that client’s context window and is sent to the model provider. “Nothing leaves your machine” describes the binary; it does not describe what your agent does with what it reads. See the egress map below.

Everything that touches your notes runs on your machine:

  • Page parsing, frontmatter validation, slug resolution.
  • FTS5 indexing.
  • Embedding inference (via candle, on CPU).
  • Vector search.
  • Graph traversal.
  • Contradiction detection.
  • The MCP server itself.

No API keys are required. No third-party service is contacted by quaid during normal use.

There are two categories of egress, and the dangerous one is easy to miss.

Content quaid reads out to a client. This is the high-volume path. The MCP read tools return page content into the calling client’s context. If that client is a cloud LLM, the content is sent to the provider as part of the model request.

PathWhenWhat is sent
MCP reads (memory_get, memory_query, memory_search)Every read your agent makesFull page content (compiled_truth + timeline for memory_get; summaries, and full chunks under depth:"auto", for query/search) into the client’s context — i.e. to the model provider when the client is cloud-hosted.
Skill research (research)If a skill calls an external search/LLM API (e.g. Exa)The query the skill sends. Its “redacted” mode is prompt-enforced by the agent today, not code-enforced.

Network calls quaid itself can make. Three narrow exceptions, all opt-in:

PathWhenWhat is sent
Model download (online build only)First semantic use, then never againStandard Hugging Face model fetch (BGE weights). No prompt or page content.
Skill enrichment (enrich)If you authorize an external API call from a skillWhatever the skill’s memory_raw payload is — under your control.
Knowledge gap escalationIf you approve a gap for external sensitivityOnly after explicit approval — see below.

The airgapped build can refuse the second category by construction. It cannot stop the first: once a client is connected, what the agent reads is by definition headed into that client. That is what the secret-scrubbing layer below is for.

To reduce what crosses the MCP wire, Quaid can scrub machine-shaped secrets out of read-tool responses before they are serialized to the client. It is off by default and deterministic.

This is secret scrubbing, not full PII redaction. It is a phase-1, regex-based pass: it catches high-confidence machine-shaped secrets — email addresses, phone numbers, API-key shapes (sk-…, AKIA…, GitHub/Slack tokens), long opaque hex/base64 tokens, and account/card-number shapes — plus a user-defined blocklist. It does not recognise names or other free-text PII; that (NER) is deliberately deferred to a later phase. Do not treat a scrubbed payload as anonymised.

Terminal window
# Mask secrets on every read for this brain:
quaid config set mcp.redact_outbound patterns # default: off
# Optional: always scrub these literal strings (comma- or newline-separated):
quaid config set mcp.redact_blocklist "Project Bluebird, internal-codename"

Each read tool also accepts a per-call redact boolean that overrides the config default — redact: true forces scrubbing even when the config is off; redact: false forces plaintext even when the config is patterns.

  • Outbound only. FTS5 and the embeddings index always store the originals, so retrieval quality is unchanged — only the bytes returned to the client are masked. A search for a term that lives inside a secret still finds the page; the returned preview is what gets tokenised.
  • Deterministic, stable tokens. Each distinct secret is replaced with a stable token like <EMAIL_1> or <SECRET_2>. The same value maps to the same token for the life of the connection, so an LLM can still reason about “the same key” appearing twice (coreference is preserved).
  • Byte-identical when off. With mcp.redact_outbound = off (and no per-call redact: true), read output is byte-for-byte what it was before this feature existed.
  • Locally reversible. The token map lives in memory on the per-connection server, never on disk and never on the wire. The memory_rehydrate MCP tool reverses the map (<EMAIL_1> → the original) locally, so an agent that needs the real value can round-trip without the original ever being sent to the provider.

When the memory can’t answer a query well, it’s tempting to log “we couldn’t answer Q.” That logged text is user data. If the agent later sends that log to an external service to ask for help, you’ve just exfiltrated a prompt that may contain PII or proprietary detail.

Quaid’s knowledge_gaps table is built around that risk.

When memory_gap is called, the table records:

  • query_hash — SHA-256 of the original query. Always populated.
  • context — short free-form context (caller-controlled).
  • sensitivity — defaults to internal.
  • query_textNULL by default.

So the default state is: we know there was a gap (and can deduplicate it), but we don’t keep the words.

To do thisYou need
Store query_text verbatimAn approved_by and approved_at set
Set sensitivity = "external" (allow external research)Same approval, plus explicit policy
Set sensitivity = "redacted" (store a sanitized variant)Same approval; redacted_query is what gets used downstream

Database CHECK constraints enforce this — there is no code path that stores raw query text without an approval pair, by construction.

  • internal — Hash-only. Default for every detected gap. Safe to store; tells you “this kind of question came up” without revealing the question.
  • redacted — Approved, with a redacted_query populated. The original is not stored; the sanitized variant is what gets shared with anything outside the memory.
  • external — Approved, raw query stored. Authorizes external research (e.g. an enrich skill that calls a third-party API). Use sparingly.

See Sensitivity contract for the agent-facing reading.

Embeddings are derived from page content. They are vectors that look like noise to a human, but they’re not opaque — a sufficiently motivated attacker with the right model can reconstruct rough paraphrases from embeddings.

Practically:

  • Embeddings live inside memory.db. Treat the file like the rest of your notes.
  • They never leave the machine unless you copy the file.
  • If you’re in a regulated environment, encrypt the volume that holds memory.db. SQLite respects filesystem-level encryption.

The MCP server writes operational logs to stderr. By default these include tool names, request shapes, and error paths — not request bodies. Specifically:

  • A memory_put log line records the slug and version, not the content.
  • A memory_query log line records the query length and result count, not the query text.
  • A memory_gap log line records the hash, not the text (matching the table policy).

If you redirect stderr to a file or a SIEM, audit what gets captured before you ship the redirect.

memory.db is one file. Back it up the way you back up everything else. There’s no remote backup baked in, no cloud sync, no telemetry. Whatever your existing volume backup or snapshot strategy is, that’s the strategy.

The same MCP surface that humans use is exposed to agents. An agent connected over MCP can read every page in your memory and call every tool. That’s a feature: agents need access to do useful work.

It’s also a responsibility. Three practical implications:

  1. Trust your client. Anything that connects via quaid serve can read everything, and a cloud client forwards what it reads to its model provider. Don’t run unknown MCP clients against a memory that contains sensitive material.
  2. Scrub outbound reads when the client is cloud-hosted. Turn on mcp.redact_outbound = patterns to mask machine-shaped secrets before they reach the client’s context (see Outbound secret scrubbing). Remember it is secret scrubbing, not full PII redaction.
  3. Use sensitivity tiers. If an agent escalates a gap, the sensitivity contract is what keeps the raw query out of any outbound payload by default.

Local-first doesn’t make those concerns disappear. It just means the trust boundary is at your machine instead of a SaaS dashboard.