Agent-readable docs index: /llms.txt. Full docs in one file: /llms-full.txt. Download /docs.zip to grep all markdown files locally.

Configuration

Two files under _meta/ describe the whole company: who exists (org.yaml) and who sees what (spaces.yaml) — plus an optional third, webhook.yaml, when you run the webhook intake receiver. All are plain YAML, validated on load — a malformed file is a clear error, never a silent misconfiguration. Nothing under _meta/ is ever compiled into a person's vault. brainkit also writes one file there itself — the health snapshot every cycle refreshes for a control plane to read, documented at the end of this page.

org.yaml

people: admin: {name: Admin, roles: [admin]} # dedicated curation identity — no agent alice: {name: Alice Nguyen, teams: [sales], email: alice@acme.com} bob: {name: Bob Rivera, teams: [ops], email: bob@acme.com}
Keep the admin role on a dedicated identity rather than an employee's account — see Spaces & Permissions.
FieldTypeRequiredMeaning
peoplemap of id → personyesThe key is the person's id (used in vault paths and grants).
namestringyesDisplay name.
roleslist of stringsnoRoles this person holds; resolved against role: grants. Defaults to none.
teamslist of stringsnoTeams this person belongs to; resolved against team: grants. Defaults to none.
emailstringnoThe person's address, used by brain ingest --from and brain webhook sender-email routing to resolve intake to them. Must be unique across people; a duplicate is a load error.
Field types are validated: a scalar where a list is expected (roles: admin instead of roles: [admin]) is rejected rather than silently coerced.

spaces.yaml

spaces: - {path: Company, read: [everyone], write: ["role:admin"]} - {path: "Teams/*", read: ["team:{name}"], write: ["team:{name}"]} - {path: "People/*", read: ["person:{name}"], write: ["person:{name}"]} - {path: "Clients/*", read: ["role:admin"], write: ["role:admin"]}
FieldTypeMeaning
pathstringThe space path. A trailing /* makes it a wildcard over one segment.
readlist of grantsWho may read notes in this space.
writelist of grantsWho may write them.

Grants

GrantMatches
everyoneEvery person in org.yaml.
role:<name>Anyone whose roles include <name>.
team:<name>Anyone whose teams include <name>.
person:<id>Exactly that person.

The {name} binding

In a wildcard rule, {name} binds to the matched path segment. Teams/* with read: ["team:{name}"] means "the Teams/sales space is readable by the sales team" — one rule covers every team without enumerating them.

Exact beats wildcard

When a space matches both an exact rule and a wildcard, the exact rule wins. Define Teams/* for the general case and add a specific Teams/leadership rule to carve one exception — the wildcard still governs every other team.
A path that matches no rule resolves to no space. The compiler excludes it from every slice and write-back rejects edits to it. Unmatched is closed, not open — there is no implicit "readable by default."

_meta/config.yaml — vault-level settings

Optional. Three keys name the vault's top-level trees:
# _meta/config.yaml — vault-level naming entities: Clients # top-level tree for third-party spaces entity: client # singular form: used in guidance text and as the # request frontmatter key (client-name:) and folder # (ClientRequests/) shared: Company # the shared top-level space everyone reads
Missing file or keys fall back to the defaults shown (a missing entity derives from entities by stripping a trailing "s"). A malformed file fails the cycle loudly rather than silently reverting the vault's vocabulary. Change the pair on a live vault with brain rename-entities, not by hand — the command also renames the folder and rewrites the affected rules and pending queues.
entities may not be Company, Teams, People, or _meta (checked case-insensitively), and may not equal shared. shared may not be Teams, People, or _meta, and is set at brain init --shared — it has no rename command, so pick it at the start. When a key is absent the default applies; permissions never depend on any of these names — spaces.yaml is the only authority on who reads what.
The default pair (Clients/client → ClientRequests/, client-name:) is one case of a general rule: the request folder is entity with its first letter capitalized, plus Requests (family → FamilyRequests/), and the frontmatter key is <entity>-name (family-name:). Reach for --entity/entity: whenever the naive plural-stripping default would get an irregular noun wrong, e.g. --entities Families --entity family.

Changing the org

Adding a person, a team, or a space is an edit to these two files plus a brain compile. There is no migration step: the compiler reads the current org.yaml / spaces.yaml on every run, so the next compile reflects the change for everyone.

webhook.yaml

Declares the sources brain webhook serves — opt-in to activate, but recommended for any deployment with push sources (meeting recorders, automation platforms). brain init scaffolds a commented _meta/webhook.yaml.example; copy it to webhook.yaml to enable. Each source is one URL path (/hook/<id>), one verification mode, and one routing rule:
sources: - id: fathom-alice # POST /hook/fathom-alice person: alice # fixed routing verify: standard-webhooks # HMAC-signed senders (Fathom, Composio, …) secret_env: WEBHOOK_FATHOM_ALICE_SECRET source: fathom # provenance label; defaults to the id body_field: transcript # payload field for the note body (dotted paths ok) title_field: title # payload field for the title (optional) - id: zapier-intake route: sender-email # resolve the person from the payload email_field: email # …via this field, against org.yaml emails verify: token # shared-secret header (Zapier, Make) secret_env: WEBHOOK_ZAPIER_TOKEN
FieldRequiredMeaning
idyesURL path segment; lowercase letters, digits, hyphens. Must be unique.
verifyyesstandard-webhooks (HMAC-SHA256 signatures with replay protection) or token (Authorization: Bearer / X-Brain-Token header).
secret_envyesName of the environment variable holding the secret. The secret itself never lives in this file; an unset variable refuses startup.
personone ofRoute every delivery to this person's Inbox.
route: sender-emailone ofResolve the person from the payload's email_field against org.yaml emails — unknown senders are refused. Exactly one of person/route per source.
email_fieldnoPayload field for sender-email routing. Default email.
sourcenoProvenance label stamped on each note. Default: the source id.
body_field / title_fieldnoPayload fields for the note body and title (dotted paths reach nested JSON). Defaults body / title.
rate_limitnoMax accepted deliveries per minute for this source. Default 60. Excess deliveries get a retryable 429 with a Retry-After header — providers redeliver them, so a backfill slows down rather than dropping notes. Raise it before a bulk import; lower it for a source that should stay quiet.
Validation fails closed: any invalid entry rejects the whole file, and brain doctor reports config errors and unset secret variables.

_meta/cache/health.json — the generated health snapshot

Unlike everything above, this one is written by brainkit, not by you. Every brain cycle overwrites it with what brain doctor found, reduced to counts:
{ "schema": 1, "generated_at": "2026-08-19T20:30:00Z", "brainkit_version": "0.4.3", "ok": false, "counts": { "error:symlinks": 1, "warn:fact-conflict": 3 }, "tamper": { "clients": 0, "shares": 0, "promotions": 0 } }
It exists so a control plane can answer "is this brain healthy?" without an operator opening a terminal — doctor's exit code cannot carry that, because only error severity moves it and most findings are warnings.
Counts and check ids only — never a finding's message or its paths. Finding text quotes note paths and can name a restricted client, so the reduction happens at the source rather than being filtered downstream by whoever reads the file.
Four things worth knowing if you consume it:
  • It lives under _meta/cache/, which brain init gitignores, so the snapshot never enters git, never compiles into anyone's vault, and never travels to a clone. A master predating that template gets no file at all rather than a committable one, and brain cycle says so in its health_warnings.
  • Absence means "cannot say", never "no findings". A box running a brainkit older than 0.4.3 publishes nothing. Reading that as a clean brain is the one mistake this file exists to prevent.
  • ok is derived — it is false exactly when some error: key is present, so it never disagrees with counts.
  • One count doesn't come from doctor: error:compile-failed, the number of people whose vault failed to build in that cycle. The cycle's own output names them (compile_failures); the snapshot carries only the count. Like any error: key, it makes ok false.

.brain/retrieval-stats.json — the generated retrieval snapshot

Written by every search into each vault's .brain/ directory. It answers one question that nothing else in brainkit answers: is search returning anything?
{"schema":1,"person":"joe","brainkit_version":"0.4.11","updated_at":"2026-08-19T14:02:11Z","searches":412,"zero_hit":157,"by_mode":{"hybrid":380,"no-index":2},"warn":{"vector-degraded":8},"raw_truncated":false,"raw_log_wrapped":false}
Counters are cumulative and only ever increase. A recompile does not reset them — .brain/ is carried across the compiler's two-phase swap, the same way .git is. The only thing that resets them is a new container volume. The useful reading is the ratio zero_hit / searches rather than either number alone.
by_mode keys are the search modes: hybrid, hybrid+graph, keyword-only, keyword-only+graph, and no-index for a search that ran before brain index had built anything. warn counts categories, never messages — warning text can contain a filesystem path, and this file never carries one.
The file is compact single-line JSON because it is read by line-oriented tooling. jq . makes it readable.
.brain/ is gitignored (see the vault .gitignore every compile writes), so this never enters git history or a compiled vault.

The raw query log

Off by default. Create the sentinel to switch it on for one person. .brain/ does not exist until the first search runs, so create it first if needed:
mkdir -p <vault>/.brain touch <vault>/.brain/retrieval-log.on # on rm <vault>/.brain/retrieval-log.on # off
While it exists, each search appends a line to <vault>/.brain/retrieval.jsonl holding the query, the mode, and the path and space of each hit. Note snippets are never written.
The log rotates rather than filling up, so it always holds the most recent searches. At 2.5 MB the live file is compressed into retrieval.jsonl.1.gz and a fresh one starts; existing segments shift up, and the oldest of seven is discarded. That keeps roughly four times the history of the old fixed cap in less disk, and bounds it without anyone having to prune.
raw_truncated reports that the segment set is full — the next rotation discards the oldest searches. It is not an error, and capture never stops.
This meaning changed between releases: on brainkit 0.4.5-0.4.8 the same field name meant "the raw log hit a fixed cap and capture stopped" — the opposite operational meaning. A consumer that could see both eras had to parse brainkit_version to know which meaning it was holding. raw_log_wrapped (0.4.11+) carries the exact same value under an unambiguous name, so its mere presence in the file tells a consumer it is reading 0.4.9-or-later semantics without looking at the version at all; its absence means the box predates the meaning change (or predates 0.4.11, before the key existed). raw_truncated is retained only for old fleet consumers already reading it, and should be considered deprecated and removable once nothing on the fleet reads it.
To read the whole window, newest last:
ls -r <vault>/.brain/retrieval.jsonl.*.gz 2>/dev/null | xargs -r gunzip -c; cat <vault>/.brain/retrieval.jsonl
This log holds the literal text people typed. Whether capture is on, and since when, is not in the stats file — it is the existence and mtime of the sentinel itself, so it cannot be switched on and forgotten:
test -f <vault>/.brain/retrieval-log.on && stat -c %Y <vault>/.brain/retrieval-log.on
Prints the sentinel's mtime as a Unix timestamp when capture is on, and nothing when it is off. (retrieval-stats.json carried raw_log and raw_log_since mirroring this through 0.6.3; both were dropped in 0.6.4 because they only refreshed on a search, so they read stale for as long as nobody searched after capture was toggled — see the CHANGELOG.)
Removing the sentinel stops future capture only — it does not erase what was already captured, and it does not change whether the switch is on or off going forward: delete the data while the sentinel is still present and capture resumes accumulating immediately. Up to roughly 4.25 MB of query text can remain on disk indefinitely, spread across the live file and up to seven gzipped segments. To delete it, remove the sentinel first, then:
rm -f <vault>/.brain/retrieval.jsonl*
One glob, not a list of exact names: it also catches retrieval.jsonl.rotating and any retrieval.jsonl.<pid>.gz.tmp left by an interrupted rotation, and — unlike listing retrieval.jsonl.*.gz as its own argument — it still matches on a log that has captured searches but has not rotated yet. That case matters: under zsh a pattern matching nothing aborts the whole command, so listing the segments separately would leave the live file of query text in place while appearing to have deleted it. (With nothing captured at all the glob matches nothing and zsh will say so — there is nothing to delete in that state.) It leaves retrieval-stats.json and retrieval-log.on alone; neither is query text.

Remote access

Read by brain mcp --http and brain mcp-router (flags win over variables). Needs the optional extra: pip install 'brainkit[remote]'. See Remote Access.
VariableMeaning
BRAIN_MCP_ISSUERThe edge's address, e.g. https://<team>.cloudflareaccess.com. Must be https://. Signed requests must name this issuer.
BRAIN_MCP_JWKS_URLWhere the edge publishes its signing keys, e.g. <issuer>/cdn-cgi/access/certs. Must be https://. Fetched at startup (the process refuses to start without them) and refreshed every 12 hours or when an unknown key appears.
BRAIN_MCP_AUDIENCEThe Access application's AUD tag. Signed requests must name it.
BRAIN_MCP_HEADERHeader carrying the signed identity. Default Cf-Access-Jwt-Assertion.
BRAIN_MCP_PORT, BRAIN_MCP_EMAILUsed by the shipped brain-mcp@.service template: one person's port and sign-in email.

Embeddings

Retrieval's semantic leg needs an embedding provider. Without one, brain index and brain search still work in keyword-only mode — configure a provider to add the vector leg. Any OpenAI-compatible /embeddings endpoint works (OpenAI, Together, a company proxy, a local llama-server).
Configure via environment variables:
VariableDefaultMeaning
BRAIN_EMBED_BASE_URL—Endpoint base, e.g. https://api.openai.com/v1. Set this to enable vectors.
BRAIN_EMBED_API_KEY—Bearer token for the endpoint.
BRAIN_EMBED_MODELtext-embedding-3-smallEmbedding model id.
BRAIN_EMBED_DIM512Output dimension (sent as dimensions).
BRAIN_EMBED_CACHE~/.cache/brain/embeddings.dbShared embedding cache, used by brain index. brain cycle keeps each brain's own cache in <master>/_meta/cache/embeddings.db instead, and brain doctor reads that one (this shared cache only if the master has none).
An empty value, or one that is still an unresolved ${VAR} placeholder, counts as unset.
brain mcp reads these variables from its own environment, and some MCP clients start servers with a filtered one. hermes, for instance, passes only HOME, PATH, TMPDIR and the server stanza's env: block — so the stanza must forward them, or the server searches keyword-only:
mcp_servers: brain: command: brain args: ["mcp", "--vault", "/vault"] env: BRAIN_EMBED_BASE_URL: ${BRAIN_EMBED_BASE_URL} BRAIN_EMBED_API_KEY: ${BRAIN_EMBED_API_KEY} BRAIN_EMBED_MODEL: ${BRAIN_EMBED_MODEL} BRAIN_EMBED_DIM: ${BRAIN_EMBED_DIM}
hermes resolves each ${VAR} from its own environment when it starts the server. The agents-box image writes this block for you. When a search runs keyword-only against an index that has vectors, it warns no embedding provider configured.
Or a config file at ~/.config/brain/config.yaml (override the path with BRAIN_CONFIG):
embeddings: base_url: https://api.openai.com/v1 model: text-embedding-3-small dim: 512 api_key_env: OPENAI_API_KEY # the key is read from this env var, never stored here
Configuring a provider means each note's text is sent to that endpoint to be embedded. On the server, brain cycle --index caches embeddings under _meta/cache/ so a chunk shared across people is embedded once. A model change forces a full re-embed of every chunk.