The Helix spec

The vault format and protocol surface, published so that Helix-the-host is an implementation rather than a definition. Anyone may build a compatible vault server or a tool that reads a Helix export. Spec text is licensed CC BY 4.0; the reference vault server is AGPL-3.0.

Version helix-export/v1 · protocol v0.7 · August 2026. Breaking changes bump the version, and exports stay importable across versions.

1. Vault schema

A vault holds facts in six fixed categories, plus two media collections: subjects (people and pets) and a single owner voice profile.

categories: identity | work | projects | preferences |
            relationships | communication-style

category = {
  base:    [ { id, text } ],              // written by the owner
  learned: [ { id, fact, source, date } ] // proposed by an app, approved by the owner
}

Entry ids are content-addressed — a short FNV-1a hash of category, list and text. They require no storage, and they change when an entry changes, which is the mechanism behind supersession: a stale id simply stops resolving rather than silently pointing at different content.

subject = {
  id, name, species,
  thumb,                                   // small data URI, app-visible
  photos: [ { id, mime, b64, thumb } ]      // never leaves the vault
}

voice = {
  takes: [ { id, style, isPhrase, mime, b64, recordedAt } ],
  phrase: { text, issuedAt },               // live verification script
  verifiedAt,                               // set when a phrase take exists
  providerVoiceId                           // cached compilation, rebuilt on change
}

Vaults, and who can reach them

Storage is keyed by vault, not by user. For a personal vault the two are the same identifier, which is why this is a clarification rather than a migration. A shared vault is a distinct object with its own members, its own review queue and its own audit chain; a person's own vault is never absorbed into one, and joining or leaving a household does not touch it.

vault:<vaultId>          the vault document
pending:<vaultId>        the review queue
audit:<vaultId>          the hash chain
labels:<vaultId>         labels and private flags
vaultmembers:<vaultId>   [{ userId, role, subject?, joinedAt }]

Roles are owner, curator, contributor and subject. The rules that govern them are normative and appear in section 6. See Households & delegation for the reasoning.

2. Labels and the private flag

Categories are a coarse grant — “work” hands over everything about work at once. Labels let an owner give an app one slice of a category and nothing else; private marks entries that leave through no app door at all.

marks = {
  labels:  { <entryId>: ["helix", "v0-7"] },   // lowercase, dashed, ≤32 chars, ≤6 per entry
  private: [ <entryId> ]
}

Stored against the same content-hash ids as the entries themselves, which is what lets marks survive an export/import: the same text in the same category yields the same id in any vault.

Two rules govern them, and both are normative:

  • Labels only narrow. A label-restricted grant sees the intersection of its categories and its labels. A helix-tagged entry sitting in a category the app wasn't granted stays invisible — otherwise a label becomes a privilege-escalation path through the consent screen. Entries with no labels are invisible to a restricted grant.
  • Private beats everything. No scope, label or grant reveals a private entry through an app door, and an import can only ever add private flags, never clear them.

Apps cannot apply labels — only propose them, through the same review queue as facts. Labels decide what otherapps can see, so that decision stays with the owner. The restriction itself rides in the grant, not in the scope strings, and is shown on the owner's connections page.

3. Export format

GET /account/export (session) or GET /owner/export (device token) returns the entire vault as one document. This is the interchange format — any tool can consume it.

{
  "format": "helix-export/v1",
  "exported_at": "2026-08-04T12:00:00Z",
  "user":     { "id", "name", "email", "created_at" },
  "vault":    { <category>: { base, learned } },
  "subjects": [ subject ],
  "voice":    { "verified_at", "takes" },
  "marks":    { "labels": { <entryId>: [label] }, "private": [ <entryId> ] },
  "pending":  [ proposal ],
  "audit":    [ { "at", "client", "action", "detail", "seq", "hash", "prev" } ],
  "connected_apps": [ { "app", "scopes" } ]
}

Secrets are deliberately excluded: no passphrase hash, no OAuth tokens, no device-token hashes. An export is your data, not your credentials.

Import

POST /account/import (session, file upload) or POST /owner/import (device token, JSON body) loads the same document back. Owner-authenticated only — apps propose, owners load documents.

  • Merge, never replace.Importing into an empty vault reproduces the original; importing into a populated one adds only what's missing. A file the owner picked never destroys what they already have.
  • Idempotent. Facts dedupe on text, photos on bytes, subjects on name + species. Importing the same file twice changes nothing, so recovery never depends on remembering whether you already tried.
  • Three things don't travel, and each refusal is reported rather than silently dropped: the audit log (it records what this server did — adopting a foreign one would let a document dictate history), voice verification (a live reading of a server-minted phrase, so it is re-earned rather than copied; the takes themselves import fine), and app grants (issued per vault).

The response reports counts per section plus any notes: { facts, subjects, photos, takes, marks, notes[] }. The import itself is written to the audit log.

4. Protocol surfaces

Three doors, one consent model, one audit log.

MCP — assistants and agents

