Brainy Brainy
Docs Brainy

Connect your terminal

In this section

The fastest way to reach brainy from a terminal is not a config file — it is one plugin, and every brainy door hands it out. .claude-plugin/plugin.json, a .mcp.json pointer, and eleven skills (recall, remember, briefs, the Accords board, the working method) ship together, versioned with the engine itself, and are always current because the door that installs them is the door you are about to talk to.

The two-word install

Tell your terminal:

join brainy.soulcraft.com

or, for a door you run yourself:

join https://<your-door>

Either phrase is an instruction to fetch that door's own GET / (or GET /join) page and follow it. The page is agent-readable on purpose — markdown by default, a minimal HTML page for a browser — and names exactly two commands:

claude plugin marketplace add https://<your-door>/plugin/marketplace.json
claude plugin install brainy@brainy

Then restart: claude --continue. That is the whole install, for any door, hosted or self-hosted — the page always names ITS OWN URL, never a fixed address, so the same two-word phrase works against any brainy door.

The three paths

1. Hosted — brainy.soulcraft.com

Nothing to run. Add the marketplace and install as above (door defaults to https://brainy.soulcraft.com), then either:

  • Sign in. When the hosted door offers it, the first tool call opens your browser, you log in, and the harness stores the token — nothing copied, nothing typed. This is the MCP authorization flow the harness itself implements: the door answers an unauthenticated POST /mcp with a 401 naming its own /.well-known/oauth-protected-resource document, the harness reads it, and the sign-in runs.

  • A suite key. Set BRAINY_API_KEY to a key your account issues. Only needed when the door does not offer sign-in.

2. Self-hosted — your own brainy host

Run brainy host --brains-dir <dir> --listen <host:port> --keys <file> --public-url <https://your-door>. That same process now serves the plugin too — GET /plugin/marketplace.json and GET /plugin/brainy-<version>.zip — with .mcp.json baked to point at ITSELF, so a plugin installed from your door always talks back to your door, never brainy.soulcraft.com.

Two ways a caller gets a credential at your door:

  • Offer sign-in through someone else's issuer. Start the host with --auth-server <url> naming your authorization server. An unauthenticated /mcp request then answers 401 with the same RFC 9728 discovery document the hosted door uses, and the tokens that server mints are the bk1 tokens this host already verifies against your issuers file — nothing else changes. The served plugin's .mcp.json OMITS the Authorization header entirely in this mode, so the harness's own sign-in flow runs instead of sending an unset environment variable literally.

  • Be your own issuer. --sign-in [<specifier>] makes THIS door its own OAuth-for-MCP authorization server — mutually exclusive with --auth-server above (that points at someone else's issuer; this one makes your door the issuer). <specifier> is a package name or a path, default @soulcraft/identity-kit — the private kit that gives a self-hosted door the same email-code-and-passkey sign-in, the same bk1 issuer and the same golden vectors the hosted door runs on, so the two can never drift apart. Also requires --public-url (the sign-in server needs a fixed origin) and --sign-in-mail <url> (your own mail sender — POSTed { email, code, expiresInMinutes }, bearer-authenticated with your service key, the same pattern --reading-door already uses). Accounts live in a SIBLING store your door opens beside the ones it serves (<brains-dir>/.identity), never inside a tenant brain — tenant policy and exports never see a passkey or a session row. Which store a signed-in account opens is your OWN register, beside --keys (one person:<sub> <store> line per account, --sign-in-accounts <file> to move it); an account with no line is refused by name — this leg ships no self-serve provisioning, so nobody signs in to a brand-new brain your door invented on the spot. A minted token carries no entitlement claim at all (never "suite"); a wider ceiling check on EVERY issuer's tokens is still owed — see docs/host.md's own --sign-in section. Full detail: docs/host.md.

  • Issue keys yourself. POST /v1/keys with the service key mints a static key for a person, business or service principal. Hand it out, and each holder sets BRAINY_API_KEY. The served plugin's .mcp.json carries the ordinary Authorization: Bearer ${BRAINY_API_KEY} header in this mode.

3. Package holders — offline install

Already have @soulcraft/brainy installed? No door needs to be running yet:

npx brainy plugin install --door <https://your-door>

This copies the package's own bundled plugin to ~/.config/soulcraft/brainy-plugin/<version>/ (a stable path, never node_modules), bakes --door into its .mcp.json, and runs the harness's own claude plugin marketplace add / claude plugin install commands against it. It never writes or asks for a key — it prints the same guidance as above: set BRAINY_API_KEY, or sign in if the door offers it.

Local zero-config — when the store and the terminal are on the SAME machine and you own both ends: npx brainy plugin install --local <brains-dir> --door <url> --keys <file> (the same --keys file your brainy host was started with) mints a per-install key, writes it to ~/.config/soulcraft/brainy-plugin/key (mode 0600), and sets BRAINY_API_KEY in your harness settings (~/.claude/settings.json's env block) — nothing to type, nothing to copy. This is the one case this CLI ever writes a credential; every remote door still gets sign-in or an admin-issued key.

npx brainy plugin url [--door <url>] prints a door's served marketplace URL, for scripting.

Embedded in an application

There is no terminal door here unless the embedding application ALSO runs brainy host — the plugin is a terminal-facing artifact; an application that links @soulcraft/brainy directly uses the in-process API (docs/overview.md) and never needs it.

Environment variables

Variable

What it does

BRAINY_URL

The door the plugin's .mcp.json points at, and the host every hook's session doors POST to. Defaults to https://brainy.soulcraft.com when unset.

BRAINY_API_KEY

The bearer credential, read by .mcp.json's Authorization header and by every session-door hook — a FLAT variable, never a nested default. Unused when the door offers browser sign-in.

BRAINY_PROJECT

The project field a session door's own request carries. Unset means null — never guessed from the terminal's working directory.

What the plugin contains

  • The pointer. .mcp.json names one door, over Streamable HTTP — nothing else runs locally.

  • The hooks, ported to plain Node (no bun required — this package already requires Node ≥ 22) and audited one by one for what a plugin every brainy user installs may do. Five of the six are thin door calls — read the harness's own hook stdin, redact it at the client's fixed denylist, POST it, answer per the door's shape — and every one fails the same way when the door cannot answer: one named line on stderr, never a blocked session.

    • guardrail-check (PreToolUse) — calls guardrail_check at the door before a tool runs (see docs/guardrails.md). Fails OPEN, loudly, when the door is unreachable — a default-shipped hook must never turn a network blip into a blocked session; only a real refuse/ask verdict blocks.

    • session-start (SessionStart → POST /v1/session/start) — hands the door your session's identity and gets back markdown context for the session that is beginning, printed verbatim.

    • session-capture (PostToolUse → POST /v1/session/capture) — hands the door the tool call you just made; when the answer carries a doorbell pointer (a board write that named parties who should hear about it now), relays the ring instruction as context. That field is anima's own owner-layer wrapper's, on the hosted door — the engine's own generic capture (below) never carries one.

    • session-save (Stop and PreCompact → POST /v1/session/save) — saves the session so far, on a stop or ahead of a compaction.

    • session-wrap (SessionEnd → POST /v1/session/wrap) — wraps the session as it ends.

    Every door answers { served: true, … } or { served: false, reason } — never a silent no-op and never a stub. Every request is bounded at 262,144 bytes (256 KiB) — over it, the door refuses SessionPayloadTooLargeError rather than reading an unbounded body, since session-capture now fires on every tool call.

  • The skills — memory-keeper (the recall → cite → remember loop) plus ten method skills (brief, counsel, design-craft, diagnose, home, oracle, panel, table-voice, verify, wrap) carrying a working method onto every machine that installs this plugin.

One client-side redaction pass, nothing semantic. Before any hook payload leaves the terminal, a FIXED denylist strips five machine-shaped secrets — this plugin's own key files under ~/.config/soulcraft/, an Authorization: Bearer … value, a SOULCRAFT_SERVICE_SECRET=… assignment, any *_API_KEY=… assignment, and a PEM-armored block — replacing each with a [REDACTED:<kind>] marker; nothing else is touched, because a pattern that guesses at "sensitive-looking" prose is a pattern that mangles ordinary conversation.

No secrets. The shipped archive's own build pins two things: its entry list matches an explicit allow-list (no file outside it ever ships), and no entry's text matches a bearer value, an sk--prefixed key, a minted brainy credential, or a JWT — the only Bearer the archive ever carries is the literal ${BRAINY_API_KEY} placeholder.

The generic road

Through 12.9.x every door on every host answered the identical, honest { served: false, reason } document for a well-formed request: session composition was purely anima's own hosted work, and no build of this engine composed one itself. Since session-capture-road-generic-in-engine (12.10.0), brainy host and brainy mcp compose all four themselves, in process, against your own store — a self-hosted brain gets real session doors with no anima account in the picture at all. brainy-serve (the Rust binary) is the one build still answering the old document: it attaches every store read-only, so it cannot compose these yet either.

What each hook writes, where, and what a host hands back:

Hook

Writes

Reads back

session-start

Nothing.

A markdown context built from memory.digest()'s head count and memory.sessions()'s newest lines — the "From your memory" section alone.

session-capture

Appends the redacted event to /sessions/<id>/episode.jsonl — one VFS file per session, written as brainy 12.9.0's WORKING-STATE class (retention: 'latest-only'): the whole file is rewritten each time, no history generation and no change-feed frame are kept for it, and readFile({ asOf }) / history() refuse WorkingStateHasNoHistoryError by name. Bounded to the last 2,000 events or 4 MB, whichever is hit first — the oldest are dropped and the running count is recorded on the file's own head line.

{ served: true, captured: true }.

session-save

Appends the redacted event this call carries to the SAME file session-capture writes — unconditionally, exactly like session-capture (the checkpoint itself is the event worth recording, even when the Stop/PreCompact hook's own payload is otherwise empty); never a second, durable snapshot.

{ served: true, saved: true, events: N } — the file's running event count.

session-wrap

Reads that file's accumulated events, composes their transcript, and writes ONE memory.remember() row (type: 'document', subtype: 'session-transcript', source: 'conversation', visibility: 'internal', metadata.sourceConversationId: <sessionId>, metadata.tags naming session:<id> and, when the session named them, project:<slug> / harness:<h>). The sourceConversationId field is what lets the store's own memory-episodes job group one session into one run instead of braiding every session sharing the 'conversation' source into one — the job derives the actual episode NODE asynchronously, on its own five-minute cycle, once the run has gone quiet (or a later session, or the 24-hour span bound, closes it sooner).

{ served: true, wrapped: true, memoryId: <id> } — the row this call just wrote, not the (not yet existing) episode.

Three pieces stay anima's own, deliberately, not carried here: a project's newest resume page and its working-state.md document under /projects/<project>/strategy/ (a fleet convention this engine has no notion of), the board-inbox line (accord_inbox, an Accords-board convention), and CAPTURE's own doorbell relay (the board-write pointer session-capture's hook prints as context) — all four ruled explicitly to stay on anima's thinned fleet layer (session-capture-road-generic-in- engine), never invented here. Anima's own hosted door still composes all of it; a self-hosted brainy host answers the table above alone, and a client reading either one's session-start context sees only what that build actually composed — never a placeholder standing in for the rest.

What an expired plan changes

A minted key can carry an optional entitlement claim — "suite" or "expired" — the bearer's plan, as its issuer wrote it. Most keys carry no claim at all, and nothing here applies to them: absence is today's law, unchanged, never read as expired.

When a key's plan reads "expired", every door that writes refuses by name (EntitlementExpiredError) — the five hooks' own session doors included, since a session door writes no entity but wants the same write posture as one that does. Reading never stops. find, get, counts and every other read door keep answering, export keeps answering, and session-start's own context read keeps composing — an expired plan narrows what a bearer may CHANGE, never what it may SEE. Renewing the plan is the only cure; nothing about the key itself changed, so no new key is needed once it is renewed.

The claim is per BRAIN, not per credential: a key minted for someone other than the brain's own owner (a grant) carries the OWNER's own plan, set by the issuer at mint time, never the guest's — a guest of a suite owner writes freely, and a guest of an expired owner is refused the same way the owner themselves would be.