_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.yaml1234people: 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}
admin role on a dedicated identity rather than an employee's account — see Spaces & Permissions.| Field | Type | Required | Meaning |
people | map of id → person | yes | The key is the person's id (used in vault paths and grants). |
name | string | yes | Display name. |
roles | list of strings | no | Roles this person holds; resolved against role: grants. Defaults to none. |
teams | list of strings | no | Teams this person belongs to; resolved against team: grants. Defaults to none. |
email | string | no | The 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. |
roles: admin instead of roles: [admin]) is rejected rather than silently coerced.spaces.yaml12345spaces: - {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"]}
| Field | Type | Meaning |
path | string | The space path. A trailing /* makes it a wildcard over one segment. |
read | list of grants | Who may read notes in this space. |
write | list of grants | Who may write them. |
| Grant | Matches |
everyone | Every person in org.yaml. |
role:<name> | Anyone whose roles include <name>. |
team:<name> | Anyone whose teams include <name>. |
person:<id> | Exactly that person. |
{name} binding{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.Teams/* for the general case and add a specific Teams/leadership rule to carve one exception — the wildcard still governs every other team._meta/config.yaml — vault-level settings123456# _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
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.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.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.yamlbrain 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:1234567891011121314sources: - 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
| Field | Required | Meaning |
id | yes | URL path segment; lowercase letters, digits, hyphens. Must be unique. |
verify | yes | standard-webhooks (HMAC-SHA256 signatures with replay protection) or token (Authorization: Bearer / X-Brain-Token header). |
secret_env | yes | Name of the environment variable holding the secret. The secret itself never lives in this file; an unset variable refuses startup. |
person | one of | Route every delivery to this person's Inbox. |
route: sender-email | one of | Resolve the person from the payload's email_field against org.yaml emails — unknown senders are refused. Exactly one of person/route per source. |
email_field | no | Payload field for sender-email routing. Default email. |
source | no | Provenance label stamped on each note. Default: the source id. |
body_field / title_field | no | Payload fields for the note body and title (dotted paths reach nested JSON). Defaults body / title. |
rate_limit | no | Max 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. |
brain doctor reports config errors and unset secret variables._meta/cache/health.json — the generated health snapshotbrain cycle overwrites it with what
brain doctor found, reduced to counts:12345678{ "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 } }
doctor's exit code cannot carry that, because
only error severity moves it and most findings are warnings._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.ok is derived — it is false exactly when some error: key is present,
so it never disagrees with counts.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.brain/ directory. It answers one
question that nothing else in brainkit answers: is search returning
anything?1{"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}
.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.jq . makes it readable..brain/ is gitignored (see the vault .gitignore every compile writes), so
this never enters git history or a compiled vault..brain/
does not exist until the first search runs, so create it first if needed:123mkdir -p <vault>/.brain touch <vault>/.brain/retrieval-log.on # on rm <vault>/.brain/retrieval-log.on # off
<vault>/.brain/retrieval.jsonl
holding the query, the mode, and the path and space of each hit. Note
snippets are never written.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.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.1ls -r <vault>/.brain/retrieval.jsonl.*.gz 2>/dev/null | xargs -r gunzip -c; cat <vault>/.brain/retrieval.jsonl
1test -f <vault>/.brain/retrieval-log.on && stat -c %Y <vault>/.brain/retrieval-log.on
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.)1rm -f <vault>/.brain/retrieval.jsonl*
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.brain mcp --http and brain mcp-router (flags win over variables). Needs the optional extra: pip install 'brainkit[remote]'. See Remote Access.| Variable | Meaning |
BRAIN_MCP_ISSUER | The edge's address, e.g. https://<team>.cloudflareaccess.com. Must be https://. Signed requests must name this issuer. |
BRAIN_MCP_JWKS_URL | Where 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_AUDIENCE | The Access application's AUD tag. Signed requests must name it. |
BRAIN_MCP_HEADER | Header carrying the signed identity. Default Cf-Access-Jwt-Assertion. |
BRAIN_MCP_PORT, BRAIN_MCP_EMAIL | Used by the shipped brain-mcp@.service template: one person's port and sign-in email. |
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).| Variable | Default | Meaning |
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_MODEL | text-embedding-3-small | Embedding model id. |
BRAIN_EMBED_DIM | 512 | Output dimension (sent as dimensions). |
BRAIN_EMBED_CACHE | ~/.cache/brain/embeddings.db | Shared 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). |
${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:123456789mcp_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}
${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.~/.config/brain/config.yaml (override the path with BRAIN_CONFIG):12345embeddings: 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
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.