Streamable HTTP at https://vault.helix.ai/mcp, OAuth 2.1 with dynamic client registration and PKCE. Tools are registered per session from the granted scopes, so the tool list is itself scope-shaped.

ToolScopeContract
get_contextany categoryMarkdown of granted categories, each entry tagged [#id], plus section freshness, the labels in use, and pending count. Optional label narrows further.
propose_learningproposeQueues a fact for owner review. Optional replaces supersedes an entry on approval; optional labels tag it.
propose_labelsproposeQueues labels for an entry that already exists. Same queue, same approval — apps never tag directly.
list_subjectslikenessNames and species only.
generate_imagelikenessSubjects by name → expiring link. refine_image_id edits a previous result.
generate_speechlikeness:voiceText → expiring audio link, in the owner's verified voice.

REST — apps

Same tokens and scopes as MCP, and the same limits: an app here sees exactly what an MCP client holding the identical grant would, and neither can write to the vault. GET /api/context, POST /api/propose, GET /api/subjects, GET /api/subjects/:id, POST /api/subjects and photo sub-routes, POST /api/generate. Detailed in REST — apps.

Two doors rather than one because MCP assumes the caller is a model. Most software is not. An app that already speaks HTTP should not have to adopt a new protocol to read six lines about its own user.

Owner door — the vault's own clients

Authenticated with the vault passphrase rather than an app scope, so approval powers can never be obtained through a consent screen. Covers facts, subjects, voice, review, audit, connections and export. Device tokens are stored hashed and are revocable like any app.

5. Scopes

Per-category grants (identity, work, projects, preferences, relationships, communication-style) plus propose, likeness, likeness:write and likeness:voice. Clients SHOULD send the scope parameter on /authorize: the consent screen pre-selects exactly what was requested and tucks the rest away. See Scopes & consent.

6. Governance invariants

These are normative — an implementation that breaks one isn't Helix.

  • Apps MUST NOT write facts directly. Proposals enter a queue the owner approves or rejects. The same applies to labels, which decide what other apps can see.
  • Labels MUST only narrow. No label may reveal a category the grant does not carry, and private entries MUST NOT cross any app door.
  • Source media MUST NOT cross the app door. Apps receive names, thumbnails and finished generations only.
  • Every read, proposal, write and generation MUST be recorded in an audit log visible to the owner. In a shared vault the entry MUST also name the principal whose grant performed it, and every member MUST be able to read that log. The party most likely to misuse shared context is not an app; it is another member. The principal field sits outside the hashed payload so that adding it does not invalidate chains written before it existed.
  • Audit entries MUST be hash-chained. hash = SHA-256(seq ␟ at ␟ client ␟ action ␟ detail ␟ prev) — the six fields as strings in that order, joined by U+001F (the ASCII unit separator, 0x1F), UTF-8 encoded, hex-encoded lowercase. previs the previous entry's hash, empty for the first, and the trailing separator before it is present even then. A separator is required rather than plain concatenation so that no combination of field contents can produce the same canonical string as a different entry. It is non-printing, so verify against a test vector rather than against what your editor displays. A verifier MUST report an unverifiable entry as broken, and an entry written before the chain existed as unchecked — those are different claims.
  • Test vector, for an independent implementation to check itself against:
    seq    1
    at     2026-07-27T09:16:00.000Z
    client You (web)
    action write
    detail created the vault
    prev   (empty)
    
    hash   0f00a168235b4fc4ebc52dd079f3737ee06b6a9343e13286d368f23f9abe151a
    If you get b0735bf9b3429c6d62a65e8b3bd96d46144416504dd50e7133359f1dbf666db6 you have concatenated without the separator. This is the mistake this spec previously invited, and it is easy to make, because the separator is invisible in source.
  • A vault MAY have more than one member. Where it does, an entry carries a subject: a fact about a particular member MUST be approved by that member and by nobody else, whatever role anyone holds. An owner is not a superuser over facts about other people. Facts about the vault as a whole are approved by any owner or curator.
  • An app MUST NOT be able to name the subject of a proposal. It may declare only whether a fact concerns the principal who granted it or the vault as a whole; the server resolves the subject from the grant. Membership MUST NOT be disclosed to any app.
  • A vault holding facts about a person who is not its owner MUST be unreadable by every app until that person accepts, through their own account. Awareness is not acceptance. Where the subject cannot yet answer for themselves, the vault MUST record that it is held on their behalf, and MUST become unreadable again on the date it passes to them, until they take it.
  • Leaving a shared vault MUST be unilateral and immediate, and MUST NOT require any other member's approval. Every member MUST be able to enumerate every other member: no implementation may offer a role that reads a shared vault invisibly.
  • Revocation MUST take effect immediately, including mid-session.
  • Generation MUST name its downstream provider on the consent screen and in the audit entry.
  • The owner MUST be able to export everything and delete everything without assistance.

7. Not yet specified

Stated plainly rather than left ambiguous: server-to-server federation, and end-to-end encryption (today: encrypted at rest, operator-readable — see the security note).