Open Second Brain
Memory server
First committed by hermes-vps-agent
Published in itechmeat/open-second-brain. Publisher page
550 GitHub stars
What it is
Second brain for AI agents using Obsidian-compatible Markdown vaults. Works with Hermes, Claude Code, Codex, and OpenClaw.
AutoGineer overview, from the brain’s own files
Open Second Brain acts as a memory provider for AI agents, enabling them to read and write persistent knowledge directly into an Obsidian-compatible Markdown vault. It employs a deterministic 'dream' pass to convert repeated signals into actionable rules, while maintaining a hash-chained log for auditability. The system supports various agent clients through MCP servers, lifecycle hooks, and a dedicated CLI, ensuring that agent-learned information remains accessible, versioned, and searchable as standard Markdown files.
How it works
Memory model: Hybrid
AutoGineer overview, from the brain’s own files
The system organizes data into a Brain folder containing Markdown files. Agents interact with this vault via MCP servers, lifecycle hooks, or the o2b CLI. Signals from agents are stored in an inbox, processed through a dream pass to generate preferences, and stored as rules in active.md. A search index is maintained in a hidden directory and can be rebuilt from the source files. The system also supports truth ledgers, decision models, and staged review gates for agent writes.
| Folder | Role | Files |
|---|---|---|
| .github | agent-config | 7 |
| bin | scripts | 1 |
| docs | docs | 774 |
| hooks | hooks | 35 |
| scripts | scripts | 22 |
| skills | agent-instructions | 6 |
| src | source-code | 1496 |
| templates | templates | 23 |
| tests | tests | 2101 |
| .agents | folder | 1 |
| .ai-notes | folder | 1 |
| .apb | folder | 87 |
| .claude-plugin | folder | 2 |
| .codex-plugin | folder | 1 |
| .githooks | folder | 2 |
| install | folder | 16 |
| openclaw | folder | 1 |
| openspec | folder | 33 |
| plugins | folder | 21 |
| schemas | folder | 5 |
The agent reads
- install/codex.md evidence: README.md
- search_iterative_recall_enabled evidence: README.md
- search_pool_max_per_session evidence: README.md
- docs/decision-models.md evidence: README.md
The agent writes
- package.json evidence: CLAUDE.md
- CHANGELOG.md evidence: CLAUDE.md
- docs/brain-store.md evidence: README.md
- docs/updating.md evidence: README.md
Requirements
Runtimes: Node.js, Python >=3.11, Bun
Databases: SQLite
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Google Antigravity, Kiro, OpenCode, Amp, Roo Code, Cline, JetBrains Junie, Devin (ex-Windsurf)
Set it up
This brain runs code automatically: 54 hooks (codex), 3 scripts. Review .githooks/pre-commit, .githooks/pre-push, hooks/hooks.json, package.json, plugins/codex/hooks/hooks.json before opening the folder in Claude Code. No step below turns a hook on for you.
.githooks/pre-commit:runs on git pre-commit.githooks/pre-push:runs on git pre-pushhooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" session-capture; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook session-capture; exit 0hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" post-write-reminder; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook post-write-reminder; exit 0hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" subagent-inject; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook subagent-inject; exit 0hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" reground-deliver; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook reground-deliver; exit 0hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" active-inject; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook active-inject; exit 0hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" gap-agenda; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook gap-agenda; exit 0hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" backlog-nudge; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook backlog-nudge; exit 0hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" log-catchup; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook log-catchup; exit 0hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" view-render; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook view-render; exit 0hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" session-capture; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook session-capture; exit 0hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" session-capture; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook session-capture; exit 0hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" recall-inject; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook recall-inject; exit 0hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" nav-inject; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook nav-inject; exit 0hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" counterparty-brief; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook counterparty-brief; exit 0hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" reground-deliver; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook reground-deliver; exit 0hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" pretool-orient; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook pretool-orient; exit 0hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" pretool-host-backup; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook pretool-host-backup; exit 0hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" session-capture; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook session-capture; exit 0hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" stop-log-guardrail; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook stop-log-guardrail; exit 0hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" answer-quote-check; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook answer-quote-check; exit 0hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" hygiene-digest; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook hygiene-digest; exit 0hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" subagent-inject; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook subagent-inject; exit 0hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" session-capture; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook session-capture; exit 0hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" session-capture; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook session-capture; exit 0hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" gap-promote; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook gap-promote; exit 0hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" active-inject; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook active-inject; exit 0hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" session-capture; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook session-capture; exit 0package.json:bun scripts/prepare.ts || exit 0plugins/codex/hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" session-capture; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook session-capture; exit 0plugins/codex/hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" post-write-reminder; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook post-write-reminder; exit 0plugins/codex/hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" subagent-inject; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook subagent-inject; exit 0plugins/codex/hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" reground-deliver; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook reground-deliver; exit 0plugins/codex/hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" active-inject; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook active-inject; exit 0plugins/codex/hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" gap-agenda; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook gap-agenda; exit 0plugins/codex/hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" backlog-nudge; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook backlog-nudge; exit 0plugins/codex/hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" log-catchup; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook log-catchup; exit 0plugins/codex/hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" view-render; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook view-render; exit 0plugins/codex/hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" session-capture; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook session-capture; exit 0plugins/codex/hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" session-capture; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook session-capture; exit 0plugins/codex/hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" recall-inject; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook recall-inject; exit 0plugins/codex/hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" nav-inject; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook nav-inject; exit 0plugins/codex/hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" counterparty-brief; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook counterparty-brief; exit 0plugins/codex/hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" reground-deliver; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook reground-deliver; exit 0plugins/codex/hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" pretool-orient; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook pretool-orient; exit 0plugins/codex/hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" pretool-host-backup; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook pretool-host-backup; exit 0plugins/codex/hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" session-capture; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook session-capture; exit 0plugins/codex/hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" stop-log-guardrail; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook stop-log-guardrail; exit 0plugins/codex/hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" answer-quote-check; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook answer-quote-check; exit 0plugins/codex/hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" hygiene-digest; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook hygiene-digest; exit 0plugins/codex/hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" subagent-inject; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook subagent-inject; exit 0plugins/codex/hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" session-capture; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook session-capture; exit 0plugins/codex/hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" session-capture; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook session-capture; exit 0plugins/codex/hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" gap-promote; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook gap-promote; exit 0plugins/codex/hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" active-inject; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook active-inject; exit 0plugins/codex/hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" session-capture; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook session-capture; exit 0
- Not scanned by AutoGineerThe author’s MCP configuration (.claude-plugin/plugin.json) also starts it in a way AutoGineer does not offer. Read that file: AutoGineer did not scan that path; what it runs is listed under “This brain runs”. https://github.com/itechmeat/open-second-brain/blob/233b91d1bdc7cf74c47ea1c1fd72e949887e1fc4/.claude-plugin/plugin.json
First committed by hermes-vps-agent. Published in itechmeat/open-second-brain. Licence: MIT License.
Usage
AutoGineer overview, from the brain’s own files
- Initialize a new vault for agent memory
- Connect agents like Claude Code or Codex via MCP
- Manage agent preferences and rules through automated dream passes
- Audit agent activity using hash-chained logs
- Perform semantic or keyword searches across agent-generated notes
- Enforce write-path integrity with staged review gates
- “o2b brain today”
- Displays the current day's view and active memory markers for the agent session.
- “o2b brain rollback”
- Restores the vault state from the last snapshot, reversing recent brain mutations.
Documentation by hermes-vps-agent
Open Second Brain
Image: Open Second Brain - your knowledge, amplified by AI
Open Second Brain is a memory layer for AI agents that lives in an Obsidian vault. Agents read and write it through MCP servers, lifecycle hooks and the o2b CLI. Everything the agent learns is a Markdown file under Brain/, so you can open, edit, grep and version it like any other note. A deterministic dream pass turns repeated signals into rules and retires the ones nothing uses; no model runs inside that algorithm.
How it fits
flowchart LR
subgraph Clients["Agent clients"]
Plug["Plugins: Claude Code, Codex,<br/>Hermes, opencode, Grok Build"]
Mcp["MCP config: Cursor, kiro, Copilot CLI,<br/>Gemini CLI, ZCode, any MCP host"]
Claw["OpenClaw<br/>in-process plugin"]
Cliu["Aider, Pi<br/>CLI and context file"]
end
subgraph Product["Open Second Brain"]
Srv["MCP servers<br/>open-second-brain, open-second-brain-writer"]
Hooks["Lifecycle hooks<br/>o2b-hook"]
Cli["CLI<br/>o2b"]
Core["Core"]
end
subgraph Vault["Your vault"]
Brain["Brain/<br/>Markdown the agent writes"]
Notes["Your notes"]
Index[(".open-second-brain/brain.sqlite<br/>rebuildable search index")]
end
Plug --> Srv
Plug --> Hooks
Mcp --> Srv
Claw --> Core
Cliu --> Cli
Srv --> Core
Hooks --> Core
Cli --> Core
Core --> Brain
Core --> Index
Core -. reads, writes a named note on request .-> NotesEach client reaches the same core through a plugin, an MCP registration or the CLI. The core keeps its own records under Brain/ and a generated search index under .open-second-brain/ that it can rebuild from the files. Your notes are read; the note tools write one only at a path the caller names.
flowchart LR
Agent["Agent"] -->|brain_feedback| Inbox["Brain/inbox/<br/>signal"]
Inbox -->|dream pass| Pref["Brain/preferences/<br/>rule"]
Pref -->|rendered into| Active["Brain/active.md"]
Active -->|read at session start| Agent
Agent -->|brain_apply_evidence| Log["Brain/log/"]
Log -->|next dream pass| PrefThe learning loop: signals become rules, rules reach the next session through Brain/active.md, and applied or violated evidence confirms, quarantines or retires them. Details: docs/how-it-works.md.
Install
Install Bun 1.1 or later:
install/prerequisites.md.Install the plugin for your client (guides below), or get the
o2bCLI:install/prerequisites.md.Create the vault files:
o2b init --vault /path/to/vault --name "My Second Brain" o2b brain init --vault /path/to/vaultConnect the client (table below), then check the result:
o2b doctor --vault /path/to/vault o2b install --check
The full router with readiness criteria is install.md; native Windows is covered in install/windows.md.
Supported clients
| Client | Integration | Guide |
|---|---|---|
| Claude Code | Marketplace plugin: MCP servers and hooks | install/claudecode.md |
| OpenAI Codex | Marketplace plugin, then o2b install --target codex --apply | install/codex.md |
| ChatGPT desktop | Reads the shared Codex config: o2b install --target codex --apply | install/codex.md |
| Hermes Agent | Plugin and memory provider | install/hermes.md |
| OpenClaw | Native plugin, no MCP | install/openclaw.md |
| opencode | o2b install --target opencode --apply: MCP servers and a native plugin | install/opencode.md |
| Grok Build | o2b install --target grok --apply: MCP servers and hooks | install/grok.md |
| Cursor | o2b install --target cursor --apply | install/cursor.md |
| kiro | o2b install --target kiro --apply | install/kiro.md |
| GitHub Copilot CLI | o2b install --target copilot-cli --apply | install/copilot-cli.md |
| Gemini CLI | o2b install --target gemini-cli --apply | install/gemini-cli.md |
| ZCode | MCP servers added to its config by hand | install/zcode.md |
| Aider | o2b install --target aider --apply: context file, no MCP | install/aider.md |
| Pi | o2b install --target pi --apply: skill over the CLI | install/pi.md |
| Any other MCP host | o2b install --target generic --apply --out - prints the server entries | install/generic.md |
o2b uninstall --target <name> --apply removes what o2b install wrote; the vault is never touched (install.md).
Capabilities
Preferences that learn and expire. Signals, a deterministic dream pass, confidence and retirement: how it works.
Reversible changes. Brain mutations take a snapshot first;
o2b brain rollbackrestores it: snapshots.Attributed writes.
o2b brain writeslists every note write by agent and device,writes revertplans a restore,o2b brain freezestops all writers, ando2b brain log verifychecks the hash-chained log: Brain CLI.Search. Keyword search with an optional semantic lane, result explanations, recall profiles and compact cards; an opt-in iterative loop (
search_iterative_recall_enabled, default off) keeps querying when coverage gaps survive, pausing with a plan the caller answers throughbrain_search's round trip; and an opt-in per-source-session cap (search_pool_max_per_session, default off) keeps one long imported session from filling the candidate window, so diversity spreads across sessions: search CLI, iterative recall, pool cap, ranking.Session recall. Import agent transcripts already on disk, search them, or select whole-thread coverage under a character budget (
o2b brain session-grep --operation coverage --char-budget <n>, with--entity-keyseeding the fan-out from mined entity mentions): session logs, coverage.Hygiene.
o2b brain hygiene scanfinds contested, duplicate, stale and unused memories;applyruns only the findings you pick: maintenance.Claim ledger.
o2b brain truth ingestrecords an entity claim with an optional validity window (--valid-from/--valid-until; with the flags absent, a window on the source record's frontmatter is adopted and frozen at ingest),truth eventsrecalls a windowed slice of the ledger, andtruth stategrounds an agent-stated claim with an anchoring verdict - committed, or reported back with reason codes.truth stateaccepts an optional opaque--sample-idnaming the context-pack sample the claim drew on, and claim reads report the receipt and outcome provenance behind it at the caller's reach - "receipt known, outcome unknown", never an invented verdict: entity truth CLI.Corrections.
o2b brain lifecycle correct <target>reports a correction's blast radius and retires the affected records - validity-closed by default, tombstoned with--flatly-wrong, dry run by default and--applyto write. A retired record that recall can still serve is served only beside its correction, never alone: belief lifecycle CLI.Sharing and backup. Knowledge packs export a chosen subset;
o2b brain bank-exportwrites a one-file backup bundle: knowledge packs.Secret custody. Credentials live encrypted in the vault's state dir behind a capability-gated store;
$secret:NAMEreferences resolve at the use sites,o2b brain secret unlockwraps the keyfile under a passphrase, andsecret export/importmove the store between installs as one passphrase-encrypted bundle: custody CLI.Today view and note markers.
o2b brain today,@osb loopand@osb setmarkers: Brain CLI.Self-inspection.
o2b version,o2b install --frictionando2b state statusreport what is installed and where state lives: CLI reference.MCP surface. Tools, tool profiles for hosts with tool limits, and the always-loaded writer server:
docs/mcp.md.
Optional features
Semantic search: an embedding provider plus
sqlite-vec; theembeddings-setupskill walks through it:skills/embeddings-setup/SKILL.md.Decision models: a typed judgment model that can rerank search and filter candidates, off by default per use:
docs/decision-models.md.Deep relational recall: a fourth search arm over typed links, off by default. Its traversal runs under width budgets - 8 seeds, 4 edges per node, 16 nodes in total, and a hub above 12 walked edges is reached but not expanded - each overridable through an
OPEN_SECOND_BRAIN_SEARCH_TRAVERSAL_*environment variable or asearch_traversal_*config key; entity co-occurrence bridges join the walk by default and switch off separately (OPEN_SECOND_BRAIN_SEARCH_ENTITY_BRIDGES): retrieval quality.Staged review for agent writes, off by default. With no key set every write publishes exactly as before.
write_approval.notes/OPEN_SECOND_BRAIN_WRITE_APPROVAL_NOTES_ENABLEDandwrite_approval.ingest/OPEN_SECOND_BRAIN_WRITE_APPROVAL_INGEST_ENABLED(each falling back to thewrite_approval.enabledmaster, default off) stage note creates and ingest summary pages intoBrain/pending/beside the staged signals, where they stay out of the search index until an operator runso2b brain pending listand applies or rejects them; write-path integrity.A permissions document, absent by default.
Brain/_permissions.yaml(an operator-edited vault file) resolvesallow/ask/denyper agent, role and target for the write, ingest and owner-write actions; with the file absent every check behaves exactly as today.askstages the write,denyrefuses with thewrite-refusedtoken and the next commando2b brain permissions show, and every ask/deny verdict lands in a queryable decision ledger underBrain/logs/decisions/;o2b brain permissions showdry-runs the decision table,ledgerreads the rows; Brain CLI.The owner-write gate, off by default.
integrity.owner_scope_writesinBrain/_brain.yaml(off|warn|fail, defaultoff) refuses a caller-named owner that differs from the caller's resolved identity on the preference and note lanes (warnallows and records one decision-ledger row); a documentowner_writeverdict composes most-restrictive-wins with the gate; write-time integrity.Named MCP tokens, optional.
o2b mcp token mint|rotate|revoke|listkeeps a hash-at-rest token per agent (.open-second-brain/secrets/mcp-tokens.json; material shown exactly once). Over HTTP a valid token authenticates as its agent per request, the shared--api-keystays valid as the operator master credential, andmcp_tokens_required/OPEN_SECOND_BRAIN_MCP_TOKENS_REQUIRED(defaultfalse) makes a non-empty token map refuse credential-less requests.o2b bootstrap --target <harness> [--token] [--rotate] [--check]provisions MCP registration, token and receipt in one idempotent command, ando2b bootstrap --remove <harness>tears a provision down again; the minted token authenticates HTTP clients configured by hand, while the registered stdio harness presents no credential and keeps its config-derived identity; core CLI.Ambient capture consent, opt-out.
guardrails.ambient_writeback: falsesuppresses the ambient extraction lane with a countedambient-withheldevent (absent keeps today's behavior), andguardrails.ambient_ttl_daysstamps anexpiration_dateon ambient-extracted signals so reads drop them after the window (absent stamps nothing); a TTL-stamped signal still stages when the review gate is on and survives apply verbatim.Open decisions.
o2b brain decision open --title <t> --question <q> --option <o> [...]parks a question with enumerated options atBrain/decisions/open-<slug>.md;resolvemints the realtype: decisionpage,discardcloses without deciding, and the morning brief renders up to five open questions: belief lifecycle.
What is new
Open Second Brain 1.98.0 is the agent-presence and self-improvement release. o2b install --target <host> --apply now writes the brain-usage block into the agent-instruction file the installed host reads at the vault root (AGENTS.md for codex, cursor, opencode and aider; GEMINI.md for gemini-cli) under the same consent prompt as the config writes, preserving every byte outside the block and recording its ownership so uninstall removes exactly the pair the tool wrote; the dream pass gains an operator-approved exit into those same files, where a lesson promotes into a managed lessons block only with a fresh approval pinned to the approved text, and the doctor grades every block state it can name. The skill lane gains a benchmark-gated rewrite loop: a skill is rewritten only after a candidate beats the incumbent on scored dev tasks with full coverage and survives the one held-out check - Open Second Brain never calls a model, the scorer reads structural predicates and nothing the agent claims about its own work, and the accept stays an operator step on the command line. And ChatGPT desktop is named where its operators look for it: it reads the shared Codex config, so one codex registration serves both. Every release is described in the CHANGELOG.
Documentation
| Topic | Page |
|---|---|
| Mental model, vault layout, dream pass, safety properties | docs/how-it-works.md |
| Every CLI verb and flag | docs/cli-reference.md |
| MCP protocol, tools, writer split | docs/mcp.md |
| Brain store: setup, sync, migration, the human view | docs/brain-store.md |
| Architecture and configuration model | docs/architecture.md |
Updating (o2b update) and upgrade notes | docs/updating.md |
| Observability events | docs/observability.md |
| Metrics data contract | docs/metrics.md |
| Cross-project pointer | docs/cross-project-pointer.md |
| Scheduled Brain digest (Hermes cron) | docs/hermes-cron.md |
| Stability policy | docs/stability.md |
| Origin of the idea | docs/idea.md |
Project
CHANGELOG · Security policy · Source on GitHub · MIT License
docs/mcp.md
MCP tool server
The optional Model Context Protocol (MCP) server exposes Open Second Brain's deterministic operations as tools that Hermes Agent (or any other MCP client) can route through its tool registry.
The server is optional: the o2b CLI remains the supported baseline. Nothing
in Open Second Brain depends on the MCP server being running.
Protocol
Transport: stdio (JSON-RPC 2.0, newline-delimited) by default; optional Streamable HTTP on the same JSON-RPC core with per-request API-key auth.
Protocol version:
2025-06-18.Capabilities advertised:
toolsandresources(see "Resources" below). Nopromptsorsampling.Standard MCP lifecycle:
initialize,notifications/initialized,tools/list,tools/call, optionalping.Request ordering (stdio, since v1.96.0): read-classified calls run concurrently, up to four in flight — a constant, not a knob, backpressured at the cap so the bound holds by construction — while write-classified calls (every tool that CAN mutate vault state — the registry's
kindclassification is write-mode-dominant) run one at a time and never beside a read. The overlap pays only across awaited I/O: a read handler whose work runs synchronously between awaits (the SQLite, filesystem and scoring reads) finishes no sooner than serialized dispatch, so pipelining does not shrink a batch's total time, and its responses shift toward the end of the batch — the first response can arrive later than arrival-order delivery would. Write-classified tools, includingbrain_search(write-classified by the write-mode-dominant rule), still run one at a time. Responses may therefore arrive out of arrival order, which is legal JSON-RPC: correlate by responseid, and progress byprogressToken. Each request's own notifications all precede its response, and every frame stays newline-terminated. On EOF the server still waits for every dispatched request before the process exits. A signal drain instead stops at a request boundary and waits only up to its deadline (10 000 ms,O2B_MCP_DRAIN_MS; see "Shutdown and draining" below): a request still open when the deadline expires is named on stderr, and a line read but parked behind the concurrency cap when the drain started is left unanswered.
Argument contract (since v1.46.0)
Every advertised tool declares additionalProperties: false, and since
v1.46.0 the server enforces it. A tools/call carrying an argument the
tool does not declare is refused with -32602 (invalid params) naming
every undeclared argument, with a did-you-mean where a declared name is
close enough by edit distance:
brain_search: unknown argument 'quiery' (did you mean 'query'?). This
tool's inputSchema declares the <n> argument names it accepts.An argument with no near match is told so rather than pointed at an
unrelated parameter: unknown argument 'wombat' (no close match).
The error carries structured data for machine callers: tool,
unknown_arguments (each {name, suggestion?}) and declared_arguments.
Before this, a mistyped argument was ignored and the call returned a
normal success envelope computed from defaults.
The gate is top-level only. Nested object properties that declare their
own additionalProperties: false are not enforced; an unknown key inside
usage or inside an operations entry still reaches the handler.
Every advertised parameter also carries a description, enforced in CI by
the schema-completeness audit in src/mcp/registry-guard.ts. Output
schemas are out of scope for that audit - their vocabulary declares
union-typed fields with no type on purpose, and responses are validated
against them at request time instead.
Vault-relative paths are forward-slash on every host
A vault-relative path a tool returns (path, signal_path, page,
log_path, ...) is spelled with / on every operating system, including
a server running natively on Windows: Brain/inbox/sig-x.md, never
Brain\inbox\sig-x.md. The path names a note inside the vault, not a
file on the host, so it takes the vault's own Obsidian-style form. That
keeps the answer portable: an agent can feed it straight back into a tool,
match it against a wikilink, and compare it with the answer another device
sharing the same vault gives, whatever OS each device runs.
A path outside the vault is returned unchanged in its host form to a
caller at local reach (stdio, the CLI, a loopback HTTP bind). A caller at
remote reach never receives an absolute path of the host: a path in the
brain store is named store:<partition>/<path> (for example
store:shared/streams/events/...), a path in this machine's device-local
state is named local:<path> (snapshots, artifacts, attestation files and
other machine state of a store-only machine), and any other host path is
<device-path>. The store block of second_brain_status follows the
same rule for the store root, its partitions and the device state
directory. The server applies the rule to every tool result on its way
out, and to resource bodies and refusals, so a field whose value is a
host path (absolute_path, absolute_log_path, config_path, the state
surface list of vault_health) and a path a longer message mentions (a
check message, a warning, error prose) are named the same way at remote
reach: a path in the vault becomes vault-relative, the vault root itself
<vault>. Only this machine's own roots are matched (home, vault, store,
device state, temp and config folders), each as a whole path segment, so
text that only looks like a path, such as another user's home quoted in a
note, is left as written. expose_host_paths: true keeps every path raw.
Running with no vault
A server started with no vault configured keeps the memory contract on
the brain store alone. brain_feedback, brain_apply_evidence,
brain_note, brain_pinned_context, brain_context, brain_search,
brain_changes, brain_dream, brain_doctor, brain_settings and
second_brain_status answer from store records: a written record is named
store:<id> (a pinned register store:<partition>/pinned), the search
index is keyed by the store id, the dream pass runs over the store's folds
with no pre-run snapshot, and the doctor runs its store-side checks. A
second machine sharing the store finds the same records once its own index
catches up. A tool that works on notes, and the vault-file modes of these
tools (a doctor repair, a single dream step, the staged dream lifecycle, a
cross-vault search), refuse with vault_required.
Progress notifications
A client asks for liveness on a long call by putting a token on the
request, params._meta.progressToken. Per the specification the token is
a string or an integer; anything else — a boolean, null, an object, a
fractional number — is refused with -32602 naming what arrived, rather
than coerced into a value the client could not match back to its request.
A request with no _meta, or a _meta carrying other members but no
progressToken, asks for nothing and is answered exactly as before.
What each transport does with the token:
| Transport | With a token | With no token |
|---|---|---|
| stdio | Emits notifications/progress frames as the operation runs, all of them ahead of that request's response frame (two concurrent calls may interleave each other's frames; correlate by the frame's own progressToken) | No notification frames at all |
| HTTP | Answers the call normally and carries a typed refusal on result._meta | No _meta on the result |
stdio owns a duplex newline-delimited stream, so it can write a frame
nobody asked for. Each frame carries the spec's own progressToken,
progress and (when the operation knows a denominator) total, so a
client that knows nothing about Open Second Brain still renders a bar.
The typed event travels beside them under
params._meta["open-second-brain/progress"]: schema, operation,
kind, stage, completed, optional total and reason. stage is
an identifier from the emitting operation's own vocabulary, never a
sentence — the sentence is rendered at the edge.
The HTTP transport answers one request with one response and closes it (see "Run from the CLI" below), so it cannot carry a notification at all. Accepting the token and silently dropping the events would advertise liveness support that does not exist, so the response carries a refusal instead:
{
"result": {
"content": [ ... ],
"structuredContent": { ... },
"isError": false,
"_meta": {
"open-second-brain/progress": {
"schema": "o2b.progress.v1",
"kind": "refused",
"reason": "transport-single-response",
"progressToken": "tok-42"
}
}
}
}It rides on _meta — MCP's reserved place for implementation metadata,
and the reciprocal of where the request carried the token — because the
two alternatives corrupt a payload a caller parses: structuredContent
is validated against the tool's outputSchema, and content[0].text is
that same payload rendered. The refusal is visible to a client that asked
for progress and entirely absent for one that did not.
Which tools report. The MCP surface that can genuinely run for
minutes is brain_dream, brain_bridges, brain_clusters and
brain_maintenance. Index management verbs (index, reindex,
check) are deliberately not exposed over MCP, so they are not on this
list. brain_maintenance is a dispatcher over the lane's built-in
tasks and forwards the sink to each task, so its events name the
task that emitted them (dream, reindex, bridges, clusters,
relink) rather
than the lane. Two more reach a consolidation pass without carrying its
name and report on the same terms: brain_brief with view: "operator",
whose operator summary runs a dry-run pass, and
brain_review_candidates, whose projection is that same dry run
reshaped. A single-step brain_dream call reports too, under the stage
that names the step rather than the five stages of a full pass — the
step owns the stream, because on that path it IS the run.
Which tools are bounded. Every call that reaches one of those long
operations runs under a cooperative deadline resolved from
safeguard_timeout_<operation>_seconds, then safeguard_timeout_seconds,
then the built-in default. Bounded and observed are now the same
population with no exception: the step row below was the one that read
none, because the two step functions took no guard and dropped the
sink, and a single step over a large tree therefore held the server for
its whole duration with nothing to show for it. Both halves are wired.
That sentence is not left to prose — tests/mcp/long-running-tools.test.ts
reads the rows out of this table, fails on any row whose deadline or
reports cell reads none, and fails on a row naming a tool it does not
itself drive through both halves. The **none** row went stale for a
release because nothing checked it; its replacement is checked.
| tool | long operation it reaches | deadline | reports |
|---|---|---|---|
brain_dream (run) | dream, twice when expect/strict asks for a guard preview | dream — one budget for the whole call, preview included | yes |
brain_dream (stage/validate/apply) | dream, through the staged bundle | dream | yes |
brain_dream (step) | one step (scan or heal-enrich), not a pass | dream — a step is part of a dream pass, so it draws on that budget. Checked per file and per directory in scan, and per page in each of heal-enrich's two loops, plus around the two phases that cross no boundary of their own — the vault listing (checkpoint after it only) and the one-shot title/alias phrase build (before and after). So it stops within one page plus whichever of those two is running, not within one page flat | yes, under the step's own stage (scan / heal-enrich) |
brain_bridges (discover) | bridges | bridges | yes |
brain_clusters (run) | clusters | clusters | yes |
brain_maintenance (run) | the five built-ins plus declared custom tasks (since v1.65.0), sequentially; since v1.64.0 the reindex task stays keyword-only unless config maintenance_embeddings is true; then it requests the embedding phase when the resolved semantic config can reach a provider, announcing the predicted spend and receipting the completed pass (additive spend block: banner estimate, receipt with model, tokens, estimated_usd and whether force_cost overrode a refusing gate); since v1.89.0 the relink task previews the anchor additions it would make and rewrites the stored signal pages only under relink_apply, its per-source report riding the result as an additive relink block either way | one fresh guard per task; a tripped task is a timed_out row, not an aborted call | yes, in its tasks' voices |
brain_brief (view: "operator") | dream, dry run | dream | yes |
brain_review_candidates | dream, dry run | dream | yes |
The deadline is cooperative, not preemptive: dream() and the graph
sweeps are synchronous, so nothing can interrupt them from outside — past
the deadline the operation's next checkpoint throws, at a boundary where
writes are already atomic. Setting safeguard_timeout_dream_seconds: 0
disables the deadline for every row above whose budget is dream.
Cooperative also means the guarantee is "stops at the next boundary", and
a phase that crosses no boundary is therefore not interruptible however
long the budget has been gone. Two such phases exist and both are in
heal-enrich. They are NOT bracketed alike, and the difference is worth
stating rather than rounding to "both sides":
the vault listing, which walks every page and parses its frontmatter in one call, is the first thing the step does. Its only checkpoint is the one immediately AFTER it — there is nothing before it to check — so a budget that has already elapsed still pays for the listing once.
the phrase build, which sorts and regex-escapes the whole title/alias set in one, is bracketed on both sides: the checkpoint immediately before it means an elapsed budget refuses to pay for it, and the first page of the rewrite loop honours it again on the far side.
Either way a call that has already entered one of them runs it to the end.
src/core/brain/heal-run.ts names the same two and the same asymmetry.
Tool errors (since v1.66.0)
Every failed call now carries a stable, machine-readable code, so a
client can branch on the failure without matching English prose. The
prose itself is unchanged: every error message and every error text
block reads byte for byte as it did before, and a successful result
never gains the envelope. The one message that changed is the HTTP
transport's answer to a throw that escapes its dispatch: it used to be
the raw error message, and it is now internal error: <message> with the
message redacted for the bind's reach, the same answer the dispatcher
gives an internal error.
A failure reaches the client on one of two channels, and the code rides in a different place on each:
| Channel | When | Where the code is |
|---|---|---|
| JSON-RPC error | the request was refused before or around the tool: an unknown method, invalid params, an undeclared argument, a refusal the tool raises as a protocol error, a transport parse error | error.data.code |
| tool error result | the tool ran and failed: isError: true with one text block | result._meta["open-second-brain/error"].code |
On the JSON-RPC channel every error response carries error.data.code.
A code the refusing site chose (vault_frozen, budget_exceeded,
unknown_argument, a search error code) wins. Otherwise the server fills
in the default for the numeric JSON-RPC code: parse_error (-32700),
invalid_request (-32600), method_not_found (-32601), invalid_params
(-32602), internal_error (-32603). Existing data members such as
tool, unknown_arguments or errors are kept as they were, with
code added after them. A refusing site's code is itself a registry
member; the server checks it, still sends an unregistered one as it is,
and names it on stderr, JSON-quoted and capped at 120 characters
(warning: unregistered error code on the wire: "<code>").
A code member that is not a string is replaced by the default, and
data is always an object, never an array.
{
"jsonrpc": "2.0",
"id": 7,
"error": {
"code": -32602,
"message": "brain_search: unknown argument 'quiery' (did you mean 'query'?). ...",
"data": {
"tool": "brain_search",
"unknown_arguments": [{ "name": "quiery", "suggestion": "query" }],
"declared_arguments": ["query", "..."],
"code": "unknown_argument"
}
}
}On the tool-result channel the code rides on _meta under a namespaced
key, with a schema tag like the progress envelope's:
{
"result": {
"content": [{ "type": "text", "text": "<the error message, unchanged>" }],
"isError": true,
"_meta": {
"open-second-brain/error": {
"schema": "o2b.error.v1",
"code": "safeguard_timeout"
}
}
}
}It is not placed in structuredContent: a strict client validates
structuredContent against the tool's outputSchema even on an error
result, and an error object there would fail that check. When the same
call also carries a progress refusal, both keys sit side by side on the
one _meta object. A call refused on the JSON-RPC channel carries its
progress refusal in error.data, under the same
open-second-brain/progress key, beside code.
The code list is closed. Every code the server can send is a member of one registry, so a client can treat an unknown value as a newer server rather than as a malformed reply. It is the union of these generic tokens:
| Code | Meaning |
|---|---|
parse_error, invalid_request, method_not_found, invalid_params, internal_error | the default for each JSON-RPC error code, used when nothing more precise applies |
safeguard_timeout | the operation outlived its cooperative deadline (see "Which tools are bounded" above) |
safeguard_aborted | the operation's abort signal fired and it stopped at a checkpoint |
output_contract_failed | the tool produced a payload that does not satisfy its own outputSchema |
config_unreadable | the configuration file could not be read |
skill_not_found, skill_invalid_path, unknown_skill | a skill tool could not resolve the named skill, refused its path, or does not know it |
unknown_operation | a tool that dispatches on an operation, op or action argument (or a brain_write_batch operation's op) was asked for one it does not have; every operation-dispatching tool shares this code, except that a brain_memory_bridge host write keeps its pinned invalid_action |
invalid_status | brain_trigger was given a status outside its vocabulary |
trigger_transition_refused | a trigger transition was refused; an absent trigger and one the caller may not see answer with the same code and the same message |
write_session_unknown, write_session_terminal, session_id_required | a write-session call named no session, an unknown one, or one that has already ended |
unknown_argument | the call carried an argument the tool does not declare (see "Argument contract" above) |
brain_artifact_unparseable | a Brain artifact file (a preference or a retired rule, for example) could not be parsed |
argument_forbidden | brain_scaffold_stub was given an argument that belongs to its other action |
quote_unverified | brain_distill_source with strict_quotes: true refused the write because a quoted span in a claim did not verify against its source (see "Source distillation" below) |
store_write_failed | a write on a machine that writes the brain store only did not land; the payload (_meta on a tool result, error.data on a JSON-RPC error) also carries reason (no-store, no-device, root-inside-vault, partition-unavailable, store-identity or store-error) and remedy, one sentence naming the fix |
plus every member of the vocabularies the core already defines, passed through unchanged:
search errors (
SEARCH_ERROR_CODES, for exampleINDEX_MISSING,EMBEDDING_QUOTA_EXHAUSTED); the bracketed[CODE]suffix some search messages already carried stays in the text;response-shape and semantic response violations (
SHAPE_VIOLATION_CODES,SEMANTIC_VIOLATION_CODES);the frozen-vault refusal
vault_frozen, the store-migration refusalmigration_in_progress, the write-binding refusalwrite-binding-refused, the reach refusalcaller-supplied-reach, and the owner-scope refusalsforeign-ownerandunresolved-identity;the vault adapter's refusals:
vault_required(a note feature called on a machine with no vault) andvault_binding_mismatch(a note write to a vault that is not the one bound to this machine's brain store; the message names no other vault's path);the permissions-document write refusals (since 1.79.0):
write-refused(a document rule denied the write) andforce-confirmed-requires-allow(force_confirmedskipped the dream trial window while the caller's documentwriteverdict is notallow); both resolvenext_command: "o2b brain permissions show", the same re-derive loop thepermissions-unreadabledoctor finding uses. The owner-write gate refuses withowner_write_refused(below) and names the fix in its message: name the caller's own resolved identity, or write without an explicit owner;the staged-target conflict
pending-stage-conflict(since 1.79.0): a write retried while its first run is still staged, answered with the queue entry holding the target (existing_id) and thetargetitself, so a caller applies or rejects that entry instead of retrying into the same wall;the codes of write batches (
budget_exceeded,invalid_action,invalid_target, ..., and since 1.79.0 the frontmatterowner_write_refusedguard), note creation (which carries the sameowner_write_refusedguard), note lifecycle, note revert, stub scaffolding, note title resolution, note templates, pinned context, exact state, host memory writes, the count guard (count_guard), and the sharedconfig_invalidandpreference_not_found;the
brain_expirerefusals, sent under their error class names:ExpirationValueError(the date is not one the server can compare),ExpirationTargetNotFoundError(no signal or preference has that id) andInvalidExpirationTargetError(the id has no addressable shape).
The canonical list is TOOL_ERROR_CODES in src/mcp/tool-error-codes.ts.
Casing follows the vocabulary. New tokens are lower snake_case. A
code that was already on the wire keeps its spelling: search codes stay
UPPER_SNAKE, the write-binding, reach, owner-scope and permissions-document
write refusals stay
kebab-case, and the brain_expire refusals keep their class names. Nothing was
renamed, so a client that already matched vault_frozen or
budget_exceeded keeps working.
No blank code and no guess. The server classifies a failure by its
error class, never by reading an arbitrary .code property, so an
operating-system code such as ENOENT never leaks onto the wire. A
failure the registry does not recognise is reported as internal_error
and the server writes one stderr line naming the error class (never its
message):
warning: unclassified tool error mapped to internal_error: <error name>Codes are fixed tokens; none is ever built from a path, a note name or a query.
The Hermes bridge surfaces the JSON-RPC channel's code as BridgeError.code
(the string error.data.code, or None for a transport failure and for a
response whose code is missing or not a string), so a Hermes caller
branches on the token without parsing the rendered message; str(err) is
unchanged.
Tool Highlights
The full server currently advertises 121 tools; the 18 deprecated predecessor
names were removed in 1.0.0 and now answer a precise INVALID_PARAMS tombstone
(see "Consolidated views and deprecated aliases" below). The table highlights
the operator-facing core,
schema, agent-source, health, and recovery tools; the full surface
also includes Brain writer, review, query, temporal, link-graph, and search
tools. In Claude Code, the full schema can push MCP definitions beyond 10% of
the context window, causing MCPSearch tool-search deferral; use the writer
split below for the always-loaded writer subset, or the runtime capability
flags for a narrower per-process full server.
| Tool | Purpose | Required arguments |
|---|---|---|
second_brain_capabilities | Report the tools available to this MCP process and the withheld-tool reasons after runtime capability filtering. | — |
second_brain_status | Report config and vault status, with secrets redacted, and where the brain store is (the store block, see below). | — |
second_brain_query | List vault pages with an optional case-insensitive title substring. | — |
second_brain_wiring | Report what this install is wired into. view=projects lists every registered project link with its pointer state and whether the vault it names still exists; view=hosts verifies every registered install target through the same verify() call o2b install --check makes. See "Installation wiring" below for the path policy and the probe cost. | view |
vault_health | Run vault, config, and plugin manifest health checks. Since v1.50.0 the response also carries state_surfaces: the same inventory o2b state status renders (every declared in-vault state surface with its resolved path, tier, reachability verdict and the configuration layer that placed it), as a sibling FIELD rather than a contribution to ok — an absent surface is normal on a young vault and an unchecked one carries its own reason. An unreadable machine config resolves to no overrides here rather than failing the report; the checks array already names that fault. | — |
brain_health | Run semantic Brain Health checks and return the health verdict/domains. | — |
brain_mcp_landscape | List the MCP servers configured across the vault: name, source config file, packages, and required env-var names. Env values never read. | — |
brain_codegraph_report | Read-only codegraph partner report: in-scope code project, index state (no_project/absent/not_indexed/indexed with counts/error), and structural Cargo.toml workspace members. When indexed, attaches a non-blocking index.health graph-health gate (empty-graph, collapsed-edges, dangling-references, self-loops, cache-root-mismatch) surfaced before labeling/import/recall trust the graph. Never installs, extracts, or mutates; non-Rust projects report cargo_workspace: null with a reason. | — |
brain_agent_query | Read-only source-agent retrieval over Brain provenance. Filters by agents, topic, free-text query, contribution kind, and limit. | — |
brain_agent_diff | Read-only comparison between source agents using browse/search/diff/map modes over the same provenance foundation. | — |
brain_writes | Read-only listing of recorded note writes (create / update / append / revert), newest first, with the agent, the device the log shard names, the target path and the content digest on each side. Filters by agent, device, path, op and time window. action: plan_revert returns the sealed plan for undoing a selection of those writes - one entry per target with its action, refusal reason and both digests - and never applies it; applying is CLI-only. | — |
brain_audit | Read-only per-preference mutation trail (create / promote / update / retire / merge) with agent, reason, revision + content-hash before/after. | pref_id |
brain_attestations | Read-only attestation evidence (since v1.97.0) — the brain_secrets split applied to signatures: list returns the vault's Ed25519-signed store attestations newest first (id, created-at, signing device, store digest, record path) and verify re-measures the store against the newest one (per-shard intact/appended/diverged/missing, the signature, the store-digest comparison). Report-only evidence about store state; it carries no note content, so it answers identically at every reach. Key generation and signing are operator CLI verbs (o2b brain attest keygen | sign) with no tool surface — an agent that could mint a signing key would attest nothing an operator vouches for. | operation |
brain_skill_rewrite_status | Read-only rewrite-lane status (agent-presence, t_635e3e72): one skill's frozen benchmark receipt and whether its task set still verifies, the completed gate rounds' verdicts and rates, and the slug of any staged rewrite awaiting a human accept. Ids and rates only — no task text, no transcript text — and MCP never accepts or scores regardless: staging and grading stay CLI-only (o2b brain skill-proposals rewrite-plan | rewrite-score). An unknown skill answers as a named empty status, not an error. | skill_name |
brain_brief | Brain summary for any window: view: morning | daily | weekly | monthly | operator | digest | today. view=today embeds the operator dashboard: obligations sorted due-today and overdue first, one-off pledges, open loops, merged recent activity, totals. view=digest also snapshots its report under Brain/reports/ for the run-over-run delta when report snapshots are enabled (skipped below local reach and under an enforced owner scope). | view |
brain_analytics | Brain analytics for any lens: view: timeline | attention_flows | belief_evolution | concept_synthesis | dedup. view=attention_flows seeds its default flow recipe when the vault lacks one; view=dedup summarises the persisted exact-hash ingest dedup records into a trend plus a per-source re-ingest ranking; every count is an exact sha-256 drop, never a semantic figure (the semantic detectors nominate merge candidates and never drop). | view |
brain_search | Vault search with optional structured query lanes, explicit focus hints, time ranges, evidence-pack diagnostics, a selectable recall profile (fast | balanced | thorough), an FTS match_mode (all | any), a pushed kind filter (page | section) plus SQL pushdown for the promoted properties keys (since v1.80.0), and since v1.82.0 an opt-in iterative recall loop (iterative, iterative_token, iterative_payload) whose surviving coverage gaps pause with a plan the caller answers. Write-classified: it persists activation access events by default and the opt-in reinforce, recall-telemetry and query-demand ledgers under Brain/search/ - ranking state later searches read back. | query |
brain_changes | Change probe over the brain store (phase 1 trial, active only when a store is in use): returns the record ids added since an optional watermark (since; absent starts from the first record), grouped by partition, stream, scope and topic, plus the watermark to pass next (truncated: true when limit cut the page, reset: true when the watermark came from a replaced index, warnings when a catch-up failed), so an agent or hook can see what other agents saved without running a search. Read-only; local reach only. | none (since, limit optional) |
brain_settings | Shared brain settings, the ones every machine agrees on, named <block>.<field> (dream.candidate_threshold, decision_model.enabled, guardrails.derived_fact_synthesis) or by a scalar key (primary_agent): action: get | list | set | unset. view.language (en, ru, or a regional tag of one) names the language of the human view; it is a store record on every machine, switched or not. A set value is a scalar or a list of scalars and goes through the same validator the settings file goes through. Once the machine is switched to the brain store, a write is a setting-set or setting-unset record with agent authority; a value the operator set (o2b brain settings set, human authority) is refused with human_value_in_force, never written and outvoted, and while a shared settings shard cannot be read (or is gone since this machine read it) an agent's write is refused with settings_unreadable. Before the switch it edits the vault's settings file, where a value that differs from the shipped default counts as the operator's and is refused the same way. A setting that holds a safety or privacy boundary (every integrity gate, notes.read_paths, vault.ignore_paths and vault.include_paths, write_binding.path_prefixes, the sessions capture patterns, the promotion thresholds, the instruction-file ceiling and the trust and owner-scope toggles of guardrails, the decision-model opt-out decision_model.enabled, the permissions document permissions.document) is the operator's alone: an agent's write is refused with operator_only_setting in both modes, whether or not the operator set it. A setting of one machine (snapshots, storage, embeddings, decision-model endpoints) is refused with device_setting and the machine-config key to set instead. Works on a server with no vault. | action (name for get/set/unset, value for set) |
brain_recall_feedback | Record explicit up/down recall feedback for one search result; feeds the deterministic learned-weight fold. | query, result_path, verdict |
brain_recall_gate | Read-only classifier for whether an automatic recall attempt should run; returns retrieve plus a stable reason. When the caller passes scores AND match_quality (the idf_weighted_coverage a search outcome reports - the two stand or fall together, and an incomplete pair is refused), also attaches an adequacy verdict (sufficient | weak | insufficient), a recommended action (proceed | re_recall | abstain), and an optional escalate flag. The LEVEL is decided by match_quality alone; the scores decide only the usable-result count. Thresholds via recall_adequacy_sufficient / recall_adequacy_weak / recall_adequacy_min_results. Since v1.82.0 a measured call also returns the shared graded surfacing key (key, tier) beside the binary decision. | prompt |
brain_context_pack | Budgeted context slice; pass lanes: true to return directives, constraints, and consider lanes. Filtered items include safety.reasons. Each item carries a structural epistemic status (observed | derived | hypothesis | plan | unknown) plus evidence_refs derived from existing graph metadata; fields are absent when the status is unknown. Takes the same adequacy pair as brain_recall_gate under its own spelling, recall_scores AND match_quality: both or neither, an incomplete pair is refused, and the schema states it as dependentRequired keyed on this tool's own scores name. Since v1.91.0 an opt-in entity_facts_context_pack config key (default off, env OPEN_SECOND_BRAIN_ENTITY_FACTS_CONTEXT_PACK) adds a conditional entity_facts section: the canonical-scope entities anchored from the query, at most three, each listing only ids the pack already kept whose bodies anchor it; the key is absent when off, so the default pack stays byte-identical. Since v1.95.0 an opt-in tail_trim_context_pack config key (default off, env OPEN_SECOND_BRAIN_TAIL_TRIM_CONTEXT_PACK) adds a score-aware tail trim whenever the call carries a query: the top three delivered items are immune, an item whose query-term overlap nearly matches the top delivered score is kept, the rest drop whole with a tail-trim skip reason, and the freed budget is offered back to over-budget candidates in priority order; the response carries a conditional tail_trim block (dropped, kept_by_query_terms, rescued, tokens_saved) only when the trim ran, and with the key off the pack is byte-identical. | max_tokens |
brain_context_receipts | List or show opt-in prompt context receipt continuity records with budgets, hashes, source refs, safety/redaction metadata, and item IDs. Since v1.95.0 show joins the receipt to its session's reply_presence continuity record by exact session_id: the per-note verdicts the answer-quote-check Stop hook folded for the session's latest checked turn (verified / strongest miss / absent per injected note, the reply-span count, the answer-text hash), or the named unjoined outcome when the receipt's session wrote no presence record or names none - a missing join is disclosed, never inferred from a time window. The join runs at the caller's reach: note rows for paths the reach view cannot read are withheld and counted (withheld). Since v1.97.0 the operation enum grows verify: re-hash one receipt against the vault (per-item match/changed/unavailable; local reach only - below it a typed {operation, measured: false, reason: reach | receipt_withheld, detail} refusal; private or redacted receipts refused symmetrically with the session fold). | operation |
brain_recall_telemetry | List or summarise opt-in recall telemetry records for search, context-pack, and pre-compress calls; since v1.80.0 the dead_weight operation reports in-scope pages no recorded recall ever surfaced. | operation |
brain_route_metrics | List or summarise opt-in route-level MCP tool latency (mcp_route_latency records); summary rolls each tool up into count, error count, and min/avg/max + p50/p95/p99 latency, slowest-first, plus a per-stage count/avg/p95 for a route whose records carry write stages. Emitted only when mcp_route_metrics_enabled is on; payload-safe (tool, scope, status, duration, arg key names, stage names). Read-only. | operation |
brain_retrieval_plan | Shadow-only retrieval advisor for one question: composes query intent/weights, the summary-surface route, the context-pack density allocation, the token-impact ledger, and observed route p95 latency into a strategy, token-budget allocation, graph-expansion advice, reliability, and a marginal-value stop. Optional token_budget. Read-only; exposes no mutating parameters and changes no ranking. | question |
brain_token_impact | Durable value-of-memory ledger: record posts a context pack's tokenizer-exact prompt-token delta (baseline − packed, method exact/fallback) plus an optional modeled inference-avoidance estimate; outcome posts first-pass/repair/retry to calibrate the model; summary keeps EXACT prompt-token savings strictly separate from the MODELED (outcome-calibrated) figure; list reads raw samples. Writes gated on token_impact_ledger_enabled (default off); payload-safe (counts + opaque pack id only). Reads ignore the gate. | operation |
brain_context_pack_outcome | Agent-operable outcome loop over the context-pack quality ledger: post records one compact outcome row for a carried context-pack quality-sample id — first-pass/repair/retry counters plus three STRICTLY SEPARATE token signals (exact_prompt_token_savings, modeled_inference_avoidance, observed_provider_tokens) — and composes the token-impact ledger by posting a matching first-pass/repair/retry calibration outcome; list/summary read the rows keeping the signals separate. Writes gated on context_pack_outcome_enabled (default off); payload-safe (counters + opaque sample id only), a field the caller omits is never invented. An optional agent_id names the ACTING agent and is recorded on every row the post lands; it is a tool ARGUMENT rather than the server's own config identity, so a config the server cannot read can never fail a telemetry post, and omitting it records no actor rather than a guessed one. Reads ignore the gate. | operation |
brain_knowledge_gaps | Aggregate the persisted cross-query demand log into recurring queries the vault answers poorly, ranked by frequency × (1 − IDF-weighted coverage). Read-only; the log is written only by opt-in recall telemetry. | — |
brain_generation_reports | Inbound, opt-in LLM generation tracing: record posts a generation's usage for a handoff (gated, default off; stores prompt hash + token counts only); list/summary read records and join them to memory paths. Kernel never calls an LLM. | action |
brain_obligation | Recurring obligations under Brain/obligations/ with a deterministic cadence-driven next-due date: add, done (advances next_due by one cadence interval), list (optionally overdue-only), show, remove. Cadences: daily/weekly/biweekly/monthly/quarterly/yearly/every-<N>-days. | operation |
brain_pledge | One-off pledges under Brain/pledges/<slug>.md: add (title, a due calendar date, the cited source page, and evidence quotes each verified to occur in that source at write time - an uncited pledge, a missing source page or an unverifiable quote refuses the write by name), fulfill (stamps the fulfillment date; a second stamp refuses), list (due-sorted open pledges with an overdue flag and whole days until due; overdue restricts to the overdue ones), show, remove (archives the page under Brain/pledges/archive/ with a collision suffix). Counterparties ride the promised_to frontmatter key, a relation in the shared vocabulary, so the existing frontmatter-relations ingest projects the typed edges. Answers at the caller's reach: below local reach a withheld page answers show as absent, drops from list, and fulfill/remove refuse with the same no pledge error an absent slug gets; a withheld remove answer carries no archive path. | operation |
brain_intake_pledges | Extraction intake for one-off pledges: the agent posts typed extractions (pledges, each with a title, a due date, evidence quotes and optional text and counterparty names) citing one source page. The kernel validates EVERY item - source readable at the caller's reach, every quote contained in the source, valid dates - before writing anything, so a batch with one invalid item writes no page. Commit is idempotent: a repeat with the same payload reports its slug under duplicates; the same identity with a changed payload is a named mismatch refusal. Counterparty names are anchored server-side against canonical-scope entities and never taken from the caller; names that anchor nowhere are reported under unanchored_counterparties and stored nowhere, and re-posting the same payload after the entity registers adds the edge (reanchored). The batch is capped by name: at most 100 pledges per intake, at most 32 counterparty names per pledge, and no counterparty name longer than 200 characters - a batch over a cap refuses whole. The kernel never calls a model. | source, pledges |
brain_agenda | Stateless agenda synthesis over caller-provided calendar events (the host fetches them; the Brain never calls a calendar API): overlap conflicts, free focus blocks (optionally clipped to a workday window), and events organised outside the operator's own email domain(s). No vault writes. | events |
brain_context_presets | Show, suggest, or diff read-only context budget presets (tight-context, long-context) without writing config. | operation |
brain_pre_compact_extract | Extract decision/commitment/outcome/rule/open-question records from bounded text into continuity storage. | session_id, turn_start, turn_end, text |
brain_hygiene | Memory hygiene: scan findings (conflicts, dedup, freshness, usefulness; since v1.64.0 slug-collisions is default-on — same-stem allocation residue such as topic.md beside topic-2.md — and tags is opt-in, auditing inline body tags only, never frontmatter tags arrays; since v1.67.0 capture-scope is default-on and warns when active knowledge rests only on url-only sources; the default is the sweep of every registered detector except the opt-in ones), apply selected ids, refresh stale pages; since v1.81.0 archive_get resolves an ingest summary's source_archive_hash to its archived bytes under Brain/.archives/ (local reach only, and the capture-scope scan also names a cited source whose archive entry is missing or mismatched). Resolver command comes from _brain.yaml only. When hygiene_digest_enabled is on in the machine config, the same default-sweep findings are also pushed at turn end as a one-line digest (Stop hook, once per change), so this pull surface doubles as a push channel. With the optional dedup decision-model use in enforce, scan dedup findings carry an advisory decision_model verdict; apply never reads it. | mode |
brain_storage | Read-only storage report (since v1.85.0): a per-surface byte census measured or refused by name, the growth forecast against the configured storage.cap_bytes with its tier (ok/warn/critical, or unbounded/unmeasurable/no_forecast) and the percent and days figures it rests on, and the storage_usage sample this invocation records. Below local reach the census is not taken: the answer carries measured: false and the reason, no figures. A report, never a gate: a critical tier still answers successfully. | — |
brain_receipts | Receipt compaction over the continuity and metrics ledgers (since v1.85.0): scan (the default) inventories both ledgers read-only per shard - records, foldable, protected, unparseable, aggregates, bytes; compact folds expired records into per-kind, per-month aggregates inside family:name shards named from a scan, preview-bound without apply, local reach only. expect asserts the fold count and aborts without writing on a mismatch; strict refuses a guardless confirmed compact. Never touches decision-change receipts, the source archive, or context compaction. | — (shards for compact) |
brain_anticipatory_context | Turn-specific context bundle kept warm by lifecycle hooks, keyed by the session's lineage root; reports cache_state warm / stale / miss. | session_id |
brain_session_grep | Search imported session recall raw turns and deterministic summary nodes; since v1.82.0 operation: "coverage" runs the coverage-first thread selection under a char_budget instead of a grep. | query |
brain_session_describe | Describe raw-turn counts and summary depths for one imported session recall DAG; topics: true adds the adaptive topic-budget report the coverage operation fans out under (since v1.82.0). | session_id |
brain_session_expand | Expand a raw or summary session recall node to immediate sources and paginated raw turn content. | id |
brain_sources | Read-only dashboard of signals grouped by (agent, source_type) with active/processed and distinct-topic counts. | — |
brain_create_note | Write an actual vault note file (path + frontmatter + content) atomically inside the vault. Distinct from brain_note (log append); refuses traversal, the Brain root, excluded paths, any destination outside the declared write binding, and — by default — clobbering. Opt-in: if_exists: "skip" returns outcome: "skipped" instead of creating, strict validates the document before writing and reports coded violations, and template + template_variables render the body through a closed two-construct grammar ({{name}} substitution; {{#name}}…{{/name}} presence sections and list iteration with {{.}}). Unknown placeholders are left intact. | path |
brain_note_lifecycle | Note-FILE lifecycle, dispatched on action: rename (new filename, same directory), move (new directory, same filename), archive (displaces the note under Archive/, mirroring its path), delete (removes it). Both the source and the destination go through the same nine-step path envelope brain_create_note uses, so a destination cannot reach where a create could not. Dry-run unless apply: true; delete additionally requires confirm: true and honours expect / strict over the inbound-reference count. A relocation rewrites inbound [[links]] vault-wide to the minimal spelling that still resolves to the destination - a bare basename only when exactly one note carries it, and a bare name the rename turns from unique to ambiguous repaired to the unambiguous spelling of its pre-rename target - and the result names the rewritten spellings, what it withheld, how stale the search index still is (references.index) and whether the write claimed the detached refresh (references.refresh). A delete rewrites nothing, reports the references it strands, and returns a recoverability verdict saying the archive it took does not cover a note living outside Brain/. Since v1.95.0 two folder actions join the enum: move-folder relocates a whole folder under a destination parent and rename-folder renames it within its parent, split for the same reason rename and move are (a caller that meant one and spells the other is refused wrong_action); every destination is checked free before the first rename so an occupied destination refuses with nothing moved, the move set's inbound path-spelled links are rewritten in one pass over the successfully moved set, a mid-set failure is named per file in move_failures (and any residue in the source folder in left_behind, absent when empty) with no rollback, and dry-run stays the default. | action, path |
brain_scaffold_stub | Unresolved wikilink targets. action: "list" reads them from the search index and REFUSES - state plus a next_command - when that index is missing, unreadable or only partially resolved, rather than reporting zero: an empty list from an index nobody finished resolving is a clean bill of health for a vault nobody measured. action: "write" materialises a stub for one target through the same createNote envelope and if_exists semantics as brain_create_note - its title is the target the link spelled and its body links back to the sources documents, each of which must be an existing Markdown note inside the vault or the call is refused with unknown_source - so nothing the stub cites is invented, though whether a cited document really carries the reference is action: "list"'s claim, read from the index, not this write's. A target that already resolves, or that names more than one note, is refused with its candidates. Dry-run unless apply: true. | action |
brain_file_context | Given a file path, surface prior vault work that mentions it (decisions, bug notes, refactor history) by querying the index with path-derived terms. Size gate skips trivial files. Read-only; no LLM. | file_path |
brain_session_summary | Session-scoped structured digest over request/decisions/learnings/next_steps: write stores agent-extracted categories, get returns a session's latest digest, list returns all. Append-only, deduped; an all-empty digest is rejected. | operation |
brain_idea_lineage | Read-only provenance tracer: reconstruct how a derived artifact was reached as an observation -> synthesis -> conclusion graph. A ctn_ id walks the sourceRefs graph; a pref-/ret- id adapts belief-evolution. Cycle-guarded, depth-bounded; unknown id errors. | id |
brain_note_history | Decompose a note's git history into recallable episodic phases split on a deterministic commit-time gap (default 72h, language-agnostic). Each phase carries subjects/dates/authors. Missing repo → available: false; no commits → zero phases. Read-only. | path |
schema_inspect | Read-only schema inspection for any view: view: graph | lint | stats | orphans | explain_type | active_pack | packs. | view (token for explain_type) |
schema_apply_mutations | Apply audited, locked schema mutations to the active schema pack: Brain/_brain.yaml before the machine is switched to the brain store, the shared brain.schema.* settings records after (audited to the store, nothing under Brain/). dry_run: true previews instead: the pack that would result plus its leaf-level diff, no config write, no audit record. The preview runs the same pack validator the apply runs, so a batch that validator rejects raises identically in both. It does NOT cover the two checks that exist only because the apply writes: the vault-identity write guard, and the atomic writer's re-parse of the rendered YAML. A batch that renders to unparseable YAML therefore previews clean and fails on apply. | mutations (dry_run to preview) |
brain_watchdog | Probe Brain config, required dirs, and search-index health; optionally apply safe directory remediation. | — |
brain_switch_vault | Activate a named vault profile; the change takes effect on the next server launch. | name |
Consolidated views and deprecated aliases
brain_brief, brain_analytics, and schema_inspect replaced three
overlapping tool families in v0.34.0; per-view output is identical to the
predecessor tools because dispatch goes to the same handlers. The 18
predecessor names were removed in 1.0.0: calling one answers a precise
INVALID_PARAMS tombstone naming the replacement tool and view (for
example brain_digest was removed in 1.0.0; call brain_brief with view="digest"), so a stale client learns the migration from the error
itself. Per-view parameters keep their old names (for example
brain_brief with view: "daily" accepts the same date argument
brain_daily_brief did, and brain_analytics with
view: "attention_flows" defaults operation to list). The full
alias-to-replacement table lives in docs/updating.md.
second_brain_query accepts pattern (string) and limit (1–500, default 50).
vault_health accepts repo (string) for plugin manifest validation.
brain_agent_query accepts agents (string array), topic, query, kind
(signal, preference, log), and limit (1-500, default 50).
brain_agent_diff accepts the same filters plus mode (browse, search,
diff, map). Omitting agents means all known source agents. Both accept
kind: note, which lists the notes an agent wrote: the vault provider maps
every note-write log event to a contribution whose path is the note and
whose title is the operation.
brain_writes accepts action (list, default; plan_revert), agent,
device, path, op (create,
update, append, revert), since / until (a bare YYYY-MM-DD or an
ISO-8601 UTC timestamp; a bare until date covers the whole day it names), and
limit (1-500, default 50). It answers with total_matched beside returned,
so a capped list is legible as one, and each row carries write_id, both
content digests, both byte counts and the device the log shard names - the
empty string being the legacy un-sharded log rather than an unknown machine.
action: plan_revert takes the same selector (agent, device, path,
since, until) and returns the sealed plan for undoing the writes it names:
one entry per target with action (restore, delete, refuse), reason
when refused (drift, interleaved, image-missing, unrecorded,
already-reverted), the selected writes oldest first, and hash_now /
hash_to; plus the digest that seals it and next_command. A selector
naming none of agent, device or path is refused with INVALID_PARAMS
carrying unbounded_selector - a time window alone is a vault rollback. There
is no apply here: applying moves bytes on targets the caller may never have
written, so o2b brain writes revert --apply <digest> is where a human types
the digest.
brain_search accepts query_document with line-oriented intent:, lex:,
vec:, and hyde: lanes; focus_query / focus_path_prefix to steer a
single call; since / until time ranges (ISO date/datetime, today,
yesterday, last week, last month, or <n>h/<n>d/<n>w shorthand,
filtered on event time - frontmatter validity first, then the
body-derived anchor, with document mtime only as the last rung);
include_superseded: true to keep superseded
predecessors undemoted (history mode); and evidence_pack: true to return
significant/matched/missing terms, abstention text, terminal-state downrank
reasons, per-result why_retrieved, IDF-weighted coverage with rare-term
classification, per-token union_records for uncovered terms, and a
completeness verdict whose uncovered_but_present_in_corpus list is the
false-absence guard. It can also emit opt-in recall telemetry with
telemetry: true. The whole query language is published as a versioned
JSON Schema artifact beside the repo
(schemas/brain/search-query.schema.json, $id brain.search_query.v1),
derived from the same table that serves this inputSchema:
o2b search schema export regenerates it and o2b search schema check
exits 2 when the committed file has drifted from the derivation.
Since v1.44.0 explain: true adds two top-level receipts alongside the
per-result score_breakdown: retrieval_decision_trace (evaluated /
surfaced / excluded counts plus every excluded candidate as a compact
reference with its structural reasons) and memory_trust_assessment (the
same exclusion set as a reason histogram). Both are by-products of the
retrieval trust gate, which runs in one of three modes
(search_trust_gate_mode: off | shadow | enforce; the bare
search_trust_gate_enabled alone maps to enforce, which excludes, and
shadow measures without excluding): with the gate off the
response carries retrieval_trace_unavailable naming the switches that
produce them (search_trust_gate_enabled, or
search_trust_gate_mode: shadow) instead of going quiet. Without
explain no key appears at all. In the same release total stopped
echoing the row count: it is the ranked candidate pool the limit
sliced the returned rows from, so total > results.length means the
ranker had more candidates than it handed you. Read it as candidates ranked, not as a corpus
statistic: the pool is capped at roughly three times the requested
limit, so it moves with limit and saturates on a large vault, and
link traversal can add a document that matched no term at all.
Since v1.80.0 brain_search accepts kind: "page"|"section", pushed into
the store's SQL on both lanes (keyword: before ORDER BY/LIMIT;
semantic: inside the overfetch window). Property pushdown is
conditional-faithful: type pushes the scalar declared values as the
normalized token the index stores, pinned pushes only for true,
visibility pushes its normalized tokens, while tags and any non-scalar
shape stay post-rank filters judged against the live file. Every other key
narrows the candidate pool against the indexed document_attributes
snapshot - a provable mismatch drops out in SQL, the bounded table never
speaks where it holds no rows, and the live filter still judges every
surfaced row. A pushed filter answers AS INDEXED - the indexed snapshot,
like the content itself - so a page edited after the last index run is
judged by its indexed frontmatter until the next run. See
docs/cli-reference.md.
A deterministic summary-search router (t_7b96f242) inspects each query for
structural summary signals - a source-targeted source:<path> token, or a
kind:<t>/type:<t> token whose value is a declared artifact kind in the
vault schema pack (schema.page_types). When a query is summary-shaped it
carries surface: "summary" in the response, naming the summary-search
surface as the intended route (target a source or artifact kind rather than
running a generic hybrid search over raw chunks); ranking is never altered
and non-summary queries omit the field entirely, so the generic-surface
response stays byte-identical. brain_recall_feedback records one feedback event as a
JSON file under Brain/search/feedback/ and returns the refreshed learned
weights (applied to ranking only when search_learned_weights_enabled is on).
brain_context_pack accepts opt-in receipt, telemetry, cache_stable, and
dedup_repeated diagnostics; brain_pre_compress_pack accepts opt-in receipt
and telemetry. brain_context_receipts supports operation: "list"|"show";
brain_recall_telemetry supports operation: "list"|"summary"|"cost" (cost
folds write volume - feedback/apply-evidence/note plus host-bridge writes -
against reads into a write-vs-read ratio, a write_heavy flag, and a rough
weighted cost signal per period; tune with write_cost/read_cost/write_heavy_ratio).
Since v1.80.0 the same tool also answers operation: "dead_weight": pages
currently in scope that no recorded recall within the telemetry window ever
surfaced - the complement of the recorded retrieved set against the vault
walk, windowed by since/until, with the observation window, a capped
dead-page list, and the gate caveat (telemetry is opt-in and records only
surfaced top rows, so the report claims "no recorded retrieval surface",
never absolute never) in every payload.
Since v1.81.0 brain_truth state accepts an optional opaque sample_id
(echoed verbatim on the committed event) and the events report hydrates
the provenance behind it: the named context receipt's items plus the
context-pack outcome row, resolved at the caller's reach - below local
reach, receipt items whose injections came from operator rules stay
hidden, a claim whose outcome never arrived renders "receipt known,
outcome unknown", and rows without a sample_id stay byte-identical to
before. brain_ingest_source in the trusted local lane archives the
source's bytes content-addressed under Brain/.archives/ (redacted
before persist, refused past 1 MiB rather than truncated, never
TTL-pruned), stamps source_archive_hash and source_archive_bytes on
the summary page and returns source_archive_hash; delete-by-source
cleanup removes the source's orphaned archive entries best-effort, and
brain_session_summary get and list omit the session_id key of a
digest that has no session instead of answering "", matching the
no-session read rule every continuity reader applies.
Since v1.82.0 brain_search can keep querying after the single
self-correcting second pass: iterative: true opts one query into the
bounded iterative recall loop, which re-measures pool coverage each round,
emits follow-up sub-queries from the uncovered rare-term groups, and merges
what they recover into the same ranked pool, until coverage completes, a
round stops paying, the round cap or the composite deadline is reached, or
a round recovers nothing. The loop is inert unless the operator's
search_iterative_recall_enabled knob is on
(OPEN_SECOND_BRAIN_SEARCH_ITERATIVE; the round cap
search_iterative_recall_max_rounds /
OPEN_SECOND_BRAIN_SEARCH_ITERATIVE_MAX_ROUNDS defaults to 2 and clamps
to 8), and with the knob off the response and the query-cache keys are
byte-identical - the cache suffix that separates iterative rows exists
only in the opt-in mode. When measurable gaps survive the deterministic
budget, the call pauses instead of finishing: the response carries an
iterative marker at phase: "needs-llm-step" with the decider envelope
(llm_step), the candidate pool_ids the decider selects evidence from,
the uncovered gaps, and an opaque single-use round token. The caller
re-invokes with iterative_token plus iterative_payload
({evidence: [chunkId, ...], subQueries: [...]}), which is validated in
full before any pull runs: unknown chunk ids, oversized or over-limit
sub-queries, sub-queries carrying no significant terms, a replayed token
(token_used) and a mismatched one (token_mismatch) are typed refusals
surfaced as caller errors, and a fully-formed commit arriving while the
loop is disabled is refused by name instead of reading as a plain answer.
The round-trip is a single-vault loop: any iterative field combined with
global: true is refused by name at the argument layer, so no iterative
field ever reaches the federated legs. A committed call, and a plain call
whose loop ran at least one gap-driven round, answer with the same marker
at phase: "final", carrying the per-round records (the terms, the group
count, what was added, the elapsed time, and whether the deterministic
floor or the decider authored the sub-queries) and the stop_reason
(coverage_complete, no_sub_query, no_progress, round_cap,
marginal_value, deadline, decider_round); a call the mode did
nothing for carries no marker at all, so the serialized outcome stays
byte-identical whenever the mode did nothing.
Since v1.82.0 brain_session_grep also answers operation: "coverage":
the coverage-first thread selection over the same lineage-scoped record
set. Facets come from the thread itself - distinct mined entity keys and
depth-1 summary clusters - and fan out under the adaptive topic budget
clamp(distinct facets, floor 12, cap 16); the selected turns return in
chronological order under the required char_budget beside recomputable
diagnostics that report the realized topic count honestly. entity_key
optionally seeds the fan-out from the session-entity-mention rows an
import already mined, and an entity no record mentions is refused by name,
never answered with an empty result. The operations keep their arguments
to themselves: char_budget under match, and query, since,
before, limit, snippet_chars, include_raw and raw_budget_chars
under coverage, are each refused as not applicable. A budget that cannot
hold the floor-weighted selection is a typed infeasibility mapped to
INVALID_PARAMS, naming the requested topics, the kept ones and the
required characters - a retry bound, never a silent truncation.
brain_session_describe accepts topics: true, which adds the
topic_report the coverage operation fans out under (the detected facets,
the budget and the realized topics); without it the response is
byte-identical.
brain_context_presets supports operation: "show"|"suggest"|"diff"; and
brain_generation_reports supports action: "record"|"list"|"summary" -
record is gated (default off) by a per-call enable flag or the
generation_trace_enabled config and persists only prompt_hash plus token
counts, never the prompt.
brain_pre_compact_extract writes idempotent typed continuity records after
deterministic media/base64 sanitization. brain_session_grep,
brain_session_describe, and brain_session_expand inspect the opt-in session
recall DAG populated by CLI import-session --recall or the core API.
brain_session_summary accepts operation: "write"|"get"|"list" (write takes
session_id plus any of request, decisions, learnings, next_steps,
source_turn_ids, host, project_scope). project_scope is the same
project axis brain_search filters on, normalized to the same [a-z0-9-] slug:
on write it files the digest under that project and folds it into the dedupe
key, so the same content under two projects is two digests; on list it returns
only that project's digests. Omitted, the digest and its dedupe key are
byte-identical to the pre-project shape. A value with no alphanumeric is an
INVALID_PARAMS error naming the argument, never a silent drop to unscoped.
This is unrelated to the o2b brain project verb, which links a code directory
to its owning vault at the configuration level. brain_idea_lineage accepts id and optional
max_depth. brain_note_history accepts path and optional gap_hours /
max_count.
brain_recall_gate accepts optional previous_prompt and
explicit; explicit: true always returns retrieve: true. Since
v1.69.0 it also accepts an optional turn_id (a string of at most 512
characters), recorded on the gate_telemetry record when that telemetry
is on. Since v1.82.0 a call that carries the scores and match_quality
pair also returns the shared graded surfacing key beside the binary
decision: key is the measured match quality the admission floor reads
(null exactly where nothing was measurable, a structurally suppressed
prompt) and tier is the closed admission vocabulary (explicit,
suppressed, admitted, below_floor, unmeasurable_quality); without
a measurement both fields are absent and the response is byte-identical.
The recall-inject hook's floor branches consume the same calculator, so a
tier boundary answers with the same reason on every channel.
Optional decision-model answerable signal (see
decision-models/answerable.md): with rerank
kind decision-model and the answerable use in shadow or enforce,
brain_search adds decision_model: { answerable: { probability, model, calibrated } }; the field is absent when the use is off, for any other kind
and when the request degraded. brain_recall_gate and brain_context_pack
accept that probability as decision_answerable (a number in [0,1]), only
together with the scores and match_quality pair (dependentRequired;
alone it is INVALID_PARAMS). With the use on, the adequacy verdict gains
decision_answerable: { probability, disagrees }, where disagrees is true
for sufficient below 0.3 or insufficient above 0.8; level and action
never change. The context-pack receipt stores the field next to its verdict.
With the use off the argument is ignored and the response carries one
warnings entry saying so.
Date format note. Brain tools use ISO 8601
YYYY-MM-DDthroughout; theBrain/log/<date>.mdsubdirectory layout shares that convention.
All tool results contain both an unstructured content text block (a JSON
serialization of the structured payload) and a structuredContent object so
clients that prefer typed results can use it directly.
Paging externalized session payloads
Session recall import moves oversized turn content into Brain/.payloads/
and leaves [payload: osb-payload://<sha256> chars=N] in the recalled turn
(see o2b brain payload in docs/cli-reference.md). brain_session_expand
pages the exact stored content when it is called with payload instead of
id: {payload: "osb-payload://<sha256>", payload_chars?: n, cursor?: "<offset>"}
returns {payload: {ref, offset, limit, total_chars, content}, next_cursor}
(4000 chars per page by default). payload cannot be combined with id or
raw_limit, and a malformed ref is INVALID_PARAMS before any path is built.
This rides the existing tool rather than adding one, so the tool count and
every profile are unchanged.
Reach follows the session data the payload came from. A local caller reads any
stored payload. A remote caller reads one only when a non-private session turn
references it, directly or through another remotely readable payload; a
payload referenced only by a private turn, a page (the Brain log included) or
nothing gets the same refusal. Pages and summary nodes keep a payload live for
o2b brain payload gc but grant no remote read, because a page can be written
by tools that a session turn is not.
At remote reach brain_session_grep, brain_session_describe,
brain_session_expand and brain_idea_lineage also withhold continuity rows
flagged private (a <private> region was stripped from them), and the
session recall tools withhold the summary nodes built from them. A withheld
row is answered exactly like one that does not exist. Local callers see every
row.
Installation wiring (since v1.56.0)
second_brain_wiring answers what this install is wired into. Both answers
existed before it and neither was reachable from this surface: the linked
project registry was readable only through o2b brain project status, and
the per-adapter verify aggregate only through o2b install --check. Nothing
here computes health of its own - each view is a seam onto the reader that
already produced the answer.
view is required and there is no aggregate member, so a projects read never
pays for a host probe. An absent or unknown view is refused with -32602
naming the accepted members, and view=hosts without a resolved vault is
refused with the same code and the sentence o2b install --check gives -
verify reads a per-vault manifest, so an unset vault would report every
runtime absent.
Every payload carries vault_path and view at the top level, whichever view
answered.
view=projects returns one entry per registered project link:
| Field | Meaning |
|---|---|
project_ref | The project directory, under the path policy below. |
vault_ref | The vault it points at, under the same policy. |
pointer | ok, missing, malformed, or mismatch - the state of that project's .o2b-vault.json against the registry. |
vault_exists | Whether the vault directory is still present. |
Beside projects the payload carries total (links found), returned
(entries actually carried) and truncated. The list is capped at 25: nothing
caps projects.json - every o2b brain project link appends to it - and this
response lands in model context, so the bound is in the payload rather than in
an assumption about how many projects an operator links. The view=hosts list
is not capped; a registry this build closes bounds it already. The CLI verb
stays uncapped too - it renders to a terminal.
A registry file damaged by hand reads as no links at all - not as the entries
it can still parse - and the call succeeds. That is the tolerance the CLI
reader already has (listLinkedProjects parses the registry as a whole), and
this view answers what o2b brain project status answers rather than refusing
where the verb tolerates. A server started without a config path reads the
machine default, as view=hosts does.
view=hosts returns one entry per registered install target, carrying
target, the VerifyStatus member (ok, drift, not-installed,
mcp-unreachable), the details the adapter observed, and fix_hint. Those
last two are the adapter's own sentences and name the host config files it
looked at, so the home prefix in them is folded to ~ under the same
expose_host_paths flag that governs the reference fields below. It is
the same verify() call over the same registry and the same InstallEnv that
o2b install --check makes, so connector health has one implementation rather
than two that can drift.
Two things this view states rather than hides. It may ask host CLIs: two of the
registered targets declare a host probe (codex mcp list, copilot mcp list),
so a worst-case call adds two subprocess round trips, each bounded by the same
ten-second deadline --check uses, and a probe that could not run renders its
named reason instead of an assumed ok. And an unresolved vault is refused by
name with the sentence o2b install --check gives, because verify() reads
the per-vault sidecar manifest and an unset vault would otherwise report every
target as not-installed off a path that does not exist.
Paths. The project registry is keyed on absolute host paths, and an MCP
response lands in model context, so project_ref and vault_ref follow the
same expose_host_paths contract vault_path obeys: an opaque, stable
vault://<hex> reference by default, the raw path when the operator sets the
flag. A config that cannot be read renders { "error": "..." } in the field
rather than degrading to the path the reference exists to hide. That reason is
path-safe: it does not name the config file either, because the file lives
under the operator's home and this is the field that exists to keep host paths
out of model context. second_brain_status and vault_health name it, from
fields whose contract is to name it.
The brain store in second_brain_status. The tool's fields keep their
order (config_path, config_exists, config_keys, config, vault_path,
vault_exists, then the vault, brain and search blocks when present),
and a store object always comes last. It answers where the brain store is
and how far this machine has got with it, so no agent needs a store path by
heart:
| Field | Meaning |
|---|---|
root | { path, origin, exists }: the store root and the setting that put it there (env, config or default). |
initialized | Whether a partition carries the store identity, so the store exists. |
store_id | The store id, or null before o2b store init. |
partitions | One { name, path, held, exists } per partition (shared, private). |
partitions_held | The names of the partitions this machine holds. |
device_state_dir | { scope, path, origin, exists }: the device-local state directory of this store (scope: "store"), or the directory that will hold it before the store exists (scope: "all-stores"). |
device_id | The device id this machine writes its shards under, or null when none is set. |
dual_write_enabled | Whether the store_dual_write trial is on. |
index | { exists, records_indexed, shards_read, last_catch_up_at }: how far the vault's search index has caught up with the store; last_catch_up_at is the last catch-up that read new records. null when the vault is not there. |
migration | { available, legacy_tree_present, store_exists, switched, next_commands, error? }: a migration is available while the vault still holds the legacy Brain/ tree and the store holds no device-switched record of this device. next_commands names what to run, in order (o2b store init first when no store exists, then o2b brain migrate-store --dry-run); store_exists is null and error says why when the store cannot be read. o2b brain doctor reports the same answer. |
error | Why the store cannot be used as configured (a root inside the vault, a corrupt store.json, partitions of two stores), or null. |
The paths are reported as they resolve, like config_path and
search.index_path in the same tool. A machine config that cannot be read
turns the whole block into { "error": "..." }; the call itself still
answers. o2b store path prints the same locations on the command line, and
o2b status --json carries the same store object.
Resources
The server also exposes a resources capability for hosts that prefer
pull-style access (no tool call, no arguments). Three concrete URIs
come back from resources/list:
osb://preferences/active— body ofBrain/active.md, the auto- generated digest of confirmed + quarantined preferences plus the last three retired entries. Auto-regenerated on first read if the file does not exist yet.osb://lessons— body ofBrain/lessons.md, the auto-generated, signed and recency-scored lessons corpus that unifies preferences and dead-ends into corroboration-tiered lessons (preferred/tentative/contested/avoid). Auto-regenerated on first read if the file does not exist yet. The SessionStart / PostCompact hook injects it alongsideactive.md.osb://digest/latest— same body asbrain_briefview="digest"in its default (24h) Markdown window.osb://status— Brain operational snapshot: counts (inbox / preferences by status / retired / log_days / snapshots), lastdreamandapply-evidencetimestamps, and a sanity flag for signals awaitingdream. Same data thesecond_brain_statustool returns under itsbrainfield, rendered as markdown.
Four templated URIs come back from resources/templates/list:
osb://preference/{id}— body ofpref-{id}.md, with fallback toret-{id}.mdwhen the active copy is gone. Accepts the bare slug (my-rule) or the prefixed form (pref-my-rule/ret-my-rule).osb://topic/{slug}— synthesised markdown of every signal, the current preference (or retired), the most recent log entries, and a deterministic Strongest objection steelman against the current preference (a retired/quarantined rule, a recorded negative counter-signal, or an unconfirmed-trial caveat) for the topic.osb://log/{date}— body ofBrain/log/<date>.md(date isYYYY-MM-DD).osb://backlinks/{id}— inbound references to the given Brain artifact id, rendered as a count plus a list grouped by source kind. Same data as thebrain_backlinkstool.
resources/read accepts both shapes uniformly and returns
text/markdown content. Malformed slug/date arguments produce
INVALID_PARAMS; missing files produce a tool-level error envelope
with a not found message — same shape as brain_query's
BrainNotFoundError.
Stored-record bodies delivered at remote reach arrive inside an
untrusted_source fence naming the record's derived provenance; at local
reach every field is byte-identical to the pre-wave shape. Withheld rows
gain no fence and no count. Log and event bodies are the stated
delivery-shaping boundary and stay verbatim.
Run from the CLI
scripts/o2b mcp --vault /path/to/vault
scripts/o2b-mcp --vault /path/to/vaulto2b-mcp is a console-script alias for o2b mcp; it injects the mcp
subcommand and forwards every flag verbatim.
Optional flags:
--config PATH— override the Open Second Brain config file location.--repo PATH— repository root used for plugin manifest checks.--scope full|writer— choose the full server or the always-loaded writer subset.--writer-only— alias for--scope writer.--tool-profile full|writer|catalog|recall|minimal— a named scope-plus-window bundle (see "Tool-surface profiles" below).--host-target <runtime>— name the runtime that launched this server, so the capability report can cite that host's published tool ceiling. Install adapters write it into the registration they generate; an unrecognised value exits2naming the known ids. It also stands in for--harnesswhen that option is absent, and changes nothing else.--harness <id>— name the harness this server runs under (since v1.70.0), from a closed list:aider,claude-code,codex,copilot-cli,cursor,gemini-cli,generic,grok,hermes,kiro,openclaw,opencode,pi. It selects whichBrain/standing-rules/harness/<id>.mdfilebrain_contextrenders (see "Scoped operator rules" inhow-it-works.md); the Claude Code plugin passes--harness claude-codeand the Hermes plugin--harness hermes. An unrecognised value exits2naming the accepted ids. No tool argument names the harness.--probe— start an in-process handshake and print whether the server can advertise tools, then exit.--transport stdio|http— choose stdio (default) or Streamable HTTP.--host HOST— HTTP bind host (default127.0.0.1).--port PORT— HTTP bind port (default0, choose an available port).--api-key KEY— optional on the loopback default, REQUIRED when--hostnames a non-loopback interface; accepted asAuthorization: Bearer KEYorX-API-Key: KEYon every request. A flag is visible in every process listing on the host, so prefer the environment:OPEN_SECOND_BRAIN_MCP_API_KEYsupplies the same key (the flag wins when both are set).--json— with--probe, print a machine-readable capability report.--allow-tool NAME— expose only named tools from the static scope. Repeatable.--disable-tool NAME— withhold named tools from the static scope. Repeatable.--max-tools N— expose only the first N non-diagnostic tools from the static scope.
The stdio server logs its banner to stderr and only writes JSON-RPC frames to
stdout, so it is safe to use as a subprocess in any MCP client. HTTP refuses
to start when --host is not loopback and neither a key (--api-key or
OPEN_SECOND_BRAIN_MCP_API_KEY) nor a non-empty agent-token map is present;
with a key
configured it checks that key on every request using a generic constant-time
comparison, and returns the same 401 Unauthorized body for a
missing or wrong key. JSON responses are the default; clients that send
Accept: text/event-stream receive a single SSE message event for the same
JSON-RPC response — one event, then the connection closes, which is why a
progress token sent over HTTP is refused by name rather than honoured (see
"Progress notifications" above).
Since 1.79.0 the HTTP transport also answers per-agent credentials. o2b mcp token mint (or o2b bootstrap --token) mints a named token per agent into
the vault's hash-at-rest store (.open-second-brain/secrets/mcp-tokens.json;
only a sha-256 hash and a non-secret prefix are stored, the material is shown
exactly once). A request whose Authorization: Bearer or X-API-Key
credential matches a stored token authenticates as that token's agent for
that one request - identity threads as a request parameter, never as server
state, so concurrent callers with different tokens never observe each other's
identity. The token map is consulted before the shared key; the shared key
keeps authenticating with the process's configured agent name; and a revoked
or unknown token gets the same generic 401 body as a missing credential, so
no oracle distinguishes them. Rotation lands on the next request with no
server restart and keeps the PREVIOUS material authenticating for a
ten-minute placement window (ROTATION_GRACE_MS in the token store) so the
caller holding it does not fail while the new one is being placed; after the
window the old material dies on its own, with no revoke. Revocation itself
stays immediate. Both are read per request behind an mtime cache; the shared
key keeps its launch-time capture. The gate
mcp_tokens_required (device config key or
OPEN_SECOND_BRAIN_MCP_TOKENS_REQUIRED, default false) makes the endpoint
refuse credential-less requests whenever a non-empty token map exists; with
the key absent every credential-less posture - loopback keyless, non-loopback
keyed - is unchanged, and one corner tightens everywhere: a presented
credential that matches neither the token map nor the shared key is refused
with the same generic 401 instead of degrading to anonymous, so a stale or
forged bearer can never ride the keyless loopback posture. stdio identity
stays config-derived (one caller per
process that already owns the process tree). A token mints identity only,
never reach: tool profiles and the reach ceiling still bind every caller
regardless of credential.
That boundary extends to o2b bootstrap --token: the registration bootstrap
writes is the stdio payload above (o2b mcp --vault ...), which presents no
credential, so the bootstrapped harness keeps its config-derived identity
and the minted token changes nothing for it. The material authenticates
HTTP MCP clients an operator configures by hand (Authorization: Bearer or
X-API-Key) - the bootstrap output says the same beside every mint, and
o2b bootstrap --remove <target> tears a provision down again (adapter
uninstall, token revoked, receipt entry dropped).
Shutdown and draining (since v1.50.0)
Neither transport could stop without cutting a request in half. The HTTP
handle's close was server.close() and nothing else — it stops the
listener and returns, while the promise the request callback returns was
floated, so a tools/call in flight was answered with a dead socket. The
stdio loop had no shutdown path at all: a SIGTERM took the default
disposition and killed the process mid-dispatch with no frame written.
SIGINT and SIGTERM now drain. The sequence is the same on both
transports:
stop accepting NEW work;
wait for the requests already begun, to a bounded deadline;
close the transport;
exit 130 (SIGINT) or 143 (SIGTERM).
The deadline is 10 000 ms, overridable with O2B_MCP_DRAIN_MS (a
non-negative number of milliseconds). A value that is not one THROWS rather
than falling back: an operator who set the variable did so to change the
behaviour, and quietly using ten seconds because they typed 10s is the
misleading quiet the drain exists to remove.
A drain that hits its deadline still exits, and the requests it gave up on are NAMED on stderr first, each with how long it had been open, plus the variable that lengthens the wait — a shutdown that silently truncates a tool call is indistinguishable from a crash to whoever was waiting on it.
The exit hooks still run. This is why the signal handler calls
process.exit(128 + signo) instead of re-raising, which is what every
foreground CLI verb does. Two process.on("exit") hooks have to run after
the transport closes — the search store synchronously checkpoints the WAL of
every open writer, and the sync lockfile unlinks every held lock — and the
default disposition for a terminating signal runs neither, because the
process dies by signal and no exit event is emitted. Exiting reports the
same thing to the shell AND emits exit, which is why the drain has to
complete before the exit rather than beside it.
A SECOND signal is not intercepted and falls through to the default handler, so an operator who decides the drain is taking too long is never trapped by it.
What each transport does while draining:
| HTTP | stdio | |
|---|---|---|
| new work | answered 503 with Retry-After: 1 and Connection: close — a code a client retries, not a protocol error | the next line is left unread; the loop stops cleanly at a request boundary rather than half-serving the shutdown |
| the listener | deliberately stays up until the drain finishes, so a supervisor can watch | — |
| in-flight | tracked from the first header to the response's close event, so a request is not counted as finished while its bytes are still queued | tracked from acceptance — a write parked behind in-flight reads included — across the dispatch AND the response write, so a drain waits for queued work it cannot see running yet |
GET /health gains a third field and is never refused during a drain — a
shutdown a supervisor cannot observe is a shutdown it will report as a
crash:
{ "status": "draining", "transport": "http", "in_flight": 2 }status is ok while serving and draining once a drain has started;
in_flight says how much of the wait is left. The probe is not counted as
in-flight work itself: counting it would make the number a supervisor reads
include the act of reading it, and a probe arriving as the last request
finishes would restart the wait it was checking on. A request that arrives
after the drain started is not counted either — it is refused, never
dispatched, and counting it would let a client retrying in a tight loop hold
the shutdown open for its whole deadline over work the server had already
declined.
Background faults (since v1.65.0)
A promise rejection nobody awaited used to end the server: no process-level fault handler was registered, so the runtime default ended the process and one stray rejection anywhere in core took the whole tool surface down mid-session, with no line on stderr that named it.
o2b mcp now installs a fault guard on both served transports (stdio and
--transport http), before the background vault refresh and before the
transport starts, and releases it when the server stops, so the CLI's own
top-level error handling applies again once it has:
An unhandled rejection is survived. It is logged on stderr by name -
[mcp] unhandled_rejection #N: <reason> (server keeps serving)plus the first stack frame, and a non-Error reason is named asnon-error reason: <value>- counted, and the server goes on answering. Each line is redacted before it is written, so a credential in an error message (a token, or a password in a URL) does not reach the host's log.The log is rate-limited, the count is not. Five full lines per 60 seconds, then one summary line (
[mcp] unhandled_rejection: N more suppressed in the last 60000ms). The summary is printed when the window ends, or when the server stops first, so a suppressed count is never lost; past the window the next rejection is logged in full again. A loop that rejects on every request therefore cannot fill the log, and the count still says how often it happened.An uncaught exception exits 70. It is logged by name -
[mcp] uncaught_exception: <reason>; exiting 70 so the exit hooks checkpoint the index and release locksplus the first stack frame - and the process exits throughprocess.exit(70). A second exception thrown while the process is already exiting goes straight to the exit, without another line. A thrown exception leaves the process in a state nothing measured, so continuing to serve from it would answer requests from unknown state. Exiting rather than dying keeps the twoexithooks described under "Shutdown and draining": the search store checkpoints its WAL and the sync lockfile releases every held lock.No drain on an exception. A drain keeps serving the requests already begun, which is exactly the unknown state the exit is there to leave. The drain stays the answer to a signal, where the process is healthy and was asked to stop.
A closed stderr does not turn the guard into the crash: the write is dropped and the fault is still counted.
GET /health on the HTTP transport carries the counts beside the drain
fields:
{
"status": "ok",
"transport": "http",
"in_flight": 0,
"faults": { "unhandled_rejection": 3, "uncaught_exception": 0, "last_fault_at": "2026-10-01T03:12:40.000Z" }
}last_fault_at is null before any fault. A supervisor can see a server
that is surviving rejections before an operator reads its log.
What happens after an exit 70 depends on the client. The Hermes bridge
restarts the server once; Claude Code and Codex do not restart it, and the
tools stay unavailable until the session reconnects the server. The guard is
installed only on the served transports: o2b mcp --probe and every other
o2b verb keep the runtime's default behaviour.
Runtime capability window
Runtime capability flags are evaluated after the static scope. They can narrow
the tool list a process advertises, but they cannot widen --scope writer into
full-server tools. The full server always keeps second_brain_capabilities
available so clients and operators can inspect which tools were available or
withheld and why.
Examples:
o2b mcp --vault /path/to/vault --probe --json --disable-tool second_brain_query
o2b mcp --vault /path/to/vault --allow-tool brain_context --allow-tool brain_feedback
o2b mcp --vault /path/to/vault --max-tools 12second_brain_capabilities returns scope, server_name,
static_tool_count, available_tool_count, advertised_tool_count,
host_ceiling, an available[] list, and a withheld[] list. Withheld
reasons are stable strings such as disabled by runtime capability window,
not allowed by runtime capability window, and outside runtime capability max tool window.
advertised_tool_count is the number a host actually LISTS: available minus
the tools marked hidden, which tools/list filters out. Under the catalog
surface it is seven whatever the tool table's size - the capability
diagnostic, the five always-loaded Brain tools and tool_hydrate - and
everything the surface merely hides stays callable through tools/call. It
is the advertised number a host's ceiling applies to.
Seven is the no-window count. The window is applied before the count, so a
window that withholds one of those seven lowers advertised_tool_count with
it, and a tool the window withheld is not callable through tools/call
either - unlike one the catalog surface merely hid. The capability
diagnostic is the one tool no window withholds.
The handshake instructions follow the window (since v1.56.0)
initialize.instructions used to be one frozen string per scope, so a host
that disabled brain_note still received a paragraph telling the agent to call
it. The text is now rendered from the same capability report: guidance whose
tool this runtime withheld is REMOVED rather than contradicted, and a block at
the end names each removed tool with the reason the evaluator produced.
A runtime that withholds nothing renders text byte-identical to what shipped
before, so the only difference is in the withheld cases. The one deliberate
exception is in the catalog body: the frozen string said every tool omitted
from tools/list stays callable through tools/call, which holds for the
tools the catalog surface hides and not for the ones a window withholds, so
that sentence now separates the two cases. The block lists the
tools this text would have instructed and cannot, not the whole withheld set -
a --max-tools window can withhold over a hundred tools, and pasting that list
into the first thing every agent reads would cost more than the removed
guidance did. On a surface that registers second_brain_capabilities the
block closes by pointing there for the complete report; no capability window
ever withholds that tool. The writer scope does not register it at all, so
there the block ends with the names - a handshake sending an agent to a tool
its own server answers unknown tool for would be the defect this rendering
removes, reintroduced by the sentence that explains the removal.
The host ceiling (since v1.50.0)
host_ceiling reports what this build can say about the per-workspace tool
limit of the runtime that launched the server, and it is present on every
report — especially when the answer is "nobody has established one". A host
that caps a workspace at forty tools and is handed a hundred and ten drops
the excess in silence, so omitting the field where no limit is known would
be indistinguishable from a host with no limit at all.
| Field | Meaning |
|---|---|
target | the install target from --host-target; null when the server was never told |
kind | declared, unbounded, or unknown — three states, and unknown is never read as unbounded |
max_tools | the published limit; null unless kind is declared |
source | where the limit, or the published absence of one, is stated; null when kind is unknown |
reason | why there is no answer; null unless kind is unknown |
within_ceiling | advertised_tool_count against max_tools; null when there is no number to compare |
A server started by hand, or by a registration written before
--host-target existed, reports kind: "unknown" with a reason naming the
flag and the o2b install --target <runtime> --apply that regenerates a
registration carrying it. That is an UNCHECKED ceiling, not an absent one.
Today exactly one runtime declares a limit — Cursor, 40 tools across every
enabled MCP server — which is why the generated Cursor registration selects
the catalog profile. No runtime is unbounded; the member exists so a
host that publishes "no limit" is not recorded as unchecked. The full
resolution ladder for the profile that ends up in a registration is
documented under "Tool ceilings, --host-target, and the tool-profile
ladder" in cli-reference.md.
Tool-surface profiles and the two-pass catalog (since v0.37.0)
Named profiles bundle a scope plus capability window so hosts stop hand-rolling allow/deny flag lists:
o2b mcp --vault /path/to/vault --tool-profile catalog
o2b mcp --vault /path/to/vault --tool-profile recall --probeProfiles: full (default), writer, catalog, recall (memory
read/write surface, no admin tools), and minimal (writers + context +
search). The mcp_tool_profile config key (env:
OPEN_SECOND_BRAIN_MCP_TOOL_PROFILE) selects one without flags; an
explicit --scope or window flag wins over the profile's fields. An
unknown profile name FAILS CLOSED: o2b mcp exits 2 naming the known
profiles and where the bad name came from, rather than serving the full
surface - a typo in a profile meant to narrow an agent's tools must not
widen them (changed in the security hardening release; earlier versions
failed open with a stderr note). Hard-window profiles always retain
second_brain_capabilities, so withheld tools stay discoverable with
reasons.
The catalog scope is the two-pass surface: tools/list advertises
only the capability diagnostic, the five always-loaded Brain tools, and
tool_hydrate; every other tool stays callable through tools/call.
Call tool_hydrate with no arguments for the compact catalog (name,
one-line description, group), then with names: [...] for the full
input/output schemas of exactly the tools you need - unknown names are
reported per-name without failing the batch.
Skill surface (since v0.37.0)
list_skills returns the agent skills shipped in the plugin's
skills/ directory plus the machine's own skills folder (its entries
shadow shipped ones by name): Brain/skills/ on a machine that keeps the
legacy tree, <vault>/skills/ once it uses the brain store, and a
device-local skills/ folder with no vault. Skills are always files, never
store records; the migration moves Brain/skills/ to <vault>/skills/. get_skill fetches a skill's SKILL.md by
name; an optional file_path reads an auxiliary file and is
path-traversal-guarded to the skill directory. skills_attach scores
skills against the current turn text with a deterministic BM25-style
scorer and returns a char-budgeted block of top matches; it returns
enabled: false with an empty block unless the skill_auto_attach
config key is "true", so default per-turn injection is unchanged. The
native Hermes provider calls it from prefetch() fail-soft.
Three properties of that surface come from the offer chain (v1.43.0):
list_skillsreports what a skill shadows. Each entry carries ashadowedarray of the same-named skill directories it overrode, in discovery order. Previously the losing path was discarded, so an operator reading a surprising skill body had nothing telling them a second copy existed. An empty array means discovery found none.skills_attachreturns anoffer_id. It is a content-addressed digest of the offer itself - the normalised turn text plus the offered(name, path)pairs in rank order - so anyone holding the offer can recompute it and no offer store has to exist. It isnullwhen nothing was offered; an offer that was never made has no identity. The rendered block cites the id too, so an agent that reads only the block can quote it back.get_skillaccepts thatoffer_id. The runtime's session log then records which offer the fetch came from, ando2b brain import-sessionstamps it onto theskill_invokedcontinuity record, wherejoinSkillInvocationsToOffersjoins the invocation back to its offer. A malformed id is refused rather than dropped ATget_skill, which can answer its caller:isSkillOfferIdrejects it withINVALID_PARAMS. The import boundary cannot refuse anyone - the session log is historical and its author is gone - soreadSkillOfferIdreports a non-conforming value the same as absence, and a host-native skill call carrying a junk id is stamped with no offer and reads as unattributed. The two surfaces answer the same malformed value differently, on purpose, and only one of them can tell anybody. A call citing no offer is stamped exactly as before and reads as unattributed, never as belonging to a nearby offer. The id is the acting agent's claim about where an invocation came from: checkable (well formed, and recomputable from the offer), not authenticated - this system has no credentials to authenticate it with. The join is also retrospective, because an invocation is only observed when a session log is imported.
The join surfaces as ONE count under two spellings, which is worth stating
because they look like two figures: brain_skill_proposals with
operation: "usage" returns offerAttributedCount per skill, and the same
number prints as from_offer= on o2b brain skill-proposals usage. It
counts the invocations of that skill which cited an offer, out of
invocationCount total. The remainder are not unattributed by error - most
runtimes' skill calls follow no offer at all.
When the optional skills decision-model use is shadow or enforce,
skills_attach also returns decision_model: { mode, applied, degraded?, model? }
and, in enforce, may offer a subset or reorder of the BM25 shortlist chosen
by two decision requests. The field is absent while the use is off, the
default. See Skill selection.
skills_attach additionally applies a discriminating-term floor: a
candidate whose entire match rests on terms more than half the descriptor
corpus carries is dropped rather than offered, because such a term is
evidence for no skill in particular. The only input is corpus document
frequency, so the rule holds in any language and there is no word list
anywhere in its derivation. A single-skill corpus carries no
discrimination at all, so the floor abstains there rather than dropping
the only candidate.
Three optional config keys (each with a matching environment variable) tune the surface; each is off/unset by default, so behaviour is unchanged unless an operator opts in:
skills_dir(OPEN_SECOND_BRAIN_SKILLS_DIR) overrides the skill discovery root, replacing the default skills folder with an arbitrary path (e.g. an external~/.hermes/skills/) without symlinks.~is expanded; a relative value is anchored to the directory of the resolved config file so the root is the same regardless of the process working directory. The shippedskills/root is still scanned.skills_attach_triggers(OPEN_SECOND_BRAIN_SKILLS_ATTACH_TRIGGERS), when"true"or"1", folds each skill'striggersfrontmatter field into the scorer as a 2x BM25 tag signal (alongside name at 3x and description at 1x). When unset,triggersis ignored and scoring stays name + description only. Thetriggersfield accepts a scalar string (triggers: "research lookup") or an inline array (triggers: [research, lookup]); the scorer also emits overlapping bigrams for runs of Han characters, so a spaceless CJK query can match a trigger keyword.skills_attach_usage_ordering(OPEN_SECOND_BRAIN_SKILLS_ATTACH_USAGE_ORDERING), when"true"or"1", reorders the survivors of the discriminating-term floor by a bounded composite of lexical score and observed invocation usage,score * (1 + 0.5 * weight), withweighta ranking weight in (0, 1] built so frequency stays visible among recently used skills: the skill's recency decay (the same 30-day half-life curve the usage report shows, measured from its last dated invocation) timeslog(1 + count) / log(1 + maxCount), wherecountis the skill's deduplicated invocation count andmaxCountthe largest count among the usage rows that join a discovered skill. A skill invoked hundreds of times this week therefore outranks one invoked once yesterday, where the usage report's decay value saturates at 1 for both and the name tie-break would decide; when every joined count is 1 the ratio is 1 everywhere and the ordering is recency alone. A skill still never rises by more than half again its own lexical score, so a wide relevance gap stays decisive and the floor is never crossed by weight. Equal composites resolve by name ascending, and the returnedscorestays the untouched lexical score. Usage joins onto the discovered skills by name, deterministically and without a word list: askill_invokedrecord whose skill string equals a discovered name joins exactly, a plugin-qualified invocation (vendor:name, the shape the Claude CodeSkilltool records) joins through the longest suffix after a colon that names a discovered skill, and a key matching nothing is dropped - unknown is never guessed onto a nearest-looking name. Private records never count: the continuity read dropsprivaterows the same way every reader does. The count is also unscoped: everyskill_invokedrecord counts, whichever agent or owner produced it, so at a remote or shared reach the ordering reflects other owners' activity, not only yours. With the flag off - or on, in a vault with no usage records - the offer is byte-identical to the lexical order. The cost of opting in is one whole-continuity-store read per attach call - a scan of everyskill_invokedrecord, so it grows with the invocation history (the same reado2b brain skill-proposals usagealready prices); it is not memoized, because the append-only store exposes no cheap signal a cache could invalidate on, and a stale ordering would be worse than a slow one. A bounded trailing window is a documented follow-up, not shipped.
Workspace Insight Suite tools (since v0.38.0)
brain_search accepts global: true for cross-vault union search:
one query fans out over the active vault, registered profile vaults,
and read-only recall sources (managed by o2b brain source), merging
results by score. Each result carries an additive origin field plus
an origin:<label> reason (local, profile/<name>,
source/<alias>). Non-active origins search with self-healing and the
query cache disabled, so an external vault is never written to; a
missing index degrades to a per-origin warning.
brain_trigger is the consolidated trigger-queue tool: scan
generates deduped triggers from semantic-health and retention data,
list / history read by effective lifecycle status,
acknowledge / dismiss / act transition one trigger. Cooldown keys
keep the same issue from reappearing while an earlier trigger is open,
snoozed, or cooling down after an acted twin; brain_brief
view="morning" surfaces capped pending
triggers and marks them delivered (once per trigger_cooldown_days) for
a caller at local reach; below local reach it shows no trigger section
and marks nothing delivered, because the queue is the operator's own.
suppress silences a cooldown key indefinitely - it is legal from any
status and carries no clock, so the finding never re-nags - and
unsuppress restores the status suppression interrupted along with its
original cooldown arithmetic. Suppressed triggers are terminal: hidden
from list, present in history, never in the brief. snooze is
suppression's time-boxed sibling: it defers an open trigger for a
bounded window (three days when the caller names none) or until the
finding fires again, whichever comes first, and unsnooze wakes it by
hand. The wake (the window's end, a refire, or an explicit unsnooze)
restores the exact status the snooze interrupted. A snoozed trigger
is terminal like a suppressed one: hidden from list, present in
history, never in the brief, and governed by its snooze window rather
than the TTL expiry clock, so a long snooze cannot be cut short by
expiry. Dismissal carries no clock: a dismissed finding stays dismissed
for however long every re-fire rests on artifacts the dismissal already
covers, and returns - as a new record through the normal creation path,
never as a status flip of the resolved one - only when a scan fires it
on a source artifact the dismissed record does not name. That
comparison is set-based and order-insensitive, so a narrower sighting
of the same grounding is not movement; an acted twin keeps the plain
cooldown window after its resolution. A candidate an
existing record silenced increments occurrences and stamps
last_seen_at on that record, whatever silenced it - suppression, an
open twin, a snooze, a dismissal, a cooldown window, or the per-kind
cap - so silence is
auditable. Two limits are exact rather than implied: one scan seeing the
same finding twice counts once, so occurrences counts scans and not
candidates, and a candidate silenced before any record for its cooldown
key existed has no ledger to write to and is reported only in skipped.
One unreadable record never costs the operator the rest of the queue.
scan, list and history each return an additive unreadable array -
path, the frontmatter key at fault (null when the file itself could
not be read), and the refusal text - so a hand-edited record is named
instead of omitted, and the readable records stay listable and
transitionable while it is broken. brain_brief view="morning" carries
the same information as triggers_unreadable plus a
trigger_queue_error when the queue could not be read at all, and
renders both into the brief text: an absent pending-trigger section now
means the queue was read and held nothing.
brain_deep_synthesis assembles a deterministic topic dossier
(matched notes, agreements, contradictions, stale claims, knowledge
gaps; triggers: true enqueues findings). It also returns a
strongest_objection — the single best-formed counter-finding
(basis: contradiction → superseded → stale → knowledge_gap →
thin_evidence) framed as a steelman seed against the dossier's implicit
conclusion, or null for a larger internally-consistent body.
brain_idea_discovery
ranks next-direction candidates from open questions, orphan notes, and
aging inbox signals.
brain_recall_gate emits a gate_telemetry continuity record per
decision when the recall_gate_telemetry config key is "true"
(default off) - decision, stable reason, host, SHA-256 prompt prefix;
never the raw prompt. brain_recall_telemetry gains gate_list /
gate_summary operations. Since v1.82.0 a record whose call carried a
measured quality also carries the graded surfacing key - key, tier
and the channel the decision arrived over - and gate_summary counts
the measured records by tier; the opt-in-off path records nothing, and no
new record kind exists.
The MCP server emits one mcp_route_latency continuity record per tool
call when the mcp_route_metrics_enabled config key
(OPEN_SECOND_BRAIN_MCP_ROUTE_METRICS_ENABLED) is "true" (default
off): tool name, scope, status (ok/error), duration, and the sorted
set of argument KEY NAMES only - never argument values. The emit is
gated and fail-open, so it can never fail or slow-fail the call it
measures beyond one synchronous continuity append. brain_route_metrics
reads them back (operation: "list"|"summary").
Since v1.74.0 a record can also carry stages: the wall time the call
spent in each write stage, as { name, ms } entries. Names come from a
closed nine-name allowlist (validate, idempotency_lookup,
near_duplicate_lookup, document_write, idempotency_remember,
log_append, preference_write, write_receipt, lint); any other
name and any negative or non-finite value is dropped before the record is
written, and the key is absent when no stage ran. Stage timings never
appear in the response of the call being measured, only in its
continuity record. brain_route_metrics summary adds a stages
roll-up per route (count, average and p95 per stage name); a route
without stages has no stages key.
The full observability contract behind these tools - event kinds,
always-on vs opt-in status, correlation IDs, payload safety, and the
continuity schema version - lives in docs/observability.md.
Hermes integration
Hermes discovers MCP servers from ~/.hermes/config.yaml under the
mcp_servers key. After installing this plugin, register the MCP server on
the same machine that hosts the vault.
The Hermes CLI accepts the registration directly:
hermes mcp add open-second-brain --command o2b --args mcp --vault /path/to/vault--args is a single flag; everything after it on the line (here
mcp --vault /path/to/vault) is collected as the argument list and forwarded
to the MCP server's command line. Do not wrap all of those arguments into one
quoted shell string and do not repeat --args per token — both forms make
Hermes pass a single concatenated argument to the MCP server.
You can also edit ~/.hermes/config.yaml by hand:
mcp_servers:
open-second-brain:
command: o2b
args: ["mcp", "--vault", "/path/to/vault"]
enabled: true
timeout: 30
tools:
include:
# Drop any line below to disable a specific tool, or remove the
# whole `tools.include` block to expose every advertised tool.
- second_brain_status
- second_brain_query
- vault_healthIf you run Open Second Brain from a checkout instead of an installed package,
point command at the absolute path of scripts/o2b:
mcp_servers:
open-second-brain:
command: /srv/projects/open-second-brain/scripts/o2b
args: ["mcp", "--vault", "/path/to/vault"]After editing the file, run /reload-mcp inside Hermes to pick up the new
server.
The Hermes plugin manifest (plugin.yaml) advertises this MCP entrypoint via
the mcp_server field so future Hermes releases can auto-register the server,
but the official Hermes config flow is the source of truth today.
Updating the MCP registration
Updating the plugin via hermes plugins update open-second-brain does not
rewrite ~/.hermes/config.yaml. Your existing mcp_servers.open-second-brain
entry keeps working as long as the command and args you originally
registered still resolve.
After an update:
Restart the gateway so the MCP subprocess is reloaded:
hermes gateway restartIf the new release adds a flag, re-add the registration with the updated
--argslist (or edit the YAML by hand):hermes mcp remove open-second-brain hermes mcp add open-second-brain --command o2b --args mcp --vault /path/to/vault
scripts/o2b mcp --vault /path/to/vault from the checkout can be used to
sanity-check the server before re-registering it.
Removing the MCP registration
To remove just the MCP server without uninstalling the plugin, run:
hermes mcp remove open-second-brain
hermes gateway restarthermes mcp remove deletes the registration entry. Open Second Brain runs
over stdio (JSON-RPC 2.0) and does not use OAuth, so there are no tokens for
this server specifically; the OAuth-token cleanup hermes mcp remove performs
only matters for transports that authenticate that way. The installed plugin
and its CLI commands stay in place, so hermes plugins update will continue
to track new releases.
To remove both the MCP server and the plugin itself, follow the
Uninstalling section in the project README. Open Second Brain never edits
~/.hermes/config.yaml on your behalf, and o2b uninstall is a read-only
helper that prints the exact Hermes commands to run.
Claude Code and Codex
The Claude Code plugin manifest exposes o2b mcp as a regular command that
Claude Code can invoke. Codex installs the same scripts/o2b script through
its plugin manifest, so the MCP entrypoint is reachable from a Codex shell as
well. There is no auto-registration into Codex's MCP discovery — add the
server to your Codex MCP config the same way as Hermes.
Writer split (Claude Code 2.1.121+)
The plugin manifest .claude-plugin/plugin.json declares two MCP-server entries inline under mcpServers (there is deliberately no .mcp.json at the repository root: Claude Code would also load it as a project config for sessions started in a checkout, where ${CLAUDE_PLUGIN_ROOT} is unset and both servers fail to spawn):
open-second-brain- the full surface, whose advertised tool count is stated once under "Tool Highlights" above (including the consolidatedbrain_brief,brain_analytics, andschema_inspect, plusbrain_health,brain_mcp_landscape,brain_agent_query,brain_agent_diff,brain_recall_gate,brain_pinned_context,brain_memory_bridge,brain_pre_compress_pack,brain_audit,brain_sources, andbrain_switch_vault) and 18 hidden deprecated aliases listed under "Consolidated views and deprecated aliases" above; subject to Claude Code'sMCPSearchtool-search deferral when MCP definitions push the system prompt past 10% of the context window.open-second-brain-writer- a minimal always-loaded surface of five tools:brain_feedback,brain_apply_evidence,brain_note,brain_pinned_contextandbrain_context(the pull-bootstrap ofBrain/active.mdplus pinned context, v0.16.0; its handler regenerates the sharedactive.mdwhen absent or stale, so it is a writer under the concurrency kind too). The agent records taste signals, evidence events, milestone notes, and current-task pinned facts - and fetches the active rule digest at session start in runtimes without a SessionStart hook - without a ToolSearch round-trip on every session boot.
Both servers reuse the same backing CLI (o2b mcp --scope writer vs the default --scope full), and since v1.70.0 both are registered with --harness claude-code. Handlers are byte-identical; the writer-mode instructions text explicitly tells the agent to prefer the writer copy over any duplicate the full server still exposes (both call the same code path).
brain_feedback's scope argument stays optional. When the vault declares feedback.default_scope in Brain/_brain.yaml, a call that omits scope records the signal under that default category; an explicit scope always wins, and with no default configured a scope-less call stays scope-less. The same effective scope is reused for a force_confirmed: true preference so the preference and its signal share one scope. The configured value is validated against the same constraints as any signal scope (non-empty after trim, single-line, at most 128 characters).
Write-time page lint (since v1.46.0)
The four note-write tools - brain_create_note, brain_update_note,
brain_append_note and brain_write_batch - can now return a top-level
lint key carrying ranked findings about exactly the pages that call
committed.
This closes a real hole rather than adding a nicety. Document validation
used to run only under strict on brain_create_note; update, append and
every batch operation committed with NO document validation at all and
returned a hardcoded success flag. Callers will therefore now see findings
on pages that used to be accepted in silence. The lint is not a new
policy: it runs the same error-severity validator a strict create runs,
plus the merged-link and broken-Brain-wikilink detectors the doctor
already reports, so a strict create and a linted update agree on what a
valid document is.
The key is absent when there is nothing to say, and that absence means
clean. A receipt for a clean write is byte-identical to the one that
shipped before. Because absence carries that meaning, a lint that could
not run reports itself rather than going quiet: the report then carries an
unavailable object (code: "page-lint-unavailable", plus a message
composed from an errno code, never a path) with all counters at zero.
The report's shape:
findings- ranked errors first, then by page, code and location, and capped at 25 entries. Each finding carriesseverity(errororwarning),code,page(the vault-relative file),path(the location WITHIN the document:body,frontmatter,tags, or a link target),message, andnext_commandwhere the code has a registered exit.total/returned/truncated- findings detected, findings carried, and whether the cap dropped any. A capped list can never be read as a complete one.skipped- written pages that were not linted, each with apage, areasonfrom the closed setpage-over-byte-cap,page-unreadable,page-lint-failed, and adetailcarrying the measurement or the errno code. A page too large to validate, or one the lint threw on, lands here rather than vanishing - and one page's failure never discards the findings already collected for the others.
Since v1.64.0 the report can also carry a near-duplicate finding: the
authored body scores at least 0.8 Jaccard against an existing page in the
same directory and scope bucket. Exact duplicates were refused before;
close ones landed in silence. The score is computed over the body exactly
as authored, frontmatter is never compared or touched, an unreadable or
over-cap sibling is excluded as a candidate, and the finding never gates
the write - the receipt says what landed next to what.
Since v1.74.0 the lint answers at the caller's reach. A sibling the caller may not read is never named, scored or counted on the receipt, and the broken-wikilink and merged-link checks treat a withheld page as absent: a wikilink to it reads as broken, and a merge chain ends at its first withheld hop.
With the config key near_duplicate_write_widening_enabled
(OPEN_SECOND_BRAIN_NEAR_DUPLICATE_WRITE_WIDENING_ENABLED, default off)
set to "true", the near-duplicate check also compares the written pages
with candidates from other directories, pulled from the keyword index
(the top 20 chunks per written page) and re-read from disk, under the
same 0.8 bar, scope bucket and reach. The pull spends no embedding call.
An index that cannot be opened, queried or closed, or any other failure
while collecting the candidates, does not fail the write and does not
disable the same-directory check: the report then carries
widening: "index_unavailable" and a widening_detail code naming the
failure, never a message or a path. With the key off nothing is opened
and the receipt is unchanged.
The lint runs AFTER the commit and reads what is on disk. It never gates
the write, and it never throws. A call that authored no bytes - a
brain_create_note with if_exists: "skip" that skipped, a log-only
batch - names no page and so carries no lint key.
Three write paths are deliberately outside this envelope, stated rather
than implied. brain_write_session keeps its own errors[] channel and
its own correction prompt: it validates BEFORE committing and refuses,
which is a different contract from an advisory computed after the fact,
and folding it in would give one tool two error channels. The write-session
engine and source distillation write through their own lexical validation
and bypass the note-write path entirely. Log appends (brain_note,
brain_apply_evidence) name no note path and are not linted, because the
log line is machine-composed rather than authored.
Source distillation
brain_distill_source writes one idempotent distillation page per source
from atomic claims the calling agent supplies. Open Second Brain runs no
model here: it validates the claims, checks every quotation inside them
against the source, and writes the page. The page cites each claim as
[[source#^block]] when the claim names a block.
Inputs:
| Argument | Type | Meaning |
|---|---|---|
source_path | string, required | the source identity: a vault-relative path or a URL |
claims | array, required | { text, block? } items; block is the source block id (^abc or abc); at most 1000 per call, and a claim's text is one line |
strict_quotes | boolean, optional | since v1.67.0, refuse the whole write when any quoted span fails the check |
excerpt | string, optional | since v1.67.0, the verbatim text the caller read from a url-only source, stored on the page |
agent | string, optional | agent identity override |
Result keys: distillation_path, created, claim_count, source_hash
(absent when the source had no bytes to hash), trust (trusted or
untrusted), and since v1.67.0 capture_scope (always present) and
quotes (present only when at least one claim contains a quoted span).
The quote check
Since v1.67.0, every quoted span in a claim is checked before the page is
written. A quoted span is the text between a pair of quotation marks in a
claim. A quotation mark is any character with the Unicode
Quotation_Mark property, so straight, curly, low-high, guillemet and
corner-bracket quotes all count without a list of languages. Marks pair
by position: an opener follows the start of the text, whitespace or
punctuation other than a mark that just closed, a closer is followed by
the end, whitespace or punctuation. A mark whose Unicode category is
opening or closing punctuation (low-9 marks, corner brackets) is
directional by itself, so corner brackets pair even when glued to CJK
letters. Any other mark with a letter on both sides (an apostrophe
inside a word) is never a delimiter. A closer pairs only with an opener
of its own width (single with single, double with double), so a
possessive apostrophe never ends a double quote. Only the outermost
closed span is checked; a quote inside it is part of its text. A mark
that finds no partner is counted as unpaired and left on the page
untouched, and it hides no later quote. A mark separated from its text by
an ordinary space is counted as unpaired, not checked; a single no-break
or thin space, as in spaced guillemets, is allowed. A pair holding fewer
than two letters or digits (rock 'n' roll) is not a quote and counts
as two unpaired marks. An empty span is not a quote.
Each span is compared with the evidence after the same normalisation is
applied to both sides: Unicode NFC, inline Markdown reduced to its
display text (paired emphasis and code markers, so snake_case and
2*3 stay as written, [text](url), [[target|alias]],
[[target]]), line-leading block markers (on the evidence side only: a
span's own leading 3. , - or > is wording) and a trailing ^id
removed, quotation-mark variants folded, whitespace runs collapsed. Case,
punctuation and wording are never folded. The normalised span must then
be an exact substring of the normalised evidence that starts and ends on
a word boundary, by the Unicode word rules, so "safe" does not verify
inside unsafe. A span holding an ellipsis (… or three or more dots)
is split there, and every fragment must appear in order without overlap;
an edge next to an ellipsis may fall inside a word. A bracketed insertion such as
[sic] is not interpreted and fails as a mismatch. The page bytes are
never normalised; normalisation is used for the comparison only.
The evidence depends on the claim:
A claim with
blockis compared with that block only. A block id that resolves nowhere givesblock-not-found, an id defined twice givesblock-ambiguous; neither falls back to the whole source, because the citation is part of what the page asserts.A claim without
blockis compared with the whole source and verifies asverified-in-source, a weaker outcome thanverified-in-block.A source whose bytes are not valid UTF-8 gives
source-not-textfor every span. A source with no local bytes givesurl-only.A vault source the caller may not read at its transport reach (or, with
integrity.owner_scope_delivery: fail, another owner's page) is treated exactly as an absent source: every span givesurl-only, nosource_hashis returned or recorded on the page, the page lands in the untrusted lane withcapture_scope: url-only, and anexcerptis accepted for it.
The bytes checked are the bytes read once for source_hash, so the
verdict and the digest on the page always describe the same content.
A failed span is unquoted by default. Its two quotation marks are
removed, the words stay, the write lands, and the result names the
failure. The page never shows an unverified quote. Unquoting changes the
bytes the caller supplied; this is deliberate, the change is reported in
quotes.findings, and an identical re-run produces an identical page.
Strict refusal. With strict_quotes: true the whole write is refused
when any span would be unquoted, and nothing is written. The refusal is a
JSON-RPC error with INVALID_PARAMS and error.data.code set to
quote_unverified; its message names claim indices and outcomes
(claim 0: not-in-block), never claim text or paths. Unpaired marks
never trigger a refusal.
The quotes report:
| Key | Meaning |
|---|---|
checked | quoted spans examined |
verified_in_block | spans found verbatim in the cited block |
verified_in_source | spans found verbatim in the whole source (claims without block) |
unquoted | spans that failed and lost their quotation marks |
unpaired | quotation marks without a partner, counted only |
findings | one { claim, outcome, span } per unquoted span: claim is the 0-based claim index, outcome one of not-in-block, not-in-source, block-not-found, block-ambiguous, source-not-text, url-only, span the span text capped at 120 characters |
total, returned, truncated | the findings list is capped at 25 and declares its own truncation |
A page whose claims held spans carries quotes_verified and
quotes_unquoted in its frontmatter. A page without spans is
byte-identical to the one earlier releases wrote.
Capture scope and the excerpt
Since v1.67.0, capture_scope says how much of the source the vault
actually holds:
| Value | Meaning |
|---|---|
full-local | the source is a file this vault holds |
bounded-local | the source has no local bytes, and the page stores a verbatim excerpt the caller read |
url-only | the source is a URL, or a vault-shaped path with no file behind it |
The page records capture_scope in its frontmatter only when the value is
not full-local, so a page over a local source stays byte-identical.
excerpt makes bounded-local reachable: for a url-only source the
caller passes the text it read, at most 65,536 bytes of UTF-8. The page
stores it under a ## Excerpt heading in a fenced block with the info
string excerpt, records its sha256 as excerpt_hash, and quotes are
checked against it. An excerpt is refused with INVALID_PARAMS, before
anything is written, when the source is a file the vault holds (the file
is the evidence), when it holds no text (only whitespace, control or
format characters), when it contains NUL, or when it exceeds the cap.
Safety notes
The vault path is bound to the server instance at startup. Tools cannot escape it.
second_brain_statusreuses the same redaction logic aso2b export-config.Brain writers (
brain_feedback,brain_apply_evidence,brain_note,brain_pinned_context) go through atomic-rename writes so an interrupted call leaves either the prior or the new file, never a torn hybrid.MCP tools can declare lightweight output contracts; declared contracts are validated against
structuredContentbefore the text mirror is emitted.Since v1.32.0 the
vault_pathfields returned by the core tools carry a stable opaque store reference (vault://<hash>) instead of the absolute host path, because tool responses land in model context. Setexpose_host_paths: true(orOPEN_SECOND_BRAIN_EXPOSE_HOST_PATHS=true) to restore the raw path. Until v1.49.0 only three sites honoured that rule and every othervault_pathreturned the raw host path; the field now comes from one function (src/mcp/vault-path-field.ts) at every emitting site, andtests/core/architecture/vault-path-census.test.tsenumerates those sites from the source so a new one cannot opt out. On a device config that cannot be read the field carries{ "error": "..." }naming the file, rather than falling back to the path it exists to redact.Since v1.32.0
brain_feedbackresponses include a conflict advisory when the incoming principle closely resembles a confirmed same-scope preference (the write still proceeds); the advisory names the preference id and the similarity evidence.Since v1.33.0 four belief-lifecycle tools join the surface (102 total):
brain_lifecycle(tombstone / supersede / temporal-replace / tip / curator),brain_claims(claim-graph queries: current truth, truth-at-instant, replaced-by, contested-by),brain_decision(record / outcome / rate / list / compare / similar / history / recall, plus the open-decision actions open / list_open / show_open / resolve / discard that park a question with enumerated options atBrain/decisions/open-<slug>.mdand later mint the real decision page throughresolve), andbrain_tension(detect / list / show / confirm / dismiss / resolve). Decision-change receipts store only accountable provenance; free-text hidden-reasoning fields are rejected by the closed schema.Since v1.33.0
brain_session_grepacceptssince/beforeturn-time bounds andbrain_searchresults carryauthoredAtwhen the indexed document preserved a turn instant. Context-pack reports list unresolved tension warnings for injected subject notes.Since v1.34.0
brain_statusjoins the surface (103 total): one consolidated operator snapshot over doctor, semantic health, hygiene, stale scan, review queue, active profile, and state-file health, with a next-command hint on every problem line.Since v1.34.0 existing tools gain optional params:
brain_ingest_batch_planacceptssrc_subpath,exclude, andreconcile(dispatched-vs-ingested gap report),brain_ingest_sourceacceptspre_extract(deterministic code-structure seeds),brain_doctoracceptsrepair/apply(guarded fixes for doctor-detected classes), andbrain_searchacceptsdegree(backlink/outlink cardinality predicates). Omitting every new param keeps each tool's output byte-identical.Since v1.35.0 three note-write tools join the surface (106 total):
brain_update_note(update an existing note's body and/or merge frontmatter keys),brain_append_note(append to an existing note's body), andbrain_write_batch(an ordered mixed batch of create note, update body or frontmatter, append note, apply evidence, and append log line operations, validated and projected in memory first and committed all-or-nothing; the first invalid operation aborts with a typed error naming its index and nothing touches disk). All three enforce the exact create-note safety envelope: path traversal, the Brain machinery root, and vault-scope-excluded paths are refused, and a missing target is a typed error.Since v1.35.0
brain_session_grepacceptsinclude_raw(carry the original raw capture inline beside each derived record, every item stamped with anextractedboolean discriminator) andraw_budget_chars(clip raw payloads while identity fields survive). Search outcomes carry thememory_trust_assessmentandretrieval_decision_tracereceipts when the retrieval trust gate is enabled. Omitting the new params keeps each tool byte-identical.Since v1.36.0
brain_diarizejoins the surface (107 total): a read-only entity profile with a deterministically computed stated-vs-evidenced section (each line carrying an evidence identity) and one needs-llm-step envelope for the prose; unknown entities are a typed error.Since v1.36.0
brain_deep_synthesisstructured content additively exposes per-findingcausal_context, decomposedconfidencecomponents (support, opposition, freshness, coverage), and theexcluded_findingsledger withexcluded_finding_count; prior fields are unchanged.Since v1.37.0
brain_retrieval_planjoins the surface (108 total): a shadow-only per-question retrieval advisor composing query intent/weights, the summary-surface route, impact-per-token allocation, the calibrated token-impact ledger, and observed route p95 latency into a read-only plan with a marginal-value stop; it exposes no mutating parameters and changes no ranking.Since v1.37.0
brain_searchaccepts optionalsession_scopeandproject_scopefilters (composite scope keys; omitting them keeps results byte-identical) and its outcome carries an advisorysurfacefield when the deterministic router selects the summary surface; non-summary queries are unchanged.Since v1.38.0
brain_healthadditively carries asuppressed: { concept_gaps, batch_inflation, baseline }object when the optionalhealth.silence_beforewatermark hides at least one advisory finding whose underlying entries are entirely older than the baseline date; the verdict is computed from the surfaced findings, the key is absent whenever nothing is hidden, and with no watermark set the output is byte-identical to v1.37.0.Since v1.39.0 eleven content-returning tools accept an optional
agent_scopeargument (brain_context_pack,brain_pre_compress_pack,brain_anticipatory_context,brain_brief,brain_retrieval_plan,brain_file_context,brain_search_expand,brain_deep_synthesis,brain_search_by_source,brain_agent_query,second_brain_query), joiningbrain_searchandbrain_querywhich already had one. A page that is ownerless matches every scope; an owner-tagged page matches only its owner; a page whose ownership cannot be read is withheld. The preference-backed surfaces additionally requireintegrity.owner_scope_deliveryto bewarnorfail; the search-backed ones filter whenever a scope is passed. Fivebrain_briefviews that cannot honour a scope (daily,weekly,monthly,operator,today) reject an explicitagent_scopewithINVALID_PARAMSnaming the view rather than accepting and ignoring it. The tool count is unchanged at 108 and no output schema changed.Since v1.49.0 the no-argument surfaces that reach owner-taggable artifacts apply the ownership rule using the server-resolved agent identity, and only under
integrity.owner_scope_delivery: fail. They arebrain_backlinks,brain_unlinked_mentions,brain_moc_audit,brain_hygiene(bothscanandapply),brain_scaffold_stub,brain_doctor(includingrepair),brain_claims,brain_idea_discovery,brain_stale_scan,brain_event_trace,brain_agent_diff,brain_analytics(every view that names an artifact:timeline,concept_synthesis,belief_evolution,dedup),brain_health,brain_retention,brain_review_candidates,brain_clusters,brain_triggerandbrain_dream- plus thepreference,topic,backlinksandlogMCP RESOURCE templates, which had no ownership filtering at all and returned another owner's file verbatim. A withheld row is dropped and no count reports it, so a filtered report reads exactly like a report over a vault that never held the rows; a withheld resource answers with the same not-found message an absent one produces.brain_hygiene applyandbrain_doctor repairbound the PLAN, so the write is bounded and not only the payload. With the gateofforwarnall of them are byte-identical -warnobserves nothing here by construction, because reporting whatfailwould withhold is itself the disclosure.brain_backlinksadditionally gains anunparsedkey, present only when the walk skipped an artifact it could not parse: acountof 0 beside a non-emptyunparsedis not a measurement, which is the distinction a legacy-frontmatter vault previously could not make.Since v1.39.0
brain_context_receiptsaccepts a thirdsummaryoperation, plussince,until, andmax_receipts. The summary folds existing receipt records for one session into counts, distinct items, and per-item injection frequency and token cost. Receipt emission is opt-in, so a window with no receipts returnsrecorded: falsecarrying no counters at all - a consumer must branch before it can read a number, so "nobody enabled receipts" can never be read as "retrieval injected nothing". An uncomparablesince/untilvalue is rejected withINVALID_PARAMSrather than filtered into an empty result.Since v1.39.0
brain_doctoradditively carries anuncertainarray for conditions the doctor attempted but cannot claim completed cleanly - dropped frontmatter lines, lineage-ledger findings, stale lock files, and a missing vault identity marker. The key is absent when there is nothing to report.brain_hygienelikewise gains an additive top-levellink_integritykey, which reportsmeasured: falsewith a reason rather than a misleading zero when the index's last run was not a forced full pass.Since v1.39.0
brain_anticipatory_contextreports acache_refusalreason when a cached pack is refused because the vault state it was built from has moved or its validity window has expired, instead of silently serving or silently rebuilding.Since v1.40.0
brain_doctoradditively carriesnext_commandon each reported issue whose code has a registered exit, and ano_exitobject giving, once per code, the reason a class has no single command. The two are complementary: a caller can tell a class it can act on from one that needs a human judgement over content, and neither is silent. Both keys are absent when every reported code has an exit, so the payload is byte-identical for a vault whose findings all resolve.Since v1.40.0
vault_healthnotices carry the samenext_commandfield, resolved from the same registry, so the notice channel no longer requires a consumer to match the command out of an English sentence.Since v1.40.0
brain_dreamacceptsstepandgates.stepruns one independently-runnable step; any other value, including the five phase labels, is refused with the coupling named rather than silently running more than was asked.gatesoverrides a dream gate for that run only and never writes to_brain.yaml.stepis deliberately not a schema enum, because an enum would swallow the per-step refusal reason, which is the deliverable for the steps that cannot run alone.Since v1.40.0
brain_skill_proposalsaccepts arecoveroperation and four contract arguments onaccept(prerequisites,rollback,side_effects,verification), plus anevidenceoperation that reports a proposal's self-declared support beside the independently recorded procedural outcomes. Three states stay distinct: recorded successes, recorded failures, and no recorded outcome at all - the last is never reported as a zero success rate.recoveris the operator surface for an accept sequence a crash left outstanding.Since v1.40.0
brain_procedural_memoryentries carry the four contract fields back onlist, so a contract written at acceptance is readable rather than write-only.Since v1.40.0
brain_session_summaryaccepts an optionalproject_scope, normalized by the same slug rule as the session axis. A digest written without one keeps its existing dedupe key byte for byte, so deduplication of pre-existing digests is unaffected.Since v1.40.0
brain_analyticsacceptsview: dedup, folding the newingest_dedupcontinuity records into a trend. The counts are exact-hash drops only; the semantic layers nominate candidates and never drop, so nothing in this view may be read as a semantic discard.Since v1.41.0
brain_ingest_batch_planscopessrc_subpathby the repository's own declarations, so a scoped plan answers the same about a tree as an unscoped one. Beyond the existing typed error for a value escaping the source root, two refusals are reachable. Asrc_subpaththat crosses a submodule or nested checkout is refused with an error naming the boundary directory: those files belong to that repository, noexcludepattern can re-open a boundary, and the error says to plan against that repository directly. Asrc_subpaththat starts below a directory the repository ignores is not walked: the plan returns no batches and carries anignore_warningsentry whosesourceis--src-subpath, naming the ignored directory and theexclude!re-include that opens it.Since v1.41.0 a batch plan carries
ignore_warningswhenever the walk could not apply something as declared: a malformed pattern in one of the repository's own ignore files, an ignore file that exists but could not be read, or the--src-subpathcase above. Each entry carriessource,line,pattern, andreason;lineis 0 for a warning that concerns no single pattern line. The key is absent when there is nothing to report, so a tree that declares no ignore files serializes exactly as before. None of these warnings fails the plan - the repository is an input, not operator intent - while a malformed operatorexcludepattern still fails the call, naming the pattern and the reason it would not compile.Since v1.43.0 the four caller-named write tools --
brain_create_note,brain_update_note,brain_append_note, and the note operations ofbrain_write_batch-- name the write binding among their refusals. All four share one envelope, so all four raise it; the tool descriptions previously enumerated a refusal list it was missing from, which a caller reads as complete. The binding is declared inwrite_binding.path_prefixesinBrain/_brain.yaml; absent block, no binding, and every write path is byte-identical to before the key existed.Since v1.43.0 a refused write reports BOTH spellings of its reason, so an agent can act on the one it received.
data.codeis the surface code (write_binding,invalid_path,excluded,exists,config_invalid, ...): snake_case, the same enumeration every other refusal on the write surface uses, and what a client branches on.data.diagnostic_codeis the advisory registry spelling of the same state (write_binding->write-binding-refused,config_invalid->config-invalid), anddata.next_commandis the command that registry code resolves to (o2b vault inspect <relpath>ando2b brain doctorrespectively). A surface code with no registered advisory code carries neither key rather than a null.Since v1.43.0 a vault whose
Brain/_brain.yamlexists and does not validate refuses caller-named writes withdata.code: "config_invalid"naming the config by its VAULT-RELATIVE path (Brain/_brain.yaml) and carryingnext_command, instead of a bare fault carrying an absolute host filesystem path. It stays a JSON-RPC INTERNAL_ERROR rather than INVALID_PARAMS on purpose: no argument the caller could send would succeed, and the operator holds the fix.Since v1.43.0 a
frontmatterkey must be a key this format can read back -- a letter or underscore followed by letters, digits,_or-. The emitter escaped values but wrote keys raw, so a key containing a newline emitted extra frontmatter lines; combined withbrain_update_notemerging caller keys over existing ones and a last-wins reader, that let a caller overwrite a key it never named.template_variableskeys are unaffected: they are not frontmatter keys and never become a line of a file.Since v1.46.0
brain_intake_entitiesrequires bytes behind the source it cites. An extraction whosesourcenames no file this vault holds is committed to the quarantine lane, where it used to land its entities active. The tool's response reports the lane it actually committed in astrust(trustedoruntrusted), so a caller told only which ids it created is no longer left to guess whether it can read them back. Quarantine is one-way, which is why the shape gate that decides "could this identity be ours at all" errs toward the operator's own notes: an identity is trusted only when it is shaped like a location inside this vault AND that location holds a readable file. An unreadable file is not a verdict - only "nothing is there" answers the trust question, and a permission denial or an I/O failure is refused with the vault-relative identity and the errno code rather than quarantining the operator's own note over achmod. What this does not buy, stated plainly: a caller forced to name a real file can still name an unrelated one. This removes a free bypass - a plausible-looking string no longer suffices - and makes the claim auditable afterwards through the SHA-256 of the cited bytes recorded beside a trusted intake. It does not make the claim true, and nothing on this side of the boundary can: there is no unforgeable caller identity in the MCP surface.Since v1.46.0
brain_searchcarries aretrieval_trailkey on any answer that narrowed or came back empty:retrieved(rows handed back, whichtotaldoes not state),pool, adegradedarray over a closed vocabulary of stable identifiers, and anemptycorpus statement on a zero-result answer no degradation accounts for. Everydegraded[].detailcarries identifiers and integers only, never a provider message, a filesystem path, or the query text. Theindex-staleentry's detail carriesbehindCountwhen the index-behind census was measured for this answer - cached for at most 60 seconds and invalidated by this process's own note writes, so a write from another process can leave the count up to a minute old - the documents the next incremental run would add, update or delete, counted under the same staleness gate that fires the code; a measured zero rides too, and absent means not measured, never a zero. The response schema declares the code enum, so a build emitting a code outside the vocabulary fails its own contract rather than handing a client a value it cannot interpret. The key is absent - never null - on a healthy answer, so an existing consumer's bytes are unchanged. The vocabulary and the matching CLI surface are incli-reference.md.Since v1.46.0 every
recall_telemetryrecord carries achannelnaming the seam it came through (mcp,cli,hook). It is stamped by the emit site and is not a tool argument, so no caller can claim a channel it did not use.brain_recall_telemetrylistandsummaryaccept it as a read filter andsummaryreturns aby_channelrollup; an unrecognised value isINVALID_PARAMSnaming the accepted set. Records predating the field carry no channel and fall into no bucket - seeobservability.md.Since v1.51.0
brain_extract_signalsjoins the surface (111 total): the batch counterpart to the regex fact extractor. Called with asessionalone it is read-only and returns that session's imported USER turns plus exactly one needs-llm-step envelope; called withitemsit validates the mined signals - structurally, then against a per-session cap and a per-item confidence floor - and writes the accepted ones intoBrain/inbox/assource_type: auto_extractsignals, subject to the durability denylist and toBrain/pending/staging when write approval is on.auto_extractis a new member of the closedsource_typevocabulary, distinct fromextracted(regex) forever, because their trust profiles differ; a reader from an older build refuses a file carrying it rather than misreading it. A payload over the cap or below the floor is refused whole, naming the limit and the offending value - nothing partial lands.Since v1.51.0
brain_skill_proposalsaccepts two further operations and no new tool.page_candidatesis read-only: it gates the vault's user pages on the page-meta trio (tier: core, a non-stale lifecycle,_confidence: high) and an observed-reuse floor, skips any page an installed skill already covers, and returns one needs-llm-step envelope per admitted page plus every skipped page with the reason and the measured value behind it.page_draftvalidates the returned SKILL.md draft - structurally, then against the rule that itsnameis a legal skill directory name - and STAGES it as a pending proposal under the newmature_pagepattern kind, inside the vault. Nothing reaches the skills root untilaccept, which materializes aSKILL.mdthere through the same write-ahead journal the procedure branch uses; the journal records the absolute path it is about to write, so a rollback removes exactly that file (and the directory it created for it, only while empty). Sticky rejection applies as it does to every other pattern kind.Since v1.51.0
brain_design_notejoins the surface (112 total): the one-shot sibling of the panel lane. Called with atopicalone it is read-only - it grounds the topic in the vault's tension records, decision records and truth projections using the same deterministic token overlapbrain_decision's similar-decision lookup ranks with, and returns one needs-llm-step envelope. A store this vault holds NOTHING in is named inempty_stores; a store that holds records and matched none of them is not, because those are different facts and lead to different next moves. An empty vault is a grounded report, never a refusal. Called with anoteit validates the written note - structurally, then against the rule that EXACTLY ONE alternative setsrecommended: true- and commits it asBrain/decisions/design-<date>-<topic>.md, beside the panel outputs. Zero and two-plus recommendations are both refused and the refusal states the count found; a note for the same topic on the same day is refused rather than overwriting the first.Since v1.52.0 the three needs-llm-step lanes whose artifact is a wikilinked note -
brain_diarize,brain_design_note, and the dream pass's rollup rungs - additively carrylink_candidateson their envelope: the note basenames a[[wikilink]]in the answer can actually resolve to, with thetotalthe vault holds and atruncatedflag when the bound could not carry them all. It is built from the basename walker alone - no embedding, no vector store, no index - and a truncated manifest says so in its own schema hint rather than reading as the whole vault. The envelope spine is unchanged and the three lanes that produce no wikilinked note (brain_extract_signals,brain_skill_proposalspage drafts,brain_write_session) carry nothing new.Since v1.52.0
brain_extract_signalsis the one lane that commits SEVERAL artifacts from one validated payload - a file per mined item - so its result additively carriesreconciliation(attempted/found/missing, the missing keys named, in the wave's shared vocabulary; a deduped or gate-rejected item is a named refusal, not an attempted write). A commit that fails part way still refuses with the same accounting AND the first real error, never a partial success, and now also leaves a durable record of the partial write under<vault>/.open-second-brain/dead-letters/- the newwrite_dead_lettersstate surface - so a caller that drops the response is not the only record that it happened. Nothing in this tool removes a dead letter;o2b state statusnames the location and the records are listable from core. Single-artifact lanes are deliberately excluded: their one failure IS the response. No new tool - the surface is unchanged.Since v1.53.0
brain_context_packreads itsquerytwo ways. The default is the substring filter it has always been - a case/Unicode-insensitive match againsttopic + principle, every miss dropped with afilter-missskip - and an omittedquery_modekeeps that reading byte-identical.query_mode: "ranked"instead ORDERS the curated candidates by structural token overlap (the same deterministic, stopword-free kernel the rated-decision matcher uses - no model, no embedding, so it works on an install that has never indexed a vector) and excludes none of them, so a natural-language turn prioritises the budget rather than emptying the pack.query_moderequiresquery; the schema states the pairing asdependentRequiredand the handler refuses a mode with nothing to read. Everything the lane exists forthe containment guard, the curated preference pool, the tombstone and supersession-tip filters, owner-scope delivery,
max_tokens, the named skip reasons, and the server-issuedreceipt_id- applies unchanged in both modes. The tool also stops discarding the report'swarnings: injection-time tension warnings and the owner-scope observation now reach the caller asbrain_pre_compress_packhas always passed them, absent when empty. No new tool - the surface is unchanged.
Since v1.84.0
brain_context_packacceptspacking_policy, a strictly opt-in claim order over the two candidate populations that already share the onemax_tokensbudget: the preference-derived pages (the facts lane, the distilled layer) and the retired pages (the pages lane).facts_firstclaims for the facts lane first;retired_firsthands the first claim to the pages lane and lets the facts lane fill the exact remainder. Inside a lane the shipped comparator order holds: tier first, then recency, with query relevance joining the comparator only in therankedandsemanticmodes - the pages lane is query-ordered only there, never in the default substring mode, where the query filtered rather than ranked. The total never grows, claiming is strictly sequential with no minimum-page floor, the per-memory character cap stays lane-internal, and the synthesized attention block stays a reserved charge neither lane draws.packing_policyrequiresquery(the schema states the pairing asdependentRequiredand the handler refuses a policy with nothing to order, exactly like a mode with nothing to read); a policy against a vault where one lane holds no candidate is refused by name with INVALID_PARAMS naming the lane and the member, never a silent no-op. The response gains apackingobject - the policy name and each lane's allocation, delivered spend, candidate count and item count - only when the caller named a policy; an omittedpacking_policyruns the unified comparator pack byte-identical, adds no member, and writes no receipts or telemetry keys. The receipt and telemetry budget metadata and the prompt-prefix metric segment carry the policy under the same rule, as a closed-vocabulary member of the frozen tuple. No new tool - the surface is unchanged.Since v1.54.0 a page may RESERVE itself against remote reads, and the reservation is enforced rather than advisory. A vault page whose
visibility:frontmatter carries the reserved token is not returned, not counted, and not named in an error when the request arrived atremotereach. The other visibility tokens are unchanged: they remain opaque, caller-scoped view filters, and the caller's ownvisibilityargument still narrows exactly as before - what it can no longer do is name the reserved token and be handed the page. A vault that never wrote the token sees no change.Since v1.54.0 the reach a request arrived at is MINTED by the transport and refused when a caller supplies it. stdio mints
local, because the caller started this process and holds whatever filesystem access it runs with. An HTTP bind mints from the bind host alone: loopback islocal, every other interface isremote. The value is set after the caller's runtime options are spread, so a runtime option cannot claim a reach the bind did not establish, and aHostheader is never read for it. Atools/callcarrying a reach-shaped argument is refused with-32602, the refusal tokencaller-supplied-reach, and structureddata(tool,refused_arguments,reserved_argument_names). The check runs ahead of the unknown-argument gate and does not consult the tool's schema, so an open schema is covered too; argument names are compared case-folded and separator-free, soreach,transport_reachandtransport-reachare one argument rather than three.disclosureis deliberately not reserved - it is the unrelated result-depth mode on the recall surfaces.A write attempted while
o2b brain migrate-storemigrates this vault on this machine (the device-local migration gate is held) is refused the same way:-32602, the refusal tokenmigration_in_progress, and structureddata:code,tool,run_id,sinceandnext_command: "o2b brain migrate-store --resume"(for a migration that was interrupted). Other machines are not gated; nothing is appended to the Brain log for this refusal.Since v1.55.0 a write attempted while the vault is FROZEN (
o2b brain freeze, markerBrain/.state/frozen.json) is refused with-32602, the refusal tokenvault_frozen, and structureddata:code,tool,frozen_at,by,reason, andnext_command: "o2b brain unfreeze". The refusal is raised by the vault write guard, so it covers every tool that puts bytes in the vault - including the internal writers a tool composes - rather than a list of write tools somebody maintained. Each refused call also appends onewrite-refusedevent to the Brain log naming the tool, the caller and the marker's reason; when that append itself fails, the payload carries anaudit_reasonsaying why rather than reporting a record that does not exist. No MCP tool sets or clears the freeze: lifting one is an operator decision about a fleet an agent cannot see, sofreezeandunfreezeare CLI-only.brain_statusreports the state asfrozen: null | { frozen_at, by, device_id, reason }.Since v1.54.0 a withheld page is reported exactly as an absent one. On the key-addressed reads - a chunk id, a preference id, the templated
osb://readers - the message is byte-identical to the one an absent subject produces, because both key spaces are enumerable and a distinguishable refusal would be an existence oracle over exactly the population the boundary hides. What thelocalbypass proves is filesystem-equivalent access, or a request that originated on this host. It does NOT prove operator intent: a remote host can spawn a stdio subprocess. The alternative - no bypass at all - would take an operator's own reserved notes away from the operator's own shell.Since v1.55.0 every note write is attributable.
brain_create_note,brain_update_note,brain_append_noteand the note operations ofbrain_write_batcheach append exactly ONEnote-writelog event carrying a write id, the operation, the target path, the content hash and byte size on both sides, and the agent the server's own config names; the bytes an update or append replaced are kept content-addressed underBrain/.state/write-images/<sha256>so a recorded write can be undone from what it displaced. The bytes land BEFORE the event is appended, which is the ordering that keeps a log failure from failing a write - and the receipts say so rather than staying silent: every note-write receipt carrieswrite_id, andwrite_id: nullarrives with anaudit_reasonnaming what went wrong. A skipped create (if_exists: "skip") authored no bytes, so it carries nowrite_idat all.brain_writesreads the record back. One new tool - the surface grows by one.With the optional decision-model turn pre-filter (
extract_prefilter, seedecision-models/extract-prefilter.md), thebrain_extract_signalsplan may additively carryturns_dropped(turn ids left out of the envelope;turns_minedthen lists the kept turns),skipped: { reason: "decision_model_prefilter", turns_dropped }withllm_step: nullwhen every turn scored below the threshold (nothing to mine, distinct from the no-user-turns refusal), anddecision_model: { degraded }when a request failed and every turn was sent. With the useoffnone of these fields appears. Commit items accept an optional stringsource_turnnaming the plan turn a rule came from.Since v1.86.0
brain_extract_signalsremembers barren mines. A commit that ends with nothing written and nothing staged records a zero-yield tombstone in the session mining ledger beside the session import ledger, keyed on a SHA-256 of the mined turn material; the next plan over that session at unchanged content runs before the pre-filter and spends nothing, returningskipped: { reason: "mining_tombstone", mining_outcome: { written, staged, deduped, durability_rejected } }withllm_step: nulland emptyturns_mined- the recorded breakdown is the evidence, never a bare empty plan. A re-import that changes the session's turns changes the hash and re-opens it with no clearing step. Theforcetool argument bypasses the tombstone for exactly one plan call, for a caller that removed the downstream preference and wants the session re-proposed; a corrupt mining ledger refuses the plan by name rather than reading as empty. The commit keys its record on the session's turns as they stand at commit time. A plan that hands out an envelope records the hash of the turns it offered as a pending survey, so answering the envelope with{session, items}(an emptyitemsincluded) is all it takes: the barren commit writes its tombstone only while the session still holds exactly the surveyed turns, and a turn imported after the plan, two pending plans over different turns, or no plan since the last commit withholds it and names why inmining_ledger_failure(a yield is recorded either way). A plan whose survey could not be recorded carriessurvey_record_failure. The optionalsurveyed_turns_hashargument passes the plan's hash back as a check - a value that is not a SHA-256 hex digest (the empty string included) refuses the commit, and one the session no longer matches writes no mining record and names why. A commit that succeeds but cannot record its tombstone still succeeds and carriesmining_ledger_failureby name.Advisory decision-model fields (optional, see decision-models/dedup-tension.md and decision-models/labels.md). All of them are absent, and every output is byte-identical, while the use is off. With the
dedupuse in enforce,brain_hygiene scandedup findings andbrain_doctorentity-alias-candidatewarnings gaindecision_model: { verdict, probabilities, model, calibrated }(same | related | different); a confidentdifferentis listed last withdecision_model_low_priority: true, never hidden.brain_tensiongains the read-onlyverifyaction (slugoptional; without it every unresolved tension) returningavailable,modeorreason, and the rows with the same field (contradicts | compatible | unrelated; a confidentcompatibleorunrelatedis listed last).brain_labelsgains the read-onlysuggestoperation (path, optionaldimensions) returning per dimension{ dimension, suggestion, probabilities, confidence, current, model, calibrated },suggestion: nullin shadow, fornoneand below the confidence threshold;{ available: false, reason: "decision_model_off" }while the use is off, and a private note is refused. None of these writes to the vault, andsuggestnever assigns.Since v1.62.0 the ingest plan accounts for what it sets aside. Every
skipped_non_extractableentry carries areasonfrom a closed, typed set and adetailnaming the value behind it (the legacy free-text sentence still parses as input);skippedstays a list of path strings for unchanged files. The plan addsskip_reason_countsper reason, and extensionless regular files nothing can classify are counted in anunclassifiableobject (total,by_extension); both keys are absent while empty, so a plan with nothing set aside serializes exactly as before. Note receipts gain an optionalpath_advisorywhen authored content mentions an absolute host path - absent when the content is clean, and never present for log-line operations; the advisory never blocks the write.brain_doctorreportsorphan-session-refwarnings whosefixfield nameso2b brain orphan-repair, and an embeddings-health verdict (embeddings-backlog,embeddings-census-unrecorded,embeddings-health-unmeasured) that is fail-soft: an unrecorded census lands inuncertain, never in healthy.Since v1.63.0
brain_write_batchaccepts an optional stringrequest_id(at most 256 characters after trimming) that makes a retried batch apply once. With it, the result gainsrequest_idandreceipt:applied: this call committed the batch and recorded the ID.duplicate: the same ID with the same operations was already recorded. Every other field is the retained result of the call that committed; nothing is written, re-validated or linted.concurrent_duplicate: this call committed, but a concurrent call recorded the same ID and payload in between. Both writes landed and the fields are this call's own.payload_conflict: this call committed, but a concurrent call recorded the same ID for a different payload in between. The writes landed and this call's receipt is not recorded; areceipt_notesays so and that the batch must never be resent under a newrequest_id.
The same ID with different operations, found before any write, is refused with
INVALID_PARAMSand the idempotency ledger's mismatch message; a blank ID, one over the cap or a non-string value is refused withINVALID_PARAMSbefore any write. The receipt is recorded in the idempotency ledger after the commit, with its log paths vault-relative (a duplicate rebuilds them under the vault serving the retry), under its ownwrite_batchkey space, so a request ID never collides with abrain_feedbackorbrain_apply_evidenceidempotency_keyof the same text. The one exception is a peer still running v1.62.0, which ignores the key space: there a feedback key equal to a synced batch request ID is refused with the named idempotency mismatch error, never silently deduplicated, until that peer is upgraded. Withoutrequest_idnone of these keys appears and the payload is unchanged.Since v1.63.0
brain_searchacceptsmatch_mode:all(the default) requires every term,anymatches a document carrying any one term. Caller-typedAND/OR/NOT/NEARtokens are dropped in both modes, and ananysearch is cached apart from anallone.match_mode(all|any) anddisclosure(full|cards) are literal enums; any other value is refused withINVALID_PARAMSnaming the accepted values. Thebrain_searchdescription now ends with a when-to-search cue: when a query misses, consultbrain_recall_gatebefore widening recall.Since v1.63.0
brain_ingest_sourcewithpre_extractaddsusesedges from.tsx/.jsxsources to the components their opening tags name, and animportsseed whose relative specifier (./or../in TypeScript and JavaScript, leading-dot in Python) names exactly one ingested file carriesresolved_to, that file's vault-relative path. A specifier that climbs above the vault root, or matches no file or several, binds nothing;toalways keeps the raw specifier.Since v1.63.0 the
brain_doctorsync-conflict-logfinding also sweeps the audit week directories (Brain/log/session-lifecycle/,hygiene/,secret-custody/,watchdog/,schema-mutations/), the hook-audit and watchdog-audit directories under.open-second-brain/,Brain/skill-proposals/,Brain/preferences/and each repository directory underBrain/projects/git/; a directory it cannot list lands inuncertain. In the directories that also hold whole files, a conflict copy that is not a.jsonlshard (a proposal file, a preference note, a gitstate.json) keeps the same code with the whole-file remedy: compare it with the original, keep the right version by hand, then delete the copy. The maintenance journal's.open-second-brain/root is not swept.Since v1.64.0
brain_maintenance(run) takesforce_cost, which bypasses a positive embedding cost gate for this run's reindex and is recorded on the spend receipt when it did. The reindex task stays keyword-only unless configmaintenance_embeddingsistrue(envOPEN_SECOND_BRAIN_MAINTENANCE_EMBEDDINGS); with it, the task requests the embedding phase whenever the resolved semantic config can reach a provider, and the spend preview is computed inside the task, so a run a gate skips carries nospendblock. The result gains an additivespendblock - thebannerestimate announced before the pass, and thereceipt(model, tokens,estimated_usd,forced) the completed pass priced, also carried on its task row and journaled on themaintenance_spendmetrics surface. A run killed mid-spend receipts nothing; a pass killed at its safeguard deadline is atimed_outrow.brain_hygiene'sdetectorsenum widens toslug-collisions(default-on; same-stem allocation residue in one directory) andtags(opt-in; inline body tags only - malformed, inconsistent and orphan findings, never frontmatter tags arrays), and the default is the sweep of every registered detector except the opt-in ones. Thelintkey on note-write receipts can carrynear-duplicatefindings: an authored body at 0.8 Jaccard or above against an existing page in the same directory and scope bucket, advisory like every lint finding.brain_doctorreportsmerge-chain-danglingwarnings for amerged_into:pointer whose canonical resolves to no file - reporting only, with the repair named as the content judgement it is.Since v1.89.0
brain_maintenance(run) also dispatches the lane's fifth built-in task,relink, which recomputes the entity anchors of the page-sourced signals already stored with the shipped matchers and the page-link resolver. The task previews by contract: it reports per source page what it would add - the result carries that report as an additiverelinkblock whenever the task ran, on the CLI JSON payload and here alike - and it rewrites the storedentities:lines only when the call passesrelink_apply(--relink-applyon the CLI), which routes the run through the same write guard every lane write answers to. A dry run writes nothing, including the skip-audit log the shared anchorable build stamps on the apply arm; a signal without a Raw section is counted by neither arm.Since v1.65.0
brain_maintenance(run) also runs the custom lane tasks an install declares in its machine config (maintenance_custom_<name>, behind the default-offmaintenance_custom_tasksswitch), after the four built-ins and under the same gates; they appear ascustom:<name>task rows.retry_tasksacceptscustom:<name>for a declared task, refuses an undeclared one by name with the registered list, and refuses a list longer than the tasks this install registered; the schemamaxItemsis the built-ins plus the custom-task cap of 8. A refused declaration comes back incustom_task_errorswhile the valid tasks still run. No argument can add, edit or read a command: commands live only in the machine config, never in the vault. Once the switch is on, a caller withforce: trueandretry_taskscan run the declared commands whenever it calls, past the window and busy gates; leave the switch off on a server whose callers should not. A task runs from the home directory unless it declares a_cwd, without the variables whose names declare a credential, and its background processes are killed with its process group at its timeout or when the server exits. A duplicatedretry_tasksentry counts once, and thestatusnotice names the env override when that turned the tasks off.Since v1.67.0
brain_distill_sourcechecks quoted spans in claims against the cited block or the source, unquotes a span that fails and reports it underquotes, refuses the write withquote_unverifiedunderstrict_quotes: true, accepts a boundedexcerptfor aurl-onlysource, and always returnscapture_scope(see "Source distillation").brain_ingest_sourcereturnscapture_scope;brain_intake_entitiesandbrain_ingest_sourcetreat a cited vault file the caller cannot read at its reach as an absent source (untrusted lane, nosource_content_hash,url-only);brain_research_reportreturnscapture_scopes, one per entry ofsourcesin the same order, and stamps acapture_scopesfrontmatter list when any source is notfull-local; a source written without an extension is matched against its.mdnote, and a source backed by a vault file the caller cannot read at its reach is reportedurl-only, the same answer as an absent file; a cited file the filesystem refuses to stat never aborts the report and readsurl-only; a title holding a line break is refused.brain_hygienegains the default-oncapture-scopedetector: a retrievable page of active knowledge whose every cited source is currentlyurl-onlygets awarningfinding withproposed_action: review. The scope is re-derived from each source identity at scan time, so a page whose local source was deleted reports asurl-only, and abounded-localpage counts as backed only while its excerpt still matchesexcerpt_hash. Quarantined pages (those carrying the untrusted marker) are skipped; the marker already names the condition. A cited file the caller cannot read at its reach counts asurl-onlyfor that caller, in the scan and in thebrain_statushygiene count. Below local reach the scan'slink_integrityreportsmeasured: falsewith reasonreach, because its counts are taken over the whole index.brain_ingest_batch_planplans a file the caller cannot read at its reach as an absent one (visibility is a rule over Markdown pages, so the plan'sunclassifiablecounts of other files are the same at every reach), andbrain_ingest_sourcerecords a source in the content manifest only in the trusted lane.Since v1.68.0 more readers treat a record the caller cannot read at its reach as absent.
brain_context, theosb://preferences/activeresource andbrain_pre_compress_packhand a remote caller the active digest without such a preference or retired record: its line, its count and its most-applied entry go, and the pack's top-K walk skips it. A remote caller always gets that render, stamped with the file'sgenerated_at; a local caller with no owner scope still gets the file's own bytes.brain_briefview="digest", theosb://digest/latestresource and theosb://lessonsresource do the same for the activity and lessons digests: such a record, its rows, its counts, the log events naming it and their share of the agent summary go, and a remote digest neither takes a report snapshot nor shows its delta.brain_briefview="morning"leaves such a preference out of its list, and below local reach it shows no pending trigger and marks none delivered. The resource also follows the owner gate now, asbrain_contextdoes, so underintegrity.owner_scope_delivery: failit answers with the caller's own view.brain_healthdrops a finding any of whose members the caller cannot read, andbrain_doctorwithrepairderives its plan only from findings the caller can read: such a record is neither planned, counted underunfixable, nor written byapply. The generic page readers (brain_searchand every other tool that reads a page by path) no longer return the compiled digest pagesBrain/active.mdandBrain/lessons.mdto a remote caller, because those pages compile records that may be reserved; a remote client reads the active digest through the three readers above and the lessons digest throughosb://lessons.brain_ingest_sourcewithpre_extractreads Terraform (.tf,.tfvars) and redacts credentials in import specifiers (seeo2b brain pre-extractincli-reference.md).Since v1.69.0 ingest reads more than Markdown. One format registry maps a file's extension to a format: Markdown and the lightweight markups (
.md,.markdown,.txt,.text,.rst,.org) are read as they are, CSV, TSV and HTML (.csv,.tsv,.html,.htm) are extracted, and PDF, the Office formats (docx,xlsx,pptx,odt,ods,odp), EPUB, RTF and images are named but not extracted.brain_ingest_batch_planplans CSV, TSV and HTML files by default, and a planned file whose format is not text carriesformatbesidestatus. A file of a named format that is not extracted is listed inskipped_non_extractablewith reasonformat-not-extractableand the format asdetail(before the schema skips, and counted inskip_reason_counts) instead of adding tounclassifiable; anextensionsoverride that lists its extension still plans it.brain_ingest_sourcederives a section from an HTML, CSV or TSV file in the vault, by format and with no new argument, in the trusted lane and only from a file the caller can read at its reach. The page gainssource_formatandsource_content_hash, and the derived section comes after## Connections to existing notes. For HTML,## Partsholds one fenced block with one line per heading,h<level> <trail> | lines <a>-<b>(the trail joins the ancestor headings with>, the line span is 1-based in the extracted text), never body text: at most 256 parts, each heading capped at 200 characters; the result gainsparts({extracted: true, count, omitted?}or{extracted: false, reason}). For CSV and TSV,## Tableholds fenced plain-text row groups under### Rows <a>-<b>. The first record is always the header, and every group repeats it, so each search chunk of the page carries it, except the later pieces of a single row longer than a group's token budget, which can still be split across chunks; a group holds at most 50 rows and 600 tokens. A.csvfile is read as semicolon-delimited when its first record has at least two fields on semicolons and more than on commas. Cells are joined by|; inside a cell a backslash,|, a backtick, a line feed, a carriage return and a tab are written as\\,\|,\`,\n,\rand\t, and every other control character (C0, DEL and C1) as\u{XXXX}with its code point in four uppercase hexadecimal digits, so no cell can end the fence or reach a terminal raw. The caps are 1,000 rows, 64 columns, 256 characters per cell and 256 KiB per section, and each cut is named intable_truncated; only the first 4,096 code units of a cell are read for redaction, and a longer cell counts as cut. A<private>region is hidden before the records are split, so a region spanning records hides every row between its tags. Each HTML heading is redacted (key=valuecredentials and URL userinfo) before it is capped, only its first 4,096 code units are read for that (a longer heading ends in…), and in the parts list a backslash in a heading is written as\\and a|as\|. The page gainstable_delimiter,table_columns,table_rows,table_rows_renderedandtable_truncated(absent when nothing was cut); the result gainstable({rendered: true, format, delimiter, columns, rows, rows_rendered, truncated?, redacted_cells}or{rendered: false, format, reason, detail?}, with reasonssource-not-local,not-utf8,contains-nul,malformed-quotingandempty). Every value in a column whose header names a credential is replaced, and every other cell goes through the output redactor with URL credentials included;redacted_cellscounts both. A credential no pass recognises stays on the indexed page and in its embeddings, as it already sits in the file in the vault. A summary page that carries a derived section is at most as visible as its source: it takes on the source's ownvisibilitytokens; when the page already had avisibility, it gets the audience both the operator and the source allow; when they share none, the page is withheld below local reach. A leading frontmatter block in an HTML, CSV or TSV source is not extracted, so it is never read as table data or text. A URL source, an absent file and a file the caller cannot read at its reach all answersource-not-local, with no digest and no section. A Markdown or text source has nopartsand notablekey and its page is unchanged. An operator-setvisibilityon a summary page is now kept when its source is ingested again.brain_recall_gateaccepts an optionalturn_id(a string of at most 512 characters), recorded on thegate_telemetryrecord, and refuses aturn_idorsession_idover 512 characters or atelemetry_hostover 200 withINVALID_PARAMSbefore it runs, with telemetry on or off. The recall verdict's root coverage and the ids of the daily and weeklybrain_briefviews answer at the caller's reach: below local reach a root counts as reached only through a page the caller can read, and the daily and weekly views leave out the status transitions, retirements and contradictions that name a record the caller cannot read (under itspref-or itsret-id), withhold a source pointer when every evidence event citing it is about such a record, or when the pointer names a page the caller cannot read, recompute theevents_by_kindandvault_deltacounts from the events the caller may see, and take no report snapshot and show nodelta. Since v1.70.0 the today, monthly and operator views answer at the caller's reach too; only the operator view'sdream_summarycounts, and the dream warnings its trust verdict folds in, still count over the whole Brain layer, and they name no record. The read-only preview of the same extraction is the CLI verbo2b brain extract(seecli-reference.md).Since v1.69.0 more readers treat a page or record the caller cannot read at its reach as absent; a local caller and the CLI see no change. Below local reach the daily log pages (
Brain/log/, the JSONL sidecars included) are not served by the generic page readers (brain_searchand every other tool that reads a page by path).brain_analyticsview="timeline",view="belief_evolution"andview="concept_synthesis",brain_briefview="today",view="digest"andbrain_event_trace,brain_claims(current and history),brain_backlinksandosb://backlinks/{id},osb://log/{date},osb://topic/{topic},osb://preference/{id},brain_querywithsince,topicorpreference,brain_moc_auditand everybrain_doctorfinding answer at the caller's reach: a record is judged under each id it answers to (pref-andret-), a log entry naming such a record is dropped before any limit or total, and a dream shared with a readable record is kept with its readable transitions only.osb://log/{date}re-renders the day from the kept entries. A refused preference or hub answers as an absent one, and a refused MOC member is neither listed nor sized. Thebrain_doctorfindingsorphan-evidenceandmalformed-evidence-rangenow carrysources(the preference the evidence is about) andremoved-tool-referencecarriespath(vault-relative on the wire), so each names its record structurally; the messages are unchanged.brain_search_by_sourceandbrain_delete_by_sourcehonour the reach of a derived summary page: such a page, and the content-manifest entry of a source the caller cannot read, are left out of the answer and left in place. Abrain_ingest_sourcebelow local reach leaves a summary page the caller cannot read untouched and answers as a first ingest would. An entity intake of a source that is reserved against remote reads records nosource_content_hash. A byte-order mark before a frontmatter block no longer hides that block, so itsvisibilityapplies to the page. The empty-search coverage verdict no longer counts a root as reached through a page the caller cannot read. Some counts still run over the whole Brain layer and name no record: the operator view'sdream_summarycounts (and the dream warnings its trust verdict folds in) and the status views; the ranking statistics ofbrain_searchare taken over the shared index.Since v1.70.0
brain_contextrenders the operator's scoped rules for a local caller. After the standing-rules block, and before the memory body,contentcarries a## Scoped operator rulesblock built fromBrain/standing-rules/project/<key>.md,Brain/standing-rules/harness/<harness id>.mdandBrain/standing-rules/host/<device id>.md, the files whose key matches the scope the server resolved: the project from the nearest.o2b-vault.jsonpointer above the directory the server was started in, the harness from--harness(falling back to--host-target) and the host from the device id. The result then carries an optionalscoped_ruleskey, present only when at least one file matched or the block carries the notice that host-scoped rules were not applied:{ "scope": { "project": "proj-x", "harness": "claude-code", "host": null }, "files": [ { "path": "Brain/standing-rules/project/proj-x.md", "axis": "project", "truncated": false } ] }pathis vault-relative,axisisproject,harnessorhost, andtruncatedistruefor a file theactive.scoped_rules_max_charscap cut;brain_contexthas no injection budget, so the cap applies as configured. The tool renders the notice that host-scoped rules were not applied, withscope.host: null, when the device id cannot be resolved but the call can still be answered, for example when no device id is stored yet and the configuration directory cannot be written; an unreadable configuration file, or a config home that cannot be created, fails the whole call first. The tool gains no input argument. Below local reach neither the block nor the key appears: the output is the same as for a vault without the directory. Every write tool refuses a path insideBrain/standing-rules/, as it refusesBrain/standing-rules.md. See "Scoped operator rules" inhow-it-works.md.Since v1.70.0 more brief and doctor counts answer at the caller's reach; a local caller and the CLI see no change. Below local reach
brain_briefview="today"lists no open loop or obligation from a page the caller cannot read and counts only the files it may read inscannedFiles;view="monthly"counts events, status transitions, retirements, contradictions and neglected areas from the events the caller may see, the same rule as the daily and weekly views;view="operator"takes its doctor counts from the findings the caller may see, its digest counts from readable pages, its top actions from readable targets before the top entries are picked, its verification entries and their counts from the records and pages the caller may read, and recomputes its trust verdict from those; andbrain_doctorfills the cap onremoved-tool-referencewarnings and the per-code cap of itsuncertainstream from readable pages, counts the stale-dependency note'sstates_changedand each stale row's consumers from the pages and records the caller can read (a withheld page is neither a state nor a consumer), runs its concept-gap and contradiction detectors over readable preferences and signals only, and leaves out aninstruction_file_warningsentry for a vault-root instruction file the caller cannot read; the operator view'sinstruction_file_warningsfollow the same rule.Since v1.70.0 more readers answer at the caller's reach; a local caller and the CLI see no change. Below local reach
brain_obligationlists, shows, completes and removes an obligation page the caller cannot read exactly as an absent one (showanswerspresent: false,doneandremoverefuse withno obligation),removeleavesarchive_pathout of its answer, andaddstill refuses a slug whose page exists.brain_intentiontreats a chain the caller cannot read the same way (listleaves it out,showanswerspresent: false,moverefuses withno active intention),moveleavesarchive_pathout, andsetrefuses a scope whose withheld chain exists instead of folding it into a new version.brain_healthcomputesconcept_gaps, thesuppressedcounts and the verdict over the preferences and signals the caller can read, so a term only withheld principles carry is not reported.brain_triggeroperation="scan"builds its candidates from readable records only, socandidatescounts none the caller cannot read and the scan writes no trigger about such a record; every trigger row and transition treats a trigger naming such a record as absent.brain_stale_scan,brain_review_candidatesandbrain_retentionlist no preference or signal the caller cannot read, andbrain_retentioncounts itssummaryover the rows it returns.brain_review_candidatesplans its dry run over the signals, preferences and retired records the caller can read, so itsclusters_below_thresholdandintent_reviewsfold no withheld signal, andbrain_intent_reviewfolds the same readable records.Since v1.70.0
brain_context_receiptsanswers at the caller's reach for the operator rules. Below local reach a SessionStart injection receipt leaves out thestanding-rulesandscoped-rulesitems, their source references and every figure that counts or measures them (the item count, the whole-text hash and lengths, the total bytes and tokens,scoped_rules_charsandbudgeted_source_count), andsummaryleaves them out of its item totals, counting a receipt left with no item inempty_receipts. A measured injection receipt whose every item was an operator-rule block (a session with noactive.mdbody) is left out oflistandsummarybefore their limit and fold bound, andshowanswers it withreceipt not found; thebudgetblock is dropped from a receipt that has no budgeted body left. A stored receipt is otherwise returned as recorded, with no visibility check at read time.Since v1.70.0
brain_doctorwithrepairruns the checks behind its plan over the pages and records the caller can read, so below local reach theunfixablecounts (the removed-tool cap, the concept gaps) are the counts a vault without the withheld pages gives.Since v1.70.0
brain_tension,brain_lifecycleandbrain_expireanswer at the caller's reach; a local caller and the CLI see no change. Below local reachbrain_tensiondetectreads only the notes the caller may read (scanned_filescounts those) and pairs nothing from the others; a persisted tension page carries the strictervisibilityof its two source notes, andlist,verifyandshow,confirm,dismissandresolvetreat a tension page the caller cannot read as absent (no tension: <slug>).brain_lifecycletombstone,supersedeandtemporal-replace, andbrain_expire, refuse a page or record the caller cannot read with the error a missing one gets (note does not exist: <path>,no signal or preference with id), before anything is written;brain_lifecyclecuratorleaves out a row whose key names such a page or record.Since v1.70.0 the writers that name a preference, a premise, a decision, a label target, a stub source or a chain id answer at the caller's reach; a local caller and the CLI see no change. Below local reach a page or record the caller cannot read is treated as absent before anything is written:
brain_apply_evidenceand theapply_evidenceoperation ofbrain_write_batchrefuse it as a missing preference,brain_derive_factas a missing premise,brain_decisionshow,outcomeandratewithno decision: <slug>(andlist,compare,history,recallandsimilarleave it out),brain_labelsassignandremovewithnote does not exist,brain_scaffold_stubas an unknown source (andlistleaves it out), andbrain_lifecycletipreads it as an unknown id. Thebrain_feedbackconflict and routing hints score only readable preferences and signals, andbrain_design_notegrounds only on readable tensions and decisions.Since v1.70.0
brain_dreamserves only a dry run below local reach, planned over the records the caller can read; a real pass, a single step and the staged lifecycle are refused, andbrain_maintenancerunis refused too (itsstatusstill answers). A preference the dream pass drafts carries the strictestvisibilityof the signals it is drafted from and of the record it supersedes or rebuts, at every reach.Since v1.70.0
brain_dead_endslist,brain_diarize,brain_hygienemode="refresh"andbrain_anticipatory_contextanswer at the caller's reach. Below local reachbrain_dead_endslists only readable dead ends andrecordleaves out the archived ids;brain_diarizeanswers a subject page the caller cannot read as an unknown entity and takes no evidence from a source page it cannot read;refreshplans, re-derives and archives only readable derived pages; andbrain_anticipatory_contextbuilds its bundle for the caller without reading or writing the shared cache, answeringcache_state: "miss".Since v1.72.0
brain_context_packacceptsquery_mode: "semantic", which orders the curated belief notes by the stored vectors of their chunks against one embedding ofquery(it still requiresquery). The response gains asemanticobject:model,price_source,query_tokens,estimated_usd(null when the price is unknown),scoredandunembedded(kept candidates with no usable vector, which sort after the scored ones within the same tier, then by recency; tier still decides first). The counts are taken after the reach filter, so a withheld page appears in neither. The mode refuses with a stableerror.data.code: the semantic capability codes (EMBEDDING_DISABLED,EMBEDDING_KEY_MISSING) for a blocked tier,VEC_EXTENSION_UNAVAILABLE, andBELIEF_VECTORS_MISSINGwhen no kept candidate has a usable vector, namingo2b search vector-backfill --path Brain/preferences/ --path Brain/retired/ --apply. The query embed is one paid call per request, disclosed in the response, and refused withEMBEDDING_COST_UNPRICEDfor a remote caller when the model has no known price andembedding_cost_gate_usdis positive.queryis capped at 2000 characters in every query mode, counted in Unicode code points as the schema'smaxLengthcounts them, andbrain_searchcounts its 2000-character cap the same way.brain_recall_feedback, which advertised the same cap without checking it, now refuses a longerquerywithINVALID_PARAMSbefore it records anything. Thesubstringandrankedmodes are otherwise unchanged. The embedding spend surfaces also change:EMBEDDING_COST_UNPRICEDjoins the stable error codes (an embedding run refused under a positive cost gate because the model has no known price),brain_maintenancespend receipts carryprice_sourcewith a nullestimated_usdfor an unknown price. No new tool.Since v1.73.0 every paid query embed passes one gate. Under a positive
embedding_cost_gate_usd, a caller that is not local is refused the query embed of a model with no known price before any provider call:brain_searchwith an explicit semantic request fails withEMBEDDING_COST_UNPRICED, and a hybrid search (also insidebrain_recall_feedback,brain_file_context,brain_eval,brain_benchmarkandbrain_tune) falls back to keyword-only withsemantic-cost-unpricedin its retrieval trail. A query longer than the effective embedding input window (embedding_input_window_tokens, then the curated model table, then unknown, which cuts nothing) is cut and recordssemantic-query-truncated; when the instruction prefix alone fills the window, an explicit semantic search and thebrain_context_packsemantic belief order refuse withINVALID_INPUT, and a hybrid search falls back to keyword-only withsemantic-query-empty-fit.brain_context_packresolves an omitted reach to remote like every other reader, and itssemantic.query_tokenscounts the text actually sent, instruction prefix included.brain_recall_feedbackreturns an additivedegradedarray with the re-run's degradation codes (empty when nothing narrowed it), and a feedback event whose re-run lost the semantic lane records zero layer contributions, so the learned weights do not learn from a keyword-only answer.brain_benchmark,brain_evalandbrain_tunereports carry adegradedunion (and per query or per grid row) when a run measured a smaller system than the configured one, andbrain_tunerefuses to save a winner measured with the semantic lane missing:EMBEDDING_COST_UNPRICEDfor the cost gate,EMBEDDING_KEY_MISSINGorEMBEDDING_DISABLEDfor a blocked tier,EMBEDDING_PROVIDER_HTTPwhen the provider did not answer, and the sameEMBEDDING_PROVIDER_HTTPfor any other stop that left the hybrid caller keyword-only (the composite deadline, an empty query vector, an empty fit recorded assemantic-query-empty-fit), with a remedy namingsearch_hybrid_deadline_msor the input window. Each refusal names the remedy.Since v1.74.0 a context-driven retire (
superseded-by-context,rebutted,quarantine-violated,user-rejected) can name the active preferences that resemble it. With the config keynear_duplicate_retire_siblings_enabled(OPEN_SECOND_BRAIN_NEAR_DUPLICATE_RETIRE_SIBLINGS_ENABLED, default off) set to"true",brain_review_candidatesgains an additiveretire_siblingsarray of{ retiring_id, sibling_id, score, method }pairs, the dream run summary - the CLIo2b brain dream --jsonand thebrain_dreamtool response alike - carries the lexical pairs under the same key, ando2b brain rejectlists the siblings of the rejected preference with the command that rejects each (--jsonaddsretire_siblings). Decay retires and merges nominate nothing, a retire the confirmed-evidence gate holds back nominates nothing, and nothing is retired by the list: each pair is accepted through an existing verb (brain_apply_evidencewithresult: outdated, oro2b brain reject). Lexical pairs score at least 0.7 Jaccard on the principle. With a search config,brain_review_candidatesalso compares the stored vectors of the retiring and sibling preferences, at zero embedding spend, and adds pairs at 0.92 cosine or above asmethod: "embedding";retire_siblings_semanticreports that tier's outcome (used,index_missing,vec_unavailable,not_embedded,model_mismatch,index_unavailable- the index exists but could not be opened or read, in which case the lexical pairs still come back and the search error code lands inretire_siblings_semantic_detail, a field that appears only for that outcome and never carries a message or a path). A pair is kept only when the caller may read both preferences, and the stored-vector tier probes only the retireswould_retirekeeps, so neither field reflects a withheld page.brain_extract_signalsnow carries each mined turn's storedtimestampon itsturns_minedentries (""when the source has none) and in the prompt lines; the envelope asks for a time bound as an ISO 8601 date or interval resolved against the stating turn's timestamp, which the dream pass turns intovalid_from/valid_until. A payload in which two items share atopicis refused whole, naming both items. No new tool.Since v1.91.0
brain_pledgeandbrain_intake_pledgesanswer at the caller's reach: below local reach a withheld pledge page answersshowas absent (present: false), drops fromlist, andfulfill/removerefuse with the sameno pledgeerror an absent slug gets, with the archive name left out of a withheldremoveanswer. An intake whosesourcenames a page the caller cannot read is refused whole, before any write. The family inherits the standing limitbrain_context_packalready carries: a readable page may name a private page, and no per-page gate rewrites prose - a stored pledge cites bytes its source contained, not a privacy guarantee over every name the source spelled.Since v1.95.0
brain_statuscarries astoregroup stating what the snapshot can honestly measure about the memory store: the search writer lock's three-state read (held/free/unknown- a state the probe could not take readsunknownwith a reason, neverfree), the held-lock age in milliseconds, the lock path, andthisCaller: "read-only"; thewriterLockOwnermember isnullwhile a lock is held, because the lock record carries no identity, and that null is a measurement, not an omission. A held lock is informational and never flips the snapshot unhealthy. Below local reach the lock PATH is withheld (a resolved host path may sit outside the vault) while every state field stays.Since v1.95.0 the receipt join and the new gates add no MCP tool of their own by design:
brain_context_receiptsshowgains thereply_presencejoin on its existing tool (see the table above), the dead-letter exit (o2b brain dead-letterslist / show / recover) stays CLI-only, and a hook-less agent that needs a commit-blocking validation gate runso2b brain validate --paths <page>... | --dirty [--strict]- the same write-receipt lint the write path enforces, with the doctor's exit contract (errors 1,--strictwarnings 2, clean 0) as the product.
512 files · 486 not served here
- .agents/plugins/marketplace.json (not served here)
- .ai-notes/.gitignore (not served here)
- .apb/.gitignore (not served here)
- .apb/config.yaml (not served here)
- .apb/playbooks/osb-feature-release/1.1.0/playbook.yaml (not served here)
- .apb/playbooks/osb-feature-release/1.2.0/playbook.yaml (not served here)
- .apb/playbooks/osb-feature-release/1.3.0/playbook.yaml (not served here)
- .apb/playbooks/osb-feature-release/1.4.0/playbook.yaml (not served here)
- .apb/playbooks/osb-feature-release/current (not served here)
- .apb/playbooks/osb-feature-release/lifecycle (not served here)
- .apb/playbooks/osb-feature-release/meta/1.1.0.yaml (not served here)
- .apb/playbooks/osb-feature-release/meta/1.2.0.yaml (not served here)
- .apb/playbooks/osb-feature-release/meta/1.3.0.yaml (not served here)
- .apb/playbooks/osb-pr-prepare/1.0.0/playbook.yaml (not served here)
- .apb/playbooks/osb-pr-prepare/1.0.0/scripts/arch-gate.sh (not served here)
- .apb/playbooks/osb-pr-prepare/1.0.0/scripts/check-context.sh (not served here)
- .apb/playbooks/osb-pr-prepare/1.0.0/scripts/check-pr.sh (not served here)
- .apb/playbooks/osb-pr-prepare/1.0.0/scripts/check-release.sh (not served here)
- .apb/playbooks/osb-pr-prepare/1.0.0/scripts/ci-wait.sh (not served here)
- .apb/playbooks/osb-pr-prepare/1.0.0/scripts/lib.sh (not served here)
- .apb/playbooks/osb-pr-prepare/1.0.0/scripts/qa-gates.sh (not served here)
- .apb/playbooks/osb-pr-prepare/1.0.0/scripts/zg-index.sh (not served here)
- .apb/playbooks/osb-pr-prepare/1.1.0/playbook.yaml (not served here)
- .apb/playbooks/osb-pr-prepare/1.1.0/scripts/arch-gate.sh (not served here)
- .apb/playbooks/osb-pr-prepare/1.1.0/scripts/check-context.sh (not served here)
- .apb/playbooks/osb-pr-prepare/1.1.0/scripts/check-pr.sh (not served here)
- .apb/playbooks/osb-pr-prepare/1.1.0/scripts/check-release.sh (not served here)
- .apb/playbooks/osb-pr-prepare/1.1.0/scripts/ci-wait.sh (not served here)
- .apb/playbooks/osb-pr-prepare/1.1.0/scripts/final-gates.sh (not served here)
- .apb/playbooks/osb-pr-prepare/1.1.0/scripts/lib.sh (not served here)
- .apb/playbooks/osb-pr-prepare/1.1.0/scripts/qa-gates.sh (not served here)
- .apb/playbooks/osb-pr-prepare/1.1.0/scripts/zg-index.sh (not served here)
- .apb/playbooks/osb-pr-prepare/1.2.0/playbook.yaml (not served here)
- .apb/playbooks/osb-pr-prepare/1.2.0/scripts/arch-gate.sh (not served here)
- .apb/playbooks/osb-pr-prepare/1.2.0/scripts/check-context.sh (not served here)
- .apb/playbooks/osb-pr-prepare/1.2.0/scripts/check-pr.sh (not served here)
- .apb/playbooks/osb-pr-prepare/1.2.0/scripts/check-release.sh (not served here)
- .apb/playbooks/osb-pr-prepare/1.2.0/scripts/ci-wait.sh (not served here)
- .apb/playbooks/osb-pr-prepare/1.2.0/scripts/final-gates.sh (not served here)
- .apb/playbooks/osb-pr-prepare/1.2.0/scripts/lib.sh (not served here)
- .apb/playbooks/osb-pr-prepare/1.2.0/scripts/qa-gates.sh (not served here)
- .apb/playbooks/osb-pr-prepare/1.2.0/scripts/zg-index.sh (not served here)
- .apb/playbooks/osb-pr-prepare/1.4.0/playbook.yaml (not served here)
- .apb/playbooks/osb-pr-prepare/1.4.0/scripts/arch-gate.sh (not served here)
- .apb/playbooks/osb-pr-prepare/1.4.0/scripts/check-context.sh (not served here)
- .apb/playbooks/osb-pr-prepare/1.4.0/scripts/check-pr.sh (not served here)
- .apb/playbooks/osb-pr-prepare/1.4.0/scripts/check-release.sh (not served here)
- .apb/playbooks/osb-pr-prepare/1.4.0/scripts/ci-wait.sh (not served here)
- .apb/playbooks/osb-pr-prepare/1.4.0/scripts/cycle-run.sh (not served here)
- .apb/playbooks/osb-pr-prepare/1.4.0/scripts/final-gates.sh (not served here)
- .apb/playbooks/osb-pr-prepare/1.4.0/scripts/lib.sh (not served here)
- .apb/playbooks/osb-pr-prepare/1.4.0/scripts/qa-gates.sh (not served here)
- .apb/playbooks/osb-pr-prepare/1.4.0/scripts/zg-index.sh (not served here)
- .apb/playbooks/osb-pr-prepare/current (not served here)
- .apb/playbooks/osb-pr-prepare/lifecycle (not served here)
- .apb/playbooks/osb-pr-prepare/meta/1.0.0.yaml (not served here)
- .apb/playbooks/osb-pr-prepare/meta/1.1.0.yaml (not served here)
- .apb/playbooks/osb-pr-prepare/meta/1.4.0.yaml (not served here)
- .apb/playbooks/osb-release/1.0.0/playbook.yaml (not served here)
- .apb/playbooks/osb-release/1.0.0/scripts/discard.sh (not served here)
- .apb/playbooks/osb-release/1.0.0/scripts/lib.sh (not served here)
- .apb/playbooks/osb-release/1.0.0/scripts/publish.sh (not served here)
- .apb/playbooks/osb-release/1.0.0/scripts/release-image-style.md (not served here)
- .apb/playbooks/osb-release/1.0.0/scripts/render.sh (not served here)
- .apb/playbooks/osb-release/1.0.0/scripts/render.ts (not served here)
- .apb/playbooks/osb-release/1.0.0/scripts/scope.sh (not served here)
- .apb/playbooks/osb-release/1.0.0/scripts/stage.sh (not served here)
- .apb/playbooks/osb-release/current (not served here)
- .apb/playbooks/osb-release/lifecycle (not served here)
- .apb/playbooks/osb-release/meta/1.0.0.yaml (not served here)
- .apb/playbooks/osb-triage-validator/1.2.0/playbook.yaml (not served here)
- .apb/playbooks/osb-triage-validator/1.3.0/playbook.yaml (not served here)
- .apb/playbooks/osb-triage-validator/current (not served here)
- .apb/playbooks/osb-triage-validator/lifecycle (not served here)
- .apb/playbooks/osb-triage-validator/meta/1.2.0.yaml (not served here)
- .apb/playbooks/osb-triage-validator/meta/1.3.0.yaml (not served here)
- .apb/playbooks/osb-upstream-watch/1.0.0/playbook.yaml (not served here)
- .apb/playbooks/osb-upstream-watch/1.0.0/scripts/materialize-collector.sh (not served here)
- .apb/playbooks/osb-upstream-watch/current (not served here)
- .apb/playbooks/osb-upstream-watch/lifecycle (not served here)
- .apb/playbooks/osb-upstream-watch/meta/1.0.0.yaml (not served here)
- .apb/profiles/osb-collector/SOUL.md (not served here)
- .apb/profiles/osb-collector/profile.yaml (not served here)
- .apb/profiles/osb-developer/SOUL.md (not served here)
- .apb/profiles/osb-developer/profile.yaml (not served here)
- .apb/profiles/osb-reviewer/SOUL.md (not served here)
- .apb/profiles/osb-reviewer/profile.yaml (not served here)
- .apb/profiles/osb-validator/SOUL.md (not served here)
- .apb/profiles/osb-validator/profile.yaml (not served here)
- .claude-plugin/marketplace.json (not served here)
- .claude-plugin/plugin.json
- .codex-plugin/plugin.json (not served here)
- .codexignore (not served here)
- .gitattributes (not served here)
- .githooks/pre-commit (not served here)
- .githooks/pre-push (not served here)
- .github/dependabot.yml (not served here)
- .github/scripts/ci-scope.sh (not served here)
- .github/scripts/windows-timings.sh (not served here)
- .github/windows-test-timings.json (not served here)
- .github/workflows/ci.yml (not served here)
- .github/workflows/plugin-scanner.yml (not served here)
- .github/workflows/release.yml (not served here)
- .gitignore (not served here)
- .oxfmtrc.json (not served here)
- ATTRIBUTION.md
- AUTOGINEER-RUNS.md
- CHANGELOG.md
- CLAUDE.md
- LICENSE
- README.md
- SECURITY.md
- __init__.py (not served here)
- after-install.md
- bin/o2b-discipline-report (not served here)
- bun.lock (not served here)
- bunfig.toml (not served here)
- cli.py (not served here)
- docs/architecture.md
- docs/brain-store.md
- docs/brainstorm/a-label-is-not-a-boundary/cli-output/claude.md (not served here)
- docs/brainstorm/a-label-is-not-a-boundary/cli-output/prompt.md (not served here)
- docs/brainstorm/a-label-is-not-a-boundary/design.md (not served here)
- docs/brainstorm/a-label-is-not-a-boundary/plan.md (not served here)
- docs/brainstorm/a-label-is-not-a-boundary/recon/census-lexer.md (not served here)
- docs/brainstorm/a-label-is-not-a-boundary/recon/conventions-and-release-machinery.md (not served here)
- docs/brainstorm/a-label-is-not-a-boundary/recon/egress-boundary.md (not served here)
- docs/brainstorm/a-label-is-not-a-boundary/recon/fleet-and-shared-scope.md (not served here)
- docs/brainstorm/a-label-is-not-a-boundary/recon/identity-and-attribution.md (not served here)
- docs/brainstorm/a-label-is-not-a-boundary/recon/mcp-instruction-surface.md (not served here)
- docs/brainstorm/a-label-is-not-a-boundary/recon/owner-scope-isolation.md (not served here)
- docs/brainstorm/a-label-is-not-a-boundary/recon/retrieval-confidence-floor.md (not served here)
- docs/brainstorm/a-label-is-not-a-boundary/variants.md (not served here)
- docs/brainstorm/actionable-now-batch/cli-output/consultant.md (not served here)
- docs/brainstorm/actionable-now-batch/cli-output/prompt.md (not served here)
- docs/brainstorm/actionable-now-batch/design.md (not served here)
- docs/brainstorm/actionable-now-batch/plan.md (not served here)
- docs/brainstorm/actionable-now-batch/variants.md (not served here)
- docs/brainstorm/agent-boundary-control-surfaces/README.md (not served here)
- docs/brainstorm/agent-boundary-control-surfaces/cli-output/claude.md (not served here)
- docs/brainstorm/agent-boundary-control-surfaces/cli-output/prompt.md (not served here)
- docs/brainstorm/agent-boundary-control-surfaces/design.md (not served here)
- docs/brainstorm/agent-boundary-control-surfaces/plan.md (not served here)
- docs/brainstorm/agent-boundary-control-surfaces/variants.md (not served here)
- docs/brainstorm/agent-capability-cli-integration/README.md (not served here)
- docs/brainstorm/agent-capability-cli-integration/adr.md (not served here)
- docs/brainstorm/agent-capability-cli-integration/cli-output/claude.md (not served here)
- docs/brainstorm/agent-capability-cli-integration/cli-output/prompt.md (not served here)
- docs/brainstorm/agent-capability-cli-integration/context.md (not served here)
- docs/brainstorm/agent-capability-cli-integration/design.md (not served here)
- docs/brainstorm/agent-capability-cli-integration/implementation-notes.md (not served here)
- docs/brainstorm/agent-capability-cli-integration/notes.md (not served here)
- docs/brainstorm/agent-capability-cli-integration/phase-log.md (not served here)
- docs/brainstorm/agent-capability-cli-integration/plan.md (not served here)
- docs/brainstorm/agent-capability-cli-integration/pr-notes.md (not served here)
- docs/brainstorm/agent-capability-cli-integration/scope.md (not served here)
- docs/brainstorm/agent-capability-cli-integration/task-body.md (not served here)
- docs/brainstorm/agent-capability-cli-integration/test-matrix.md (not served here)
- docs/brainstorm/agent-capability-cli-integration/variants.md (not served here)
- docs/brainstorm/agent-capability-cli-integration/verification.md (not served here)
- docs/brainstorm/agent-presence-self-improvement/cli-output/consultant.md (not served here)
- docs/brainstorm/agent-presence-self-improvement/cli-output/prompt.md (not served here)
- docs/brainstorm/agent-presence-self-improvement/design.md (not served here)
- docs/brainstorm/agent-presence-self-improvement/plan.md (not served here)
- docs/brainstorm/agent-presence-self-improvement/variants.md (not served here)
- docs/brainstorm/agent-surface-suite/cli-output/claude.md (not served here)
- docs/brainstorm/agent-surface-suite/cli-output/prompt.md (not served here)
- docs/brainstorm/agent-surface-suite/design.md (not served here)
- docs/brainstorm/agent-surface-suite/plan.md (not served here)
- docs/brainstorm/agent-surface-suite/variants.md (not served here)
- docs/brainstorm/agent-write-contract-suite/cli-output/claude.md (not served here)
- docs/brainstorm/agent-write-contract-suite/cli-output/prompt.md (not served here)
- docs/brainstorm/agent-write-contract-suite/design.md (not served here)
- docs/brainstorm/agent-write-contract-suite/plan.md (not served here)
- docs/brainstorm/agent-write-contract-suite/variants.md (not served here)
- docs/brainstorm/answer-trust/cli-output/consultant.md (not served here)
- docs/brainstorm/answer-trust/cli-output/prompt.md (not served here)
- docs/brainstorm/answer-trust/design.md (not served here)
- docs/brainstorm/answer-trust/plan.md (not served here)
- docs/brainstorm/answer-trust/variants.md (not served here)
- docs/brainstorm/auto-migrate-on-upgrade/design.md (not served here)
- docs/brainstorm/auto-migrate-on-upgrade/plan.md (not served here)
- docs/brainstorm/auto-migrate-on-upgrade/variants.md (not served here)
- docs/brainstorm/belief-lifecycle-decision-memory/cli-output/claude.md (not served here)
- docs/brainstorm/belief-lifecycle-decision-memory/cli-output/prompt.md (not served here)
- docs/brainstorm/belief-lifecycle-decision-memory/design.md (not served here)
- docs/brainstorm/belief-lifecycle-decision-memory/plan.md (not served here)
- docs/brainstorm/belief-lifecycle-decision-memory/variants.md (not served here)
- docs/brainstorm/brain-centric-layout/design.md (not served here)
- docs/brainstorm/brain-centric-layout/plan.md (not served here)
- docs/brainstorm/brain-integrity-safety-hardening/cli-output/claude.md (not served here)
- docs/brainstorm/brain-integrity-safety-hardening/cli-output/prompt.md (not served here)
- docs/brainstorm/brain-integrity-safety-hardening/design.md (not served here)
- docs/brainstorm/brain-integrity-safety-hardening/plan.md (not served here)
- docs/brainstorm/brain-integrity-safety-hardening/variants.md (not served here)
- docs/brainstorm/brain-integrity-suite/cli-output/claude.md (not served here)
- docs/brainstorm/brain-integrity-suite/cli-output/prompt.md (not served here)
- docs/brainstorm/brain-integrity-suite/design.md (not served here)
- docs/brainstorm/brain-integrity-suite/plan.md (not served here)
- docs/brainstorm/brain-integrity-suite/pr-body.md (not served here)
- docs/brainstorm/brain-integrity-suite/variants.md (not served here)
- docs/brainstorm/brain-lifecycle-review-suite/cli-output/claude.md (not served here)
- docs/brainstorm/brain-lifecycle-review-suite/cli-output/prompt.md (not served here)
- docs/brainstorm/brain-lifecycle-review-suite/design.md (not served here)
- docs/brainstorm/brain-lifecycle-review-suite/plan.md (not served here)
- docs/brainstorm/brain-lifecycle-review-suite/variants.md (not served here)
- docs/brainstorm/brain-lifecycle-suite/cli-output/claude.md (not served here)
- docs/brainstorm/brain-lifecycle-suite/cli-output/prompt.md (not served here)
- docs/brainstorm/brain-lifecycle-suite/design.md (not served here)
- docs/brainstorm/brain-lifecycle-suite/plan.md (not served here)
- docs/brainstorm/brain-lifecycle-suite/variants.md (not served here)
- docs/brainstorm/brain-model-semantics-foundation/adr.md (not served here)
- docs/brainstorm/brain-model-semantics-foundation/cli-output/claude.md (not served here)
- docs/brainstorm/brain-model-semantics-foundation/cli-output/prompt.md (not served here)
- docs/brainstorm/brain-model-semantics-foundation/design.md (not served here)
- docs/brainstorm/brain-model-semantics-foundation/plan.md (not served here)
- docs/brainstorm/brain-model-semantics-foundation/variants.md (not served here)
- docs/brainstorm/brain-portability-interop/cli-output/claude.md (not served here)
- docs/brainstorm/brain-portability-interop/cli-output/prompt.md (not served here)
- docs/brainstorm/brain-portability-interop/design.md (not served here)
- docs/brainstorm/brain-portability-interop/plan.md (not served here)
- docs/brainstorm/brain-portability-interop/variants.md (not served here)
- docs/brainstorm/brain-safety-governance-suite/cli-output/claude.md (not served here)
- docs/brainstorm/brain-safety-governance-suite/cli-output/prompt.md (not served here)
- docs/brainstorm/brain-safety-governance-suite/design.md (not served here)
- docs/brainstorm/brain-safety-governance-suite/plan.md (not served here)
- docs/brainstorm/brain-safety-governance-suite/variants.md (not served here)
- docs/brainstorm/cjk-schema-lifecycle-recovery/cli-output/claude.md (not served here)
- docs/brainstorm/cjk-schema-lifecycle-recovery/cli-output/prompt.md (not served here)
- docs/brainstorm/cjk-schema-lifecycle-recovery/design.md (not served here)
- docs/brainstorm/cjk-schema-lifecycle-recovery/plan.md (not served here)
- docs/brainstorm/cjk-schema-lifecycle-recovery/variants.md (not served here)
- docs/brainstorm/codegraph-link-depth-mcp/cli-output/claude.md (not served here)
- docs/brainstorm/codegraph-link-depth-mcp/cli-output/prompt.md (not served here)
- docs/brainstorm/codegraph-link-depth-mcp/design.md (not served here)
- docs/brainstorm/codegraph-link-depth-mcp/plan.md (not served here)
- docs/brainstorm/codegraph-link-depth-mcp/variants.md (not served here)
- docs/brainstorm/codegraph-mcp-operational-readability/cli-output/claude.md (not served here)
- docs/brainstorm/codegraph-mcp-operational-readability/cli-output/prompt.md (not served here)
- docs/brainstorm/codegraph-mcp-operational-readability/design.md (not served here)
- docs/brainstorm/codegraph-mcp-operational-readability/plan.md (not served here)
- docs/brainstorm/codegraph-mcp-operational-readability/variants.md (not served here)
- docs/brainstorm/context-continuity-receipts-suite/cli-output/claude.md (not served here)
- docs/brainstorm/context-continuity-receipts-suite/cli-output/prompt.md (not served here)
- docs/brainstorm/context-continuity-receipts-suite/design.md (not served here)
- docs/brainstorm/context-continuity-receipts-suite/plan.md (not served here)
- docs/brainstorm/context-continuity-receipts-suite/variants.md (not served here)
- docs/brainstorm/context-injection-pipeline/cli-output/consultant.md (not served here)
- docs/brainstorm/context-injection-pipeline/cli-output/prompt.md (not served here)
- docs/brainstorm/context-injection-pipeline/design.md (not served here)
- docs/brainstorm/context-injection-pipeline/plan.md (not served here)
- docs/brainstorm/context-injection-pipeline/variants.md (not served here)
- docs/brainstorm/context-integrity-gates/cli-output/claude.md (not served here)
- docs/brainstorm/context-integrity-gates/cli-output/prompt.md (not served here)
- docs/brainstorm/context-integrity-gates/design.md (not served here)
- docs/brainstorm/context-integrity-gates/plan.md (not served here)
- docs/brainstorm/context-integrity-gates/variants.md (not served here)
- docs/brainstorm/context-pack-economics-observability/cli-output/claude.md (not served here)
- docs/brainstorm/context-pack-economics-observability/cli-output/prompt.md (not served here)
- docs/brainstorm/context-pack-economics-observability/design.md (not served here)
- docs/brainstorm/context-pack-economics-observability/plan.md (not served here)
- docs/brainstorm/context-pack-economics-observability/variants.md (not served here)
- docs/brainstorm/continuity-hygiene-freshness/cli-output/claude.md (not served here)
- docs/brainstorm/continuity-hygiene-freshness/cli-output/prompt.md (not served here)
- docs/brainstorm/continuity-hygiene-freshness/design.md (not served here)
- docs/brainstorm/continuity-hygiene-freshness/plan.md (not served here)
- docs/brainstorm/continuity-hygiene-freshness/variants.md (not served here)
- docs/brainstorm/cross-agent-query-foundation/cli-output/claude.md (not served here)
- docs/brainstorm/cross-agent-query-foundation/cli-output/prompt.md (not served here)
- docs/brainstorm/cross-agent-query-foundation/design.md (not served here)
- docs/brainstorm/cross-agent-query-foundation/plan.md (not served here)
- docs/brainstorm/cross-agent-query-foundation/variants.md (not served here)
- docs/brainstorm/crossvault-cards-eventtrace-fixes/design.md (not served here)
- docs/brainstorm/crossvault-cards-eventtrace-fixes/plan.md (not served here)
- docs/brainstorm/crossvault-cards-eventtrace-fixes/variants.md (not served here)
- docs/brainstorm/cycle-20261020/cli-output/claude.md (not served here)
- docs/brainstorm/cycle-20261020/cli-output/prompt.md (not served here)
- docs/brainstorm/cycle-20261020/design.md (not served here)
- docs/brainstorm/cycle-20261020/plan.md (not served here)
- docs/brainstorm/cycle-20261020/variants.md (not served here)
- docs/brainstorm/dated-commitments/cli-output/claude.md (not served here)
- docs/brainstorm/dated-commitments/cli-output/prompt.md (not served here)
- docs/brainstorm/dated-commitments/design.md (not served here)
- docs/brainstorm/dated-commitments/plan.md (not served here)
- docs/brainstorm/dated-commitments/variants.md (not served here)
- docs/brainstorm/default-scope-feedback/cli-output/claude.md (not served here)
- docs/brainstorm/default-scope-feedback/cli-output/prompt.md (not served here)
- docs/brainstorm/default-scope-feedback/design.md (not served here)
- docs/brainstorm/default-scope-feedback/plan.md (not served here)
- docs/brainstorm/default-scope-feedback/variants.md (not served here)
- docs/brainstorm/delivery-trust/cli-output/consultant.md (not served here)
- docs/brainstorm/delivery-trust/cli-output/prompt.md (not served here)
- docs/brainstorm/delivery-trust/design.md (not served here)
- docs/brainstorm/delivery-trust/plan.md (not served here)
- docs/brainstorm/delivery-trust/variants.md (not served here)
- docs/brainstorm/distilled-provenance/cli-output/claude.md (not served here)
- docs/brainstorm/distilled-provenance/cli-output/expected-output.md (not served here)
- docs/brainstorm/distilled-provenance/cli-output/prompt.md (not served here)
- docs/brainstorm/distilled-provenance/design.md (not served here)
- docs/brainstorm/distilled-provenance/plan.md (not served here)
- docs/brainstorm/distilled-provenance/variants.md (not served here)
- docs/brainstorm/embedding-provider-suite/cli-output/claude.md (not served here)
- docs/brainstorm/embedding-provider-suite/cli-output/prompt.md (not served here)
- docs/brainstorm/embedding-provider-suite/design.md (not served here)
- docs/brainstorm/embedding-provider-suite/plan.md (not served here)
- docs/brainstorm/embedding-provider-suite/variants.md (not served here)
- docs/brainstorm/entity-truth-dream-suite/cli-output/claude.md (not served here)
- docs/brainstorm/entity-truth-dream-suite/cli-output/prompt.md (not served here)
- docs/brainstorm/entity-truth-dream-suite/design.md (not served here)
- docs/brainstorm/entity-truth-dream-suite/plan.md (not served here)
- docs/brainstorm/entity-truth-dream-suite/variants.md (not served here)
- docs/brainstorm/evidence-at-the-boundary/cli-output/claude.md (not served here)
- docs/brainstorm/evidence-at-the-boundary/cli-output/prompt.md (not served here)
- docs/brainstorm/evidence-at-the-boundary/design.md (not served here)
- docs/brainstorm/evidence-at-the-boundary/plan.md (not served here)
- docs/brainstorm/evidence-at-the-boundary/recon/0e79f0-lint-on-write.md (not served here)
- docs/brainstorm/evidence-at-the-boundary/recon/130-hermes-readiness.md (not served here)
- docs/brainstorm/evidence-at-the-boundary/recon/155-include-paths.md (not served here)
- docs/brainstorm/evidence-at-the-boundary/recon/160-source-trust.md (not served here)
- docs/brainstorm/evidence-at-the-boundary/recon/161-slug-race.md (not served here)
- docs/brainstorm/evidence-at-the-boundary/recon/3309-retrieval-trail.md (not served here)
- docs/brainstorm/evidence-at-the-boundary/recon/a3254-readiness-liveness.md (not served here)
- docs/brainstorm/evidence-at-the-boundary/recon/conventions.md (not served here)
- docs/brainstorm/evidence-at-the-boundary/recon/e24c6d-schema-completeness.md (not served here)
- docs/brainstorm/evidence-at-the-boundary/recon/e5f447-recall-channels.md (not served here)
- docs/brainstorm/evidence-at-the-boundary/variants.md (not served here)
- docs/brainstorm/grok-native-integration/cli-output/claude.md (not served here)
- docs/brainstorm/grok-native-integration/cli-output/prompt.md (not served here)
- docs/brainstorm/grok-native-integration/design.md (not served here)
- docs/brainstorm/grok-native-integration/plan.md (not served here)
- docs/brainstorm/grok-native-integration/variants.md (not served here)
- docs/brainstorm/hermes-memory-provider/cli-output/claude.md (not served here)
- docs/brainstorm/hermes-memory-provider/cli-output/prompt.md (not served here)
- docs/brainstorm/hermes-memory-provider/design.md (not served here)
- docs/brainstorm/hermes-memory-provider/plan.md (not served here)
- docs/brainstorm/hermes-memory-provider/variants.md (not served here)
- docs/brainstorm/hermes-static-tool-schemas/cli-output/claude.md (not served here)
- docs/brainstorm/hermes-static-tool-schemas/cli-output/prompt.md (not served here)
- docs/brainstorm/hermes-static-tool-schemas/design.md (not served here)
- docs/brainstorm/hermes-static-tool-schemas/plan.md (not served here)
- docs/brainstorm/hermes-static-tool-schemas/variants.md (not served here)
- docs/brainstorm/hindsight-brain-loop-ops/cli-output/claude.md (not served here)
- docs/brainstorm/hindsight-brain-loop-ops/cli-output/prompt.md (not served here)
- docs/brainstorm/hindsight-brain-loop-ops/design.md (not served here)
- docs/brainstorm/hindsight-brain-loop-ops/plan.md (not served here)
- docs/brainstorm/hindsight-brain-loop-ops/variants.md (not served here)
- docs/brainstorm/honest-embedding-spend/cli-output/claude.md (not served here)
- docs/brainstorm/honest-embedding-spend/cli-output/prompt.md (not served here)
- docs/brainstorm/honest-embedding-spend/design.md (not served here)
- docs/brainstorm/honest-embedding-spend/plan.md (not served here)
- docs/brainstorm/honest-embedding-spend/variants.md (not served here)
- docs/brainstorm/honest-query-embed-and-safe-upgrades/cli-output/claude.md (not served here)
- docs/brainstorm/honest-query-embed-and-safe-upgrades/cli-output/prompt.md (not served here)
- docs/brainstorm/honest-query-embed-and-safe-upgrades/design.md (not served here)
- docs/brainstorm/honest-query-embed-and-safe-upgrades/plan.md (not served here)
- docs/brainstorm/honest-query-embed-and-safe-upgrades/variants.md (not served here)
- docs/brainstorm/hook-update-resilience/design.md (not served here)
- docs/brainstorm/hook-update-resilience/plan.md (not served here)
- docs/brainstorm/hook-update-resilience/variants.md (not served here)
- docs/brainstorm/index-freshness/design.md (not served here)
- docs/brainstorm/index-freshness/plan.md (not served here)
- docs/brainstorm/indexer-durability/cli-output/claude.md (not served here)
- docs/brainstorm/indexer-durability/cli-output/prompt.md (not served here)
- docs/brainstorm/indexer-durability/design.md (not served here)
- docs/brainstorm/indexer-durability/plan.md (not served here)
- docs/brainstorm/indexer-durability/variants.md (not served here)
- docs/brainstorm/ingestion-import-robustness/design.md (not served here)
- docs/brainstorm/ingestion-import-robustness/plan.md (not served here)
- docs/brainstorm/ingestion-import-robustness/variants.md (not served here)
- docs/brainstorm/integrity-control-agent-memory-surface/cli-output/claude.md (not served here)
- docs/brainstorm/integrity-control-agent-memory-surface/cli-output/prompt.md (not served here)
- docs/brainstorm/integrity-control-agent-memory-surface/design.md (not served here)
- docs/brainstorm/integrity-control-agent-memory-surface/plan.md (not served here)
- docs/brainstorm/integrity-control-agent-memory-surface/variants.md (not served here)
- docs/brainstorm/knowledge-intake-and-consolidation/cli-output/claude.md (not served here)
- docs/brainstorm/knowledge-intake-and-consolidation/cli-output/prompt.md (not served here)
- docs/brainstorm/knowledge-intake-and-consolidation/design.md (not served here)
- docs/brainstorm/knowledge-intake-and-consolidation/plan.md (not served here)
- docs/brainstorm/knowledge-intake-and-consolidation/variants.md (not served here)
- docs/brainstorm/knowledge-provenance/cli-output/claude.md (not served here)
- docs/brainstorm/knowledge-provenance/cli-output/prompt.md (not served here)
- docs/brainstorm/knowledge-provenance/design.md (not served here)
- docs/brainstorm/knowledge-provenance/plan.md (not served here)
- docs/brainstorm/knowledge-provenance/variants.md (not served here)
- docs/brainstorm/language-agnostic-fact-extraction/cli-output/claude.md (not served here)
- docs/brainstorm/language-agnostic-fact-extraction/cli-output/prompt.md (not served here)
- docs/brainstorm/language-agnostic-fact-extraction/design.md (not served here)
- docs/brainstorm/language-agnostic-fact-extraction/plan.md (not served here)
- docs/brainstorm/language-agnostic-fact-extraction/variants.md (not served here)
- docs/brainstorm/link-graph-surfaces/cli-output/claude.md (not served here)
- docs/brainstorm/link-graph-surfaces/cli-output/prompt.md (not served here)
- docs/brainstorm/link-graph-surfaces/design.md (not served here)
- docs/brainstorm/link-graph-surfaces/plan.md (not served here)
- docs/brainstorm/link-graph-surfaces/variants.md (not served here)
- docs/brainstorm/link-recall-intelligence/cli-output/claude.md (not served here)
- docs/brainstorm/link-recall-intelligence/cli-output/prompt.md (not served here)
- docs/brainstorm/link-recall-intelligence/design.md (not served here)
- docs/brainstorm/link-recall-intelligence/plan.md (not served here)
- docs/brainstorm/link-recall-intelligence/variants.md (not served here)
- docs/brainstorm/manifest-dependency-facts/cli-output/claude.md (not served here)
- docs/brainstorm/manifest-dependency-facts/cli-output/expected-output.md (not served here)
- docs/brainstorm/manifest-dependency-facts/cli-output/prompt.md (not served here)
- docs/brainstorm/manifest-dependency-facts/design.md (not served here)
- docs/brainstorm/manifest-dependency-facts/plan.md (not served here)
- docs/brainstorm/manifest-dependency-facts/variants.md (not served here)
- docs/brainstorm/mcp-context-economy/cli-output/claude.md (not served here)
- docs/brainstorm/mcp-context-economy/cli-output/prompt.md (not served here)
- docs/brainstorm/mcp-context-economy/design.md (not served here)
- docs/brainstorm/mcp-context-economy/plan.md (not served here)
- docs/brainstorm/mcp-context-economy/variants.md (not served here)
- docs/brainstorm/mcp-error-codes/cli-output/claude.md (not served here)
- docs/brainstorm/mcp-error-codes/cli-output/prompt.md (not served here)
- docs/brainstorm/mcp-error-codes/design.md (not served here)
- docs/brainstorm/mcp-error-codes/plan.md (not served here)
- docs/brainstorm/mcp-error-codes/variants.md (not served here)
- docs/brainstorm/memory-integrity/cli-output/claude.md (not served here)
- docs/brainstorm/memory-integrity/cli-output/prompt.md (not served here)
- docs/brainstorm/memory-integrity/design.md (not served here)
- docs/brainstorm/memory-integrity/plan.md (not served here)
- docs/brainstorm/memory-integrity/variants.md (not served here)
- docs/brainstorm/memory-observability-suite/atof-atif-mapping.md (not served here)
- docs/brainstorm/memory-observability-suite/cli-output/claude.md (not served here)
- docs/brainstorm/memory-observability-suite/cli-output/prompt.md (not served here)
- docs/brainstorm/memory-observability-suite/design.md (not served here)
- docs/brainstorm/memory-observability-suite/plan.md (not served here)
- docs/brainstorm/memory-observability-suite/variants.md (not served here)
- docs/brainstorm/memory-signal-provenance-lifecycle/cli-output/claude.md (not served here)
- docs/brainstorm/memory-signal-provenance-lifecycle/cli-output/prompt.md (not served here)
- docs/brainstorm/memory-signal-provenance-lifecycle/design.md (not served here)
- docs/brainstorm/memory-signal-provenance-lifecycle/plan.md (not served here)
- docs/brainstorm/memory-signal-provenance-lifecycle/variants.md (not served here)
- docs/brainstorm/memory-subsystem-alignment/cli-output/claude.md (not served here)
- docs/brainstorm/memory-subsystem-alignment/cli-output/prompt.md (not served here)
- docs/brainstorm/memory-subsystem-alignment/design.md (not served here)
- docs/brainstorm/memory-subsystem-alignment/plan.md (not served here)
- docs/brainstorm/memory-subsystem-alignment/variants.md (not served here)
- docs/brainstorm/memory-write-path-integrity/cli-output/claude.md (not served here)
- docs/brainstorm/memory-write-path-integrity/cli-output/prompt.md (not served here)
- docs/brainstorm/memory-write-path-integrity/design.md (not served here)
- docs/brainstorm/memory-write-path-integrity/plan.md (not served here)
- docs/brainstorm/memory-write-path-integrity/variants.md (not served here)
- docs/brainstorm/near-duplicate-defense/cli-output/claude.md (not served here)
- docs/brainstorm/near-duplicate-defense/cli-output/codex.md (not served here)
- docs/brainstorm/near-duplicate-defense/cli-output/opencode.md (not served here)
- docs/brainstorm/near-duplicate-defense/cli-output/prompt.md (not served here)
- docs/brainstorm/near-duplicate-defense/cli-output/second-claude.md (not served here)
- docs/brainstorm/near-duplicate-defense/design.md (not served here)
- docs/brainstorm/near-duplicate-defense/plan.md (not served here)
- docs/brainstorm/near-duplicate-defense/variants.md (not served here)
- docs/brainstorm/no-dead-ends/cli-output/claude.md (not served here)
- docs/brainstorm/no-dead-ends/cli-output/prompt.md (not served here)
- docs/brainstorm/no-dead-ends/design.md (not served here)
- docs/brainstorm/no-dead-ends/plan.md (not served here)
- docs/brainstorm/no-dead-ends/variants.md (not served here)
- docs/brainstorm/non-markdown-ingest/cli-output/claude.md (not served here)
- docs/brainstorm/non-markdown-ingest/cli-output/expected-output.md (not served here)
- docs/brainstorm/non-markdown-ingest/cli-output/prompt.md (not served here)
- docs/brainstorm/non-markdown-ingest/design.md (not served here)
- docs/brainstorm/non-markdown-ingest/plan.md (not served here)
- docs/brainstorm/non-markdown-ingest/variants.md (not served here)
- docs/brainstorm/note-change-truth-maintenance/cli-output/claude.md (not served here)
- docs/brainstorm/note-change-truth-maintenance/cli-output/prompt.md (not served here)
- docs/brainstorm/note-change-truth-maintenance/cli-output/task-bodies.md (not served here)
- docs/brainstorm/note-change-truth-maintenance/design.md (not served here)
- docs/brainstorm/note-change-truth-maintenance/plan.md (not served here)
- docs/brainstorm/note-change-truth-maintenance/variants.md (not served here)
- docs/brainstorm/nothing-runs-unwatched/cli-output/claude.md (not served here)
- docs/brainstorm/nothing-runs-unwatched/cli-output/prompt.md (not served here)
- docs/brainstorm/nothing-runs-unwatched/design.md (not served here)
- docs/brainstorm/nothing-runs-unwatched/plan.md (not served here)
- docs/brainstorm/nothing-runs-unwatched/recon/architect-module.md (not served here)
- docs/brainstorm/nothing-runs-unwatched/recon/concurrency-and-cancellation.md (not served here)
- docs/brainstorm/nothing-runs-unwatched/recon/durability-and-ownership.md (not served here)
- docs/brainstorm/nothing-runs-unwatched/recon/failure-mode-bench.md (not served here)
- docs/brainstorm/nothing-runs-unwatched/recon/ingest-adapters-and-lease.md (not served here)
- docs/brainstorm/nothing-runs-unwatched/recon/progress-reporting.md (not served here)
- docs/brainstorm/nothing-runs-unwatched/recon/provider-sunset.md (not served here)
- docs/brainstorm/nothing-runs-unwatched/review/claims-and-refusals.md (not served here)
- docs/brainstorm/nothing-runs-unwatched/review/cross-cutting.md (not served here)
- docs/brainstorm/nothing-runs-unwatched/review/locks-and-races.md (not served here)
- docs/brainstorm/nothing-runs-unwatched/review/progress-and-cancellation.md (not served here)
- docs/brainstorm/nothing-runs-unwatched/review/tests-and-docs.md (not served here)
- docs/brainstorm/nothing-runs-unwatched/variants.md (not served here)
- docs/brainstorm/nothing-writes-silently/cli-output/claude.md (not served here)
- docs/brainstorm/nothing-writes-silently/cli-output/prompt.md (not served here)
- docs/brainstorm/nothing-writes-silently/design.md (not served here)
- docs/brainstorm/nothing-writes-silently/plan.md (not served here)
- docs/brainstorm/nothing-writes-silently/variants.md (not served here)
- docs/brainstorm/opencode-native-integration/cli-output/claude.md (not served here)
- docs/brainstorm/opencode-native-integration/cli-output/prompt.md (not served here)
- docs/brainstorm/opencode-native-integration/design.md (not served here)
- docs/brainstorm/opencode-native-integration/plan.md (not served here)
- docs/brainstorm/opencode-native-integration/variants.md (not served here)
- docs/brainstorm/operability-first-run/design.md (not served here)
- docs/brainstorm/operability-first-run/plan.md (not served here)
- docs/brainstorm/operability-first-run/variants.md (not served here)
- docs/brainstorm/operational-friction-reduction/design.md (not served here)
- docs/cli-reference.md
- docs/cross-project-pointer.md
- docs/decision-models.md
- docs/hermes-cron.md
- docs/how-it-works.md
- docs/idea.md
- docs/ingest-manifest.md
- docs/mcp.md
- docs/metrics.md
- docs/observability.md
- docs/stability.md
- docs/updating.md
- hooks/hooks.json
- install.md
- plugins/codex/LICENSE
Advantages
- Human-readable and editable memory stored as standard Markdown files in an Obsidian vault. evidence: README.md
- Deterministic dream pass ensures rules are derived from repeated signals without requiring additional model runs. evidence: README.md
- Supports reversible changes through snapshots and rollback capabilities. evidence: README.md
Limitations
- Requires Bun runtime for the CLI and core operations. evidence: README.md
- Advanced features like semantic search require additional setup with sqlite-vec. evidence: README.md
- Some integrations depend on specific client-side plugin support or MCP host capabilities. evidence: README.md
Security
Risk level: Low risk
This brain runs
MCP servers (2)
.claude-plugin/plugin.json(open-second-brain):${CLAUDE_PLUGIN_ROOT}/scripts/o2b mcp --harness claude-code.claude-plugin/plugin.json(open-second-brain-writer):${CLAUDE_PLUGIN_ROOT}/scripts/o2b mcp --scope writer --harness claude-code
Git hooks (2)
.githooks/pre-commit(pre-commit):runs on git pre-commit.githooks/pre-push(pre-push):runs on git pre-push
Agent instructions that run or fetch something (46)
CLAUDE.md:bun run scripts/sync-version.tsCLAUDE.md:CI gates on `bun run scripts/sync-version.ts --check` (the `validate` job, step "Verify manifest version sync").CLAUDE.md:Run the `--check` form locally before pushing a version change.CLAUDE.md:Bump `package.json` (and run `sync-version.ts`) on the feature branch and include it in the SAME pull request as the feature, before you push.CLAUDE.md:Bump `package.json` to `X.Y.Z` and run `bun run scripts/sync-version.ts`.CLAUDE.md:After changing a skill or `hooks/hooks.json`, run:CLAUDE.md:bun run sync-plugin-mirrorsCLAUDE.md:CI gates on `bun run sync-plugin-mirrors:check` (the `validate` and `windows` jobs), and the pre-commit hook runs the same check.docs/legacy-skills/agent-event-log/SKILL.md:Read-only queries (running `git log`, `o2b status`, `vault_health`, `second_brain_query`).docs/legacy-skills/agent-event-log/SKILL.md:**Technical identifiers stay verbatim regardless of surrounding language**: command names (`bun test`, `o2b doctor`), file paths (`src/core/event-log.ts`), package and library names (`proper-lockfile`, `@anthropic-ai/sdk`), API symbols, error messages quoted from logs.plugins/codex/skills/brain-memory/SKILL.md:Read-only inspection (running `git log`, `o2b status`, `vault_health`).plugins/codex/skills/brain-memory/SKILL.md:After scan, the source line becomes `@osb✓ [[sig-...]]` so a second run is idempotent.plugins/codex/skills/codegraph-partner/SKILL.md:Run these checks in order.plugins/codex/skills/codegraph-partner/SKILL.md:No install talk.plugins/codex/skills/codegraph-partner/SKILL.md:| | not_indexed (CLI present, no `.codegraph/` or `initialized: false`) | Offer one line: "run `codegraph init <path>` to index this repo".plugins/codex/skills/codegraph-partner/SKILL.md:Whenever you run `codegraph init` in any repository on the user's behalf, immediately read that repo's `.gitignore` and append a `.codegraph/` line if one is not already present.plugins/codex/skills/decision-model-setup/SKILL.md:That is the operator's command, and only the operator can run it: the key is operator-only, so the `brain_settings` tool refuses an agent's write to it with `operator_only_setting` on every machine, switched or not.plugins/codex/skills/decision-model-setup/SKILL.md:Ask the operator to run the command;plugins/codex/skills/decision-model-setup/SKILL.md:Does not install servers or download model weights.plugins/codex/skills/embeddings-setup/SKILL.md:Every run that uses it prints one stderr warning per endpoint.plugins/codex/skills/embeddings-setup/SKILL.md:If the install path missed it, force a rebuild:plugins/codex/skills/embeddings-setup/SKILL.md:bun pm ls | grep sqlite-vecplugins/codex/skills/embeddings-setup/SKILL.md:bun install --forceplugins/codex/skills/embeddings-setup/SKILL.md:When the index already exists and only some chunks lack vectors, price the work first with the dry run, then apply it:plugins/codex/skills/embeddings-setup/SKILL.md:Does not invoke `brew install` on the user's behalf.plugins/codex/skills/schema-author/SKILL.md:Preview the batch first with `schema_apply_mutations` (`dry_run=true`) or `o2b brain schema apply --dry-run --mutation ...`: it returns the pack that would result and its diff and writes nothing at all - no config write, no audit record, no lock file.plugins/codex/skills/schema-author/SKILL.md:It does not run the two checks that exist only because the apply writes: the vault-identity write guard, and the atomic writer's re-parse of the rendered YAML.plugins/codex/skills/schema-author/SKILL.md:Re-run lint/stats after mutation and report changed tokens plus any remaining findings.skills/brain-memory/SKILL.md:Read-only inspection (running `git log`, `o2b status`, `vault_health`).skills/brain-memory/SKILL.md:After scan, the source line becomes `@osb✓ [[sig-...]]` so a second run is idempotent.skills/codegraph-partner/SKILL.md:Run these checks in order.skills/codegraph-partner/SKILL.md:No install talk.skills/codegraph-partner/SKILL.md:| | not_indexed (CLI present, no `.codegraph/` or `initialized: false`) | Offer one line: "run `codegraph init <path>` to index this repo".skills/codegraph-partner/SKILL.md:Whenever you run `codegraph init` in any repository on the user's behalf, immediately read that repo's `.gitignore` and append a `.codegraph/` line if one is not already present.skills/decision-model-setup/SKILL.md:That is the operator's command, and only the operator can run it: the key is operator-only, so the `brain_settings` tool refuses an agent's write to it with `operator_only_setting` on every machine, switched or not.skills/decision-model-setup/SKILL.md:Ask the operator to run the command;skills/decision-model-setup/SKILL.md:Does not install servers or download model weights.skills/embeddings-setup/SKILL.md:Every run that uses it prints one stderr warning per endpoint.skills/embeddings-setup/SKILL.md:If the install path missed it, force a rebuild:skills/embeddings-setup/SKILL.md:bun pm ls | grep sqlite-vecskills/embeddings-setup/SKILL.md:bun install --forceskills/embeddings-setup/SKILL.md:When the index already exists and only some chunks lack vectors, price the work first with the dry run, then apply it:skills/embeddings-setup/SKILL.md:Does not invoke `brew install` on the user's behalf.skills/schema-author/SKILL.md:Preview the batch first with `schema_apply_mutations` (`dry_run=true`) or `o2b brain schema apply --dry-run --mutation ...`: it returns the pack that would result and its diff and writes nothing at all - no config write, no audit record, no lock file.skills/schema-author/SKILL.md:It does not run the two checks that exist only because the apply writes: the vault-identity write guard, and the atomic writer's re-parse of the rendered YAML.skills/schema-author/SKILL.md:Re-run lint/stats after mutation and report changed tokens plus any remaining findings.
Hooks (54)
hooks/hooks.json(PostToolUse):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" session-capture; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook session-capture; exit 0hooks/hooks.json(PostToolUse):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" post-write-reminder; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook post-write-reminder; exit 0hooks/hooks.json(PostToolUse):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" subagent-inject; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook subagent-inject; exit 0hooks/hooks.json(PostToolUse):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" reground-deliver; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook reground-deliver; exit 0hooks/hooks.json(SessionStart):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" active-inject; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook active-inject; exit 0hooks/hooks.json(SessionStart):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" gap-agenda; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook gap-agenda; exit 0hooks/hooks.json(SessionStart):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" backlog-nudge; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook backlog-nudge; exit 0hooks/hooks.json(SessionStart):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" log-catchup; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook log-catchup; exit 0hooks/hooks.json(SessionStart):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" view-render; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook view-render; exit 0hooks/hooks.json(SessionStart):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" session-capture; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook session-capture; exit 0hooks/hooks.json(UserPromptSubmit):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" session-capture; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook session-capture; exit 0hooks/hooks.json(UserPromptSubmit):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" recall-inject; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook recall-inject; exit 0hooks/hooks.json(UserPromptSubmit):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" nav-inject; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook nav-inject; exit 0hooks/hooks.json(UserPromptSubmit):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" counterparty-brief; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook counterparty-brief; exit 0hooks/hooks.json(UserPromptSubmit):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" reground-deliver; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook reground-deliver; exit 0hooks/hooks.json(PreToolUse):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" pretool-orient; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook pretool-orient; exit 0hooks/hooks.json(PreToolUse):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" pretool-host-backup; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook pretool-host-backup; exit 0hooks/hooks.json(Stop):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" session-capture; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook session-capture; exit 0hooks/hooks.json(Stop):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" stop-log-guardrail; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook stop-log-guardrail; exit 0hooks/hooks.json(Stop):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" answer-quote-check; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook answer-quote-check; exit 0hooks/hooks.json(Stop):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" hygiene-digest; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook hygiene-digest; exit 0hooks/hooks.json(SubagentStart):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" subagent-inject; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook subagent-inject; exit 0hooks/hooks.json(SubagentStop):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" session-capture; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook session-capture; exit 0hooks/hooks.json(SessionEnd):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" session-capture; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook session-capture; exit 0hooks/hooks.json(SessionEnd):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" gap-promote; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook gap-promote; exit 0hooks/hooks.json(PostCompact):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" active-inject; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook active-inject; exit 0hooks/hooks.json(PostCompact):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" session-capture; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook session-capture; exit 0plugins/codex/hooks/hooks.json(PostToolUse):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" session-capture; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook session-capture; exit 0plugins/codex/hooks/hooks.json(PostToolUse):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" post-write-reminder; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook post-write-reminder; exit 0plugins/codex/hooks/hooks.json(PostToolUse):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" subagent-inject; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook subagent-inject; exit 0plugins/codex/hooks/hooks.json(PostToolUse):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" reground-deliver; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook reground-deliver; exit 0plugins/codex/hooks/hooks.json(SessionStart):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" active-inject; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook active-inject; exit 0plugins/codex/hooks/hooks.json(SessionStart):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" gap-agenda; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook gap-agenda; exit 0plugins/codex/hooks/hooks.json(SessionStart):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" backlog-nudge; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook backlog-nudge; exit 0plugins/codex/hooks/hooks.json(SessionStart):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" log-catchup; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook log-catchup; exit 0plugins/codex/hooks/hooks.json(SessionStart):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" view-render; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook view-render; exit 0plugins/codex/hooks/hooks.json(SessionStart):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" session-capture; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook session-capture; exit 0plugins/codex/hooks/hooks.json(UserPromptSubmit):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" session-capture; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook session-capture; exit 0plugins/codex/hooks/hooks.json(UserPromptSubmit):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" recall-inject; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook recall-inject; exit 0plugins/codex/hooks/hooks.json(UserPromptSubmit):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" nav-inject; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook nav-inject; exit 0plugins/codex/hooks/hooks.json(UserPromptSubmit):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" counterparty-brief; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook counterparty-brief; exit 0plugins/codex/hooks/hooks.json(UserPromptSubmit):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" reground-deliver; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook reground-deliver; exit 0plugins/codex/hooks/hooks.json(PreToolUse):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" pretool-orient; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook pretool-orient; exit 0plugins/codex/hooks/hooks.json(PreToolUse):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" pretool-host-backup; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook pretool-host-backup; exit 0plugins/codex/hooks/hooks.json(Stop):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" session-capture; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook session-capture; exit 0plugins/codex/hooks/hooks.json(Stop):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" stop-log-guardrail; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook stop-log-guardrail; exit 0plugins/codex/hooks/hooks.json(Stop):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" answer-quote-check; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook answer-quote-check; exit 0plugins/codex/hooks/hooks.json(Stop):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" hygiene-digest; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook hygiene-digest; exit 0plugins/codex/hooks/hooks.json(SubagentStart):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" subagent-inject; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook subagent-inject; exit 0plugins/codex/hooks/hooks.json(SubagentStop):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" session-capture; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook session-capture; exit 0plugins/codex/hooks/hooks.json(SessionEnd):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" session-capture; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook session-capture; exit 0plugins/codex/hooks/hooks.json(SessionEnd):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" gap-promote; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook gap-promote; exit 0plugins/codex/hooks/hooks.json(PostCompact):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" active-inject; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook active-inject; exit 0plugins/codex/hooks/hooks.json(PostCompact):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" session-capture; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook session-capture; exit 0
Package scripts (1)
package.json(prepare):bun scripts/prepare.ts || exit 0
- Low risk: scripts-present (.githooks/pre-commit)
- Low risk: scripts-present (.githooks/pre-push)
- Low risk: network-instructions (plugins/codex/skills/codegraph-partner/SKILL.md:115)
- Low risk: scripts-present (scripts/prepare.ts)
- Low risk: network-instructions (skills/codegraph-partner/SKILL.md:115)
Attribution and licence
- First committed by
hermes-vps-agent
- Declared authors
- Open Second Brain contributors (declared in .claude-plugin/plugin.json)
- Open Second Brain contributors (declared in pyproject.toml)
- Publisher
- Serg Eroshe (Community)
- Source
- itechmeat/open-second-brain · commit 233b91d
- Licence
- MIT License (files served here)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
- Contributors
- solaitken, itechmeat, Yori940619, hermes-vps-agent, DanBennettUK
AutoGineer is not affiliated with or endorsed by hermes-vps-agent or Serg Eroshe.
Install from source
486 of its files are not served here, so there is no ZIP. itechmeat/open-second-brain at 233b91d