Generated banner for Open Second Brain
Generated logo for Open Second Brain

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.
— the description by hermes-vps-agent

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.

Folders
FolderRoleFiles
.githubagent-config7
binscripts1
docsdocs774
hookshooks35
scriptsscripts22
skillsagent-instructions6
srcsource-code1496
templatestemplates23
teststests2101
.agentsfolder1
.ai-notesfolder1
.apbfolder87
.claude-pluginfolder2
.codex-pluginfolder1
.githooksfolder2
installfolder16
openclawfolder1
openspecfolder33
pluginsfolder21
schemasfolder5

The agent reads

The agent writes

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

  1. 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-push
    • 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
    • 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 0
    • 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 0
    • 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 0
    • 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 0
    • 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 0
    • 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 0
    • 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 0
    • 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 0
    • 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
    • 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
    • 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 0
    • 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 0
    • 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 0
    • 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 0
    • 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 0
    • 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 0
    • 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
    • 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 0
    • 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 0
    • 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 0
    • 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 0
    • 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
    • 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
    • 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 0
    • 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 0
    • 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
    • package.json: bun scripts/prepare.ts || exit 0
    • plugins/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
    • plugins/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 0
    • plugins/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 0
    • plugins/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 0
    • plugins/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 0
    • plugins/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 0
    • plugins/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 0
    • plugins/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 0
    • plugins/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 0
    • plugins/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
    • plugins/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
    • plugins/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 0
    • plugins/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 0
    • plugins/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 0
    • plugins/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 0
    • plugins/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 0
    • plugins/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 0
    • plugins/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
    • plugins/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 0
    • plugins/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 0
    • plugins/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 0
    • plugins/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 0
    • plugins/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
    • plugins/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
    • plugins/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 0
    • plugins/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 0
    • plugins/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
  2. 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
  3. 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 .-> Notes

Each 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| Pref

The 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

  1. Install Bun 1.1 or later: install/prerequisites.md.

  2. Install the plugin for your client (guides below), or get the o2b CLI: install/prerequisites.md.

  3. Create the vault files:

    o2b init       --vault /path/to/vault --name "My Second Brain"
    o2b brain init --vault /path/to/vault
  4. Connect 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

ClientIntegrationGuide
Claude CodeMarketplace plugin: MCP servers and hooksinstall/claudecode.md
OpenAI CodexMarketplace plugin, then o2b install --target codex --applyinstall/codex.md
ChatGPT desktopReads the shared Codex config: o2b install --target codex --applyinstall/codex.md
Hermes AgentPlugin and memory providerinstall/hermes.md
OpenClawNative plugin, no MCPinstall/openclaw.md
opencodeo2b install --target opencode --apply: MCP servers and a native plugininstall/opencode.md
Grok Buildo2b install --target grok --apply: MCP servers and hooksinstall/grok.md
Cursoro2b install --target cursor --applyinstall/cursor.md
kiroo2b install --target kiro --applyinstall/kiro.md
GitHub Copilot CLIo2b install --target copilot-cli --applyinstall/copilot-cli.md
Gemini CLIo2b install --target gemini-cli --applyinstall/gemini-cli.md
ZCodeMCP servers added to its config by handinstall/zcode.md
Aidero2b install --target aider --apply: context file, no MCPinstall/aider.md
Pio2b install --target pi --apply: skill over the CLIinstall/pi.md
Any other MCP hosto2b install --target generic --apply --out - prints the server entriesinstall/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 rollback restores it: snapshots.

  • Attributed writes. o2b brain writes lists every note write by agent and device, writes revert plans a restore, o2b brain freeze stops all writers, and o2b brain log verify checks 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 through brain_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-key seeding the fan-out from mined entity mentions): session logs, coverage.

  • Hygiene. o2b brain hygiene scan finds contested, duplicate, stale and unused memories; apply runs only the findings you pick: maintenance.

  • Claim ledger. o2b brain truth ingest records 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 events recalls a windowed slice of the ledger, and truth state grounds an agent-stated claim with an anchoring verdict - committed, or reported back with reason codes. truth state accepts an optional opaque --sample-id naming 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 --apply to 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-export writes a one-file backup bundle: knowledge packs.

  • Secret custody. Credentials live encrypted in the vault's state dir behind a capability-gated store; $secret:NAME references resolve at the use sites, o2b brain secret unlock wraps the keyfile under a passphrase, and secret export/import move the store between installs as one passphrase-encrypted bundle: custody CLI.

  • Today view and note markers. o2b brain today, @osb loop and @osb set markers: Brain CLI.

  • Self-inspection. o2b version, o2b install --friction and o2b state status report 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; the embeddings-setup skill 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 a search_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_ENABLED and write_approval.ingest / OPEN_SECOND_BRAIN_WRITE_APPROVAL_INGEST_ENABLED (each falling back to the write_approval.enabled master, default off) stage note creates and ingest summary pages into Brain/pending/ beside the staged signals, where they stay out of the search index until an operator runs o2b brain pending list and applies or rejects them; write-path integrity.

  • A permissions document, absent by default. Brain/_permissions.yaml (an operator-edited vault file) resolves allow/ask/deny per agent, role and target for the write, ingest and owner-write actions; with the file absent every check behaves exactly as today. ask stages the write, deny refuses with the write-refused token and the next command o2b brain permissions show, and every ask/deny verdict lands in a queryable decision ledger under Brain/logs/decisions/; o2b brain permissions show dry-runs the decision table, ledger reads the rows; Brain CLI.

  • The owner-write gate, off by default. integrity.owner_scope_writes in Brain/_brain.yaml (off | warn | fail, default off) refuses a caller-named owner that differs from the caller's resolved identity on the preference and note lanes (warn allows and records one decision-ledger row); a document owner_write verdict composes most-restrictive-wins with the gate; write-time integrity.

  • Named MCP tokens, optional. o2b mcp token mint|rotate|revoke|list keeps 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-key stays valid as the operator master credential, and mcp_tokens_required / OPEN_SECOND_BRAIN_MCP_TOKENS_REQUIRED (default false) 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, and o2b 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: false suppresses the ambient extraction lane with a counted ambient-withheld event (absent keeps today's behavior), and guardrails.ambient_ttl_days stamps an expiration_date on 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 at Brain/decisions/open-<slug>.md; resolve mints the real type: decision page, discard closes 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

TopicPage
Mental model, vault layout, dream pass, safety propertiesdocs/how-it-works.md
Every CLI verb and flagdocs/cli-reference.md
MCP protocol, tools, writer splitdocs/mcp.md
Brain store: setup, sync, migration, the human viewdocs/brain-store.md
Architecture and configuration modeldocs/architecture.md
Updating (o2b update) and upgrade notesdocs/updating.md
Observability eventsdocs/observability.md
Metrics data contractdocs/metrics.md
Cross-project pointerdocs/cross-project-pointer.md
Scheduled Brain digest (Hermes cron)docs/hermes-cron.md
Stability policydocs/stability.md
Origin of the ideadocs/idea.md

Project

CHANGELOG · Security policy · Source on GitHub · MIT License

docs/observability.md

Observability contract

Open Second Brain records what it did - learning events, recall decisions, served context, session activity - across a small set of file-based surfaces inside the vault. This document is the contract for anyone consuming those surfaces: which events exist, when they fire, what correlates them, what safety guarantees their payloads carry, and how the record shapes evolve. Everything here is verified against the source files named in each section.

Surfaces at a glance

SurfaceLocationWriterFormat
Brain logBrain/log/<date>[.<device-id>].md + JSONL sidecarappendLogEvent() in src/core/brain/log.tsMarkdown line + JSONL row per event
Continuity storeBrain/log/continuity/<month>[.<device-id>].jsonlappendContinuityRecord() in src/core/brain/continuity/store.tsone JSON record per line
Idempotency ledgerBrain/logs/idempotency/<month>[.<device-id>].jsonlrememberKey() in src/core/brain/idempotency-ledger.tsone key -> content hash record per line
Decision ledgerBrain/logs/decisions/<month>[.<device-id>].jsonlappendDecisionLedger() in src/core/brain/permissions/ledger.ts; read back by o2b brain permissions ledgerone JSON decision row per line (see "The decision ledger" below)
Preference mutation auditBrain/log/pref-audit/<pref-id>/device[.<device-id>].jsonl (legacy flat Brain/log/pref-audit/<pref-id>[.<device-id>].jsonl still read)appendPrefAudit() in src/core/brain/pref-audit.tsone JSON record per line
Session lineage ledgerBrain/.state/session-lineage[.<device-id>].jsonl (+ session-lineage-gaps[.<device-id>].jsonl)recordLineageObservation() in src/core/brain/lineage/ledger.tsone JSON record per line, sequence-numbered and hash-chained per file
Session lifecycle auditBrain/log/session-lifecycle/<ISO-week>[.<device-id>].jsonlcaptureSessionLifecycleEvent() in src/core/brain/session-lifecycle.tsJSONL audit rows
Other audit weeks<ISO-week>[.<device-id>].jsonl under Brain/log/hygiene/, Brain/log/secret-custody/, Brain/log/schema-mutations/, Brain/log/watchdog/ (fallback .open-second-brain/watchdog-audit/) and .open-second-brain/hook-audit/appendAuditRecord() in src/core/reliability/audit.ts, the one writer every audit site callsJSONL audit rows
Fixed-name ledgersBrain/log/query-demand, Brain/log/recurrence-support, Brain/log/capture-decisions, Brain/skill-proposals/verifier-rejections, Brain/preferences/pref-<slug>.history, Brain/projects/git/<repo>/commits and .open-second-brain/maintenance-runs, each <stem>[.<device-id>].jsonlthe owning module's path helper through shardedFileName() in src/core/brain/ledger-shards.tsone JSON record per line
Bench runs<runs-dir>/<run-id>/ (default .open-second-brain/bench-runs/; excluded from git by the guard block o2b brain init seeds into the vault .gitignore)runMemoryBench() in src/core/bench/phases.tscheckpoint.json, per-question results, report.json
Failed automatic upgrade.open-second-brain/self-heal-upgrade.json (per device, not synced with Brain/)recordSelfHealUpgradeFailure() in src/core/maintenance/self-heal-upgrade-state.tsone JSON object: failed_at, error, consecutive_failures, pending, retry_after; read by o2b doctor (self_heal_upgrade), o2b brain status and o2b brain upgrade --dry-run
MetricsBrain/metrics/<surface>[.<device-id>].jsonlappendMetric() in src/core/brain/metrics.tsone run-level JSON record per line (see docs/metrics.md)
Signal archiveBrain/inbox/archived/sig-*.mdthe dream pass (archiveStaleSignals() in src/core/brain/dream-apply.ts, policy in src/core/brain/signal-archive.ts)the inbox signal file, moved byte for byte; the moving run's dream log event lists every id under archived_signals
Dedup index cache$OPEN_SECOND_BRAIN_DEDUP_CACHE_DIR, else the user cache dir open-second-brain/dedup-index/<vault-digest>.json (outside the vault; OPEN_SECOND_BRAIN_DEDUP_CACHE=0 disables)writeDedupIndexCache() in src/core/brain/dedup-index-cache.tsone JSON file per vault: per directory its listing digest, count and mtime, per signal file its size, mtime, id and dedup_hash; derived, safe to delete
The per-device shard rule

Every append-only ledger above is one file per period (or per key, or per surface) PER DEVICE. The vault is replicated with Syncthing, and two machines appending to one file inside a single sync window produce a .sync-conflict-* copy that no reader merges — so the rows split silently across two files. The shard suffix removes the contention instead of resolving it after the fact: a device only ever appends to the file its own id names.

The grammar is <base>[.<device-id>].<ext>, and it lives in exactly one place, src/core/brain/ledger-shards.ts. The device id is the lowercase slug resolveDeviceId() produces (src/core/config.ts), which is device-local config and never synced. A file with no device suffix is the shard with the EMPTY id: legacy history stays readable, no migration renames anything, and a vault whose device id resolves empty (O2B_DEVICE_ID="", or a config that cannot be read) keeps writing exactly the bytes it always did.

Every reader merges every shard it finds, in the one order all devices agree on: the ledger's own sort key, then the shard id, then position within the shard. The shard id is the tie-break rather than the arrival order because arrival order is precisely what differs between two Syncthing peers — so two machines reading the same set of shards return the same sequence. The continuity store adds its content-hash record id as a last resort after the shard id, which is what keeps a single-shard vault reading back in exactly the order it did before sharding existed.

Two names are never shards. A *.sync-conflict-* copy is excluded from every listing even when its middle segment fits the shard-id shape (the sync-conflict prefix is reserved in the device-id validator for the same reason), and o2b brain doctor reports any leftover copy under any ledger directory as sync-conflict-log, naming the directory so the union+dedup merge has an address. A name whose base does not match the ledger's own layout is not that ledger's file at all — bases go into the grammar as patterns, or as escaped literals where the base is a value (a preference id may legitimately contain a dot).

The audit weeks and the fixed-name ledgers adopted the grammar in 1.63.0. Their readers merge every shard: the maintenance journal, the git commit store and the recurrence-support ledger interleave shards by each row's own time without ever reordering the rows of one shard (so a single-shard vault reads exactly as before, and the order-sensitive recurrence fold never replays one device's purge before another device's earlier event), the git store keeps one copy of each commit and tag, the query-demand read keeps its timestamp order, the preference edit history reads shards in shard-id order, and the schema-pack digest picks the newest record across every week shard. The capture-decision and verifier-rejection ledgers have no reader; only their writers changed. A rewrite touches only the shard this device writes: query-demand compaction caps that shard, and the maintenance-journal cap sweep rewrites only the local shard, so the journal cap applies per device. o2b state status reports a sharded fixed-name ledger present when its legacy file or any shard exists, and the doctor's sync-conflict-log sweep covers the directories of these two rows except the maintenance journal's .open-second-brain/ root. Where a swept directory also holds whole files (Brain/skill-proposals/, Brain/preferences/, a git repository's state.json), a conflict copy without the .jsonl extension is not a split shard: it keeps the sync-conflict-log code with a hand-resolve remedy instead of the merge instruction. A source-layout test fails when a new append-mode writer uses neither the shard helpers nor a named allow-list entry.

The session lineage ledger is the one with an extra invariant: its sequence numbers and hash chain are per FILE, so a per-device shard is a per-device chain, compaction is per shard, and verifyLineageLedger verifies each shard separately and reports each finding under that shard's own path. A break in one machine's ledger says nothing about another's.

The prompt_prefix metric surface measures STRUCTURAL prompt-prefix stability (whether the kernel handed the agent a byte-stable, cache-eligible preamble across a generation pass), never a provider's cache-hit rate the kernel cannot observe. It stores only the SHA-256 hash and length of the prefix, never the raw prompt - the same payload-safety rule as generation_report. See docs/metrics.md.

Brain log events

appendLogEvent() writes one human-readable line to the daily Markdown log plus a structured JSONL sidecar row. These events are always-on: each one is the audit trail of the operation that produced it, never optional telemetry. Kinds (from BRAIN_LOG_EVENT_KIND in src/core/brain/types.ts):

KindFires when
dream, promote, retire, noted-redundant, signal-suppressed, skip-corrupted-frontmatter, reconcilethe deterministic learning pass runs. The dream event lists signals moved to inbox/processed/ under moved_to_processed and signals moved to inbox/archived/ (older than dream.contradiction_window_days, never consumed) under archived_signals
feedback, apply-evidence, force-confirmed, rejecta taste signal or evidence event is recorded
pin, unpin, rollback, merge, upgradeoperator-facing vault maintenance
freeze, unfreezean operator stopped or reopened the content lane for every device that syncs the vault (o2b brain freeze / o2b brain unfreeze). One event per real transition only - a second freeze, or an unfreeze on an open vault, writes nothing. freeze carries the reason and the device_id it was set from; unfreeze carries the frozen_at, by and reason read off the marker before it was removed, which is the only place they survive the file
write-refuseda write was refused because the vault is frozen. Appended at the MCP tool boundary - the guard that refuses sits below the log module and cannot append without closing an import cycle - one per refused call, carrying the tool, the agent and the marker's reason. The Brain log itself keeps writing while frozen (the audit write lane, WRITE_LANE.audit in src/core/brain/freeze-marker.ts), because a freeze that silenced the log would erase the record of itself
scan-inline, import-session, import-claude-memorycapture and import operations
notea narrative milestone is recorded (brain_note); since v1.81.0 also the answer-quote-check Stop hook's enforce trail - exactly one event per turn that found unverified quoted spans, carrying hook: answer-quote-check, the phases that ran, the checked and unverified counts, and the findings as capped one-line previews
note-writea vault note is created, updated, appended to, or reverted
session-lifecyclea captured lifecycle event also produced Brain writes
decision-opena question was parked with enumerated options at Brain/decisions/open-<slug>.md, ahead of the decision it may become. Payload carries the open wikilink, the title, the option count and the agent. Distinct from decision-record because nothing has been decided yet
decision-resolvedan open decision was closed by choosing one of its options, minting a real type: decision page. Payload carries the open wikilink, the minted decision wikilink, the choice and the agent. The minted page itself carries the usual decision-record event; this event records the transition
decision-discardedan open decision was closed WITHOUT a decision. Payload carries the open wikilink, the reason and the agent. Separated from decision-resolved so "never decided" is machine-filterable against "decided as X"
ambient-withheldan ambient extraction capture was suppressed by operator consent (guardrails.ambient_writeback: false). One event per suppressed capture, carrying the count of signals the capture would have written - never their content. Keys absent log nothing, because the lane ran as today
lessons-approved, lessons-denied, lessons-promoted (agent-presence, t_1e952e43)the operator records a lesson-promotion decision or a managed block receives lesson content. Operator CLI only - o2b brain lessons approve / deny / refresh-blocks; no agent-facing surface can mint these events, so the audit trail of what entered an agent rules file is operator-authored by construction. lessons-approved carries the pref wikilink, the targets, the pinned approved_content_hash and the consent verdict; lessons-denied carries the pref and the verdict; lessons-promoted carries the targets that changed and the prefs delivered, one event per write that landed (a no-op refresh emits nothing). The doctor grades managed blocks in the four-state vocabulary absent | current | drifted | stale-lessons (registered in the verdict-vocabulary census), where stale-lessons is an approval whose pinned hash no longer matches the live text; the counts ride the brain status snapshot (lessons.approved/denied/stale) without a new surface
Note writes

Every note mutation funnels through two seams - createNote and the write-batch kernel - and each one appends exactly ONE note-write event. The payload is all strings, because the log payload contract admits string | string[] and nothing else:

FieldWhat it holds
write_idnw_<14 timestamp digits>_<16 hex>, derived from the event body, so two devices that record the same write agree on its id
opcreate, update, append, or revert (NOTE_WRITE_OP)
targetvault-relative POSIX path of the note
hash_beforesha-256 of the bytes replaced, or the literal absent when the target did not exist - deliberately not the sha-256 of zero bytes, which an empty FILE would also produce
hash_aftersha-256 of the bytes now on disk, or the same literal absent when the write left no file - which only a revert that removes a note the selected writes created produces, paired with bytes_after: 0
bytes_before / bytes_afterbyte sizes, rendered as decimal strings
agentresolveAgentName() against the config the writing surface runs under; caller-asserted by construction, as src/core/write-binding/index.ts states

The device is NOT a payload field: it rides on the log shard name exactly as it does for every other event, and origin_channel is stamped by the appender. listNoteWrites (src/core/brain/notes/write-log.ts) reads the record back and reports the shard as the device, the empty string being the legacy un-sharded log rather than an unknown machine. o2b brain writes and the brain_writes MCP tool are the two surfaces over it, and brain_agent_query --kind note answers "which notes did this agent touch" off the same events.

Order, and the gap the receipt names. The bytes are written FIRST and the event appended second, so a log failure never fails a write that has already landed - the snapshot-event precedent, with the silence removed. recordNoteWrite never throws: it returns write_id: null with an audit_reason, and every note-write result and MCP receipt carries both. A write that authored no bytes - a skipped create (if_exists: "skip") or a byte-identical rewrite - records nothing and carries no write_id at all; the receipt reports the skip through the op's own flag (updated: false / appended: false).

Before-image store. The bytes an update or append replaced are kept at Brain/.state/write-images/<sha256>, stored BEFORE the atomic write so a crash or a failed store never leaves the prior bytes replaced without their copy - a byte-identical rewrite is decided first and stores no image, because the prior bytes stay on disk and an image would claim a replace that never happened - and skipped when an image of that content already exists - so a note toggled between two states costs two files, not two per write. The store is a STATE_SURFACES row inside the snapshot region; o2b brain writes prune-images [--older-than-days N] [--dry-run] bounds it by file age, default 30 days (WRITE_IMAGE_RETENTION_DAYS). An image is a copy of note content the snapshot region already archives, so removing one loses no content - it only narrows how far back a revert can reach, which a revert plan reports by name.

Revert. o2b brain writes revert (--agent A | --device D | --path P) [--since --until] reads the same events back and plans what undoing them would do; brain_writes action plan_revert returns the same plan and never applies it. A selector naming none of agent, device or path is refused by name (unbounded_selector): a time window alone selects every write by every agent on every machine, which is a vault rollback and has its own verb. Per target, over the selected writes S (oldest..newest) and every recorded write A on that target:

VerdictWhen
refuse / unrecordedthe current bytes cannot be read at all, so no drift verdict is possible
refuse / driftsha-256 of the bytes on disk is not the hash_after of the newest write in S - including the target being gone when it should be there
refuse / interleaveda write in A \ S sits between the oldest and newest write in S in the merged total order - (timestamp, shard id, line), the order readLogDay already yields. Timestamps are second-precision, so "between" is a position and not a clock comparison: line order inside one shard IS append order, and three writes in one second on one device are perfectly ordered. The one tie that is not evidence is a same-second tie ACROSS shards, where the merge falls back to the shard id - a name, not a clock - and two devices could have written in either order. So a non-selected write sharing a second with a selected write is trusted only when it shares that write's shard; a cross-shard tie is interleaved on doubt
refuse / already-revertedthe target already holds exactly what the revert would produce
refuse / image-missinga restore's before-image is not in the store, or no longer hashes to its own name
deletethe oldest write in S has hash_before: absent AND no write in A precedes it - the selection brought the note into existence
restoreotherwise: put back the before-image of the oldest write in S

A refused target is reported, never skipped, and the plan's digest - sha256(canonical JSON of { selector, entries }), deliberately NOT covering planned_at - covers refusals too, so an operator who applies a digest applies the plan they read. --apply <digest> re-plans from scratch and refuses digest_mismatch before any byte moves when the vault changed in between; a plan with no actionable entry is refused nothing_to_apply; a frozen vault refuses the whole apply by name before anything is read. Everything that does run goes inside ONE withDestructiveSnapshot under the note-revert reason, so a revert that half-succeeds still has one recovery point covering the state before all of it. Each restored or deleted target keeps a before-image of the bytes it replaced and is recorded through recordNoteWrite with op: revert - a delete as hash_after: absent, bytes_after: 0 - which is what makes a revert attributable and itself revertible: reverting the reverting agent puts the bytes back.

The log chain

Once the log is the attribution record for every write, a line someone deleted or edited has to be detectable. Every JSONL row appended from v1.55.0 on carries two extra keys after payload:

  • h — sha256(canonical JSON of { prev, ts, kind, payload }), this row's chain hash;

  • prev — the h of the previous chained line in the same shard, or null at that shard's genesis.

prev is inside the hash rather than beside it, which is what makes the structure a chain: editing any earlier line changes every later hash, so a tampered history cannot be re-linked without rewriting the whole shard from the edit forward. The appender computes the link from the shard text it already holds under the directory lock, which is also what makes the link correct — two appenders reading the same head would otherwise write two lines claiming the same predecessor.

One chain per shard. Two machines append to two files that Syncthing delivers in whatever order it likes, so there is no total order to chain across; the chain's unit is one file and a break in one device's shard says nothing about another's. The markdown twin is a derived rendering and is NOT chained — a human-editable view is the wrong thing to hold to a hash, and the JSONL sidecar is the machine-primary surface every reader already prefers.

Report-only. readLogDay reads ts, kind and payload and nothing else, so a broken shard still yields every event it holds. A log that refused to be read because someone edited it would be a worse outcome than the edit. What the chain buys is that the edit has a name, a file and a line number.

o2b brain log verify [--json] walks every shard and reports the first break in each, by path and line: hash-mismatch (the line was edited after it was written), prev-mismatch (a line between it and its predecessor was removed or reordered), malformed (the line carries no usable chain link where the chain had already started). It exits 1 when any shard does not link up. o2b brain doctor emits one log-chain-broken warning per broken shard with the same next command. Neither repairs anything: the only way to make a broken chain verify is to rewrite the history it records, which is the act the chain exists to detect.

Rows written before the chain shipped carry no h. They are counted as legacy and are clean while they precede the chain — the first chained line after them anchors the shard with prev: null. The same shape after a chained line is not history but a line whose links were stripped, and it is reported. The head of a shard is not exempt either: the Brain log never compacts, so a first chained line naming a predecessor means the head of the file was cut off. Sync-conflict copies are excluded from verification — they are the doctor's separate sync-conflict-log finding, and their chain never held by construction.

The decision ledger

The Brain log, the pref-audit, the idempotency ledger and the continuity store each record what happened; none of them answers "which rule allowed, asked or denied this write". The decision ledger (Brain/logs/decisions/, write-side-trust wave) is the queryable record of that answer: one JSONL row per gate disposition that was not a plain publish, written by the gate that produced the verdict. appendDecisionLedger() (src/core/brain/permissions/ledger.ts) appends to the month/device shards on the shared shard grammar - the per-device rule and the merged (timestamp, shard id, line) read order are exactly the ones "The per-device shard rule" above states.

The row shape is closed: ts, actor (the resolved principal), via (token, config or operator - where the identity came from), action (write, ingest or owner_write), target, verdict (the disposition - allow, ask, deny, a gate mode such as warn, or a refusal token), source (the deciding rule: an entry id, an agent or role name, default, or a gate key such as integrity.owner_scope_writes), reason (the named token), and the optional tool and correlation_id that join a row to the other trails the way event-trace.ts joins log events.

When rows land is deliberately quiet:

TriggerRows
No permissions document, every gate key offnone - default-silent like every ledger
A permissions document resolves ask or deny for a staged-lane writeone row naming the entry, role or default that decided
A permissions document resolves allowone row ONLY when the document sets ledger.record_allows: true
integrity.owner_scope_writes: warn allows a caller-named foreign owner on the preference laneone row carrying the gate key as source

The append contract is absolute: a failed append NEVER throws and never blocks the verdict it records. The ledger rides behind gates that must refuse or stage a write even when accountability cannot be written, so every failure - an unwritable directory, a lock another writer holds - comes back as { logged: false, audit_reason } that the caller surfaces; the receipt carries the reason rather than claiming a record that does not exist.

o2b brain permissions ledger --actor <name> --action <a> --verdict <v> --since <iso> --until <iso> --limit <n> reads the merged rows back (--json for the raw rows); every filter is optional and applied after the deterministic sort, so two devices listing the same shards agree on the order. The ledger is append-only and has no prune verb: retention follows the same open question as every sharded ledger in the vault.

Continuity record kinds

Every continuity record shares one envelope, and exactly these eight fields in this order: schema, id, kind, createdAt, sourceRefs, payload, private, redacted. The table marks how each kind is gated - this is the always-on vs opt-in matrix, verified against the call sites named in the right column.

KindGateEmitted by
recall_telemetryopt-in per call (telemetry option/param); the prompt-time hook emitter is gated by recall_inject_enabled insteadpackContext (mode: context_pack), buildPreCompressPack (mode: pre_compress), brain_search MCP handler (mode: search), brain_query MCP handler (mode: query, since v0.40.0 - payload carries the query kind only, never the supplied preference id / topic / timestamp), and since v1.46.0 the UserPromptSubmit recall-inject hook (mode: search, channel: hook, mapping its inject / abstain / error decision onto ok / empty / error; with the optional recall_inject decision-model use in shadow or enforce, metadata gains a decision_model object - mode, outcome (ok, not_sent or degraded), latency_ms, notes_dropped, abstained - and the abstain reason decision_model_abstain, never prompt or note text; see docs/decision-models/recall-inject.md)
context_receiptopt-in per call (receipt option/param)packContext, buildPreCompressPack
gate_telemetryconfig key recall_gate_telemetry (default off)brain_recall_gate MCP handler; a call carrying decision_answerable while that decision-model use is on adds adequacy_level and decision_answerable (probability, disagrees); since v1.69.0 a call carrying turn_id records it under turn_id, which the read-model lifts like every other turn id; since v1.82.0 a call that carried a measured quality also records the graded surfacing key - key (null exactly where nothing was measurable), tier and the channel the decision arrived over - and the summary counts the measured records by tier (by_tier)
negative_recallconfig key recall_gate_telemetry (default off - the SAME gate as gate_telemetry above, so one config decision governs both observations the recall gate makes)brain_recall_gate MCP handler on the zero-usable-result path only (emitNegativeRecallTelemetry in src/core/brain/gate-telemetry.ts) - one record per gate call whose caller reported no usable result, carrying the typed corpus verdict (state, complete, the named unknown_reason when the verdict is unknown, reason) plus the reduced coverage receipt when one could be built (coverage_digest, documents, chunks, schema_version, embedding_signature, last_indexed_at, scope, unindexed_roots). Two receipt fields are not copied into the payload: index_path is deliberately dropped - a host filesystem path is not vault content and these records sync between devices, and the digest already binds it; the embeddings census the receipt carries is likewise absent from the payload and likewise bound by coverage_digest, so a reader comparing two records still sees a keyword-only index differ from a fully embedded one. Same prompt invariant as gate_telemetry: a SHA-256 prefix (prompt_hash) and the length (prompt_chars), never the prompt. A call with usable results writes NO record, so a corpus statement is never invented for an attempt that had one
generation_reportopt-in per call (enable) or config key generation_trace_enabled (default off)inbound agent report after a generation handoff (brain_generation_reports action record, o2b brain generation-reports record)
mcp_route_latencyconfig key mcp_route_metrics_enabled (default off)the MCP server's tool-call seam (MCPServer.invokeToolHandler in src/mcp/server.ts) - one record per tools/call and per CLI-bridge callTool; payload carries the tool name, scope, status, duration, and argument KEY NAMES only, never argument values, plus the optional decision_ms and allowlisted stages timings
decision_model_callthe optional decision model is active (decision_model_enabled: "true" AND the named key variable set, which a self-hosted laya or openjev server on loopback does not need, and no vault opt-out); an install without the feature writes nonerunDecision in src/core/decision-model/run.ts - one record per attempted decision request: use, mode, provider, model (the answering version), calibrated, question_count, candidate_count, input_tokens, output_tokens, cost_usd, cost_source (reported, estimated or unknown), latency_ms, outcome (ok or a degrade reason), state_hash (sha-256 of the redacted state), origin (eval or ping) when not an ordinary use call, plus per-use identifiers (a rerank adds head_size, withheld_count, budget_dropped_count, frontmatter_only_count when a frontmatter-only chunk was not sent, injection_suspected in enforce, answerable_probability, and in shadow heuristic_order and decision_order as paths, in enforce decision_permutation as head positions; every other use adds its own identifiers and counts, described on its page under docs/decision-models/). Never the state, a passage, a prompt or a question text. Fail-open: a failed write never fails the search. The daily cost gate and o2b decision-model report read these records
decision_model_extract_committhe optional decision model is active and the extract_prefilter use is not offcommit phase of brain_extract_signals / o2b brain extract-signals (src/core/brain/extract-signals-prefilter.ts) - one record per commit that wrote items: session_id, mode, written_count, source_turns (the cited turn ids that name a real turn of the session) and without_source_turn_count; turn ids and counts only, never text. Read by o2b decision-model report --use extract_prefilter for its regret metric
token_impactconfig key token_impact_ledger_enabled (default off)brain_token_impact action record (emitTokenImpact in src/core/brain/token-impact.ts) - one sample per context pack; payload carries the tokenizer-exact prompt-token counts (baseline_tokens, packed_tokens, signed delta_tokens), the method label (exact/fallback), an optional opaque pack_id, an optional source attribution (decision_model:<use> for savings of a decision-model use), and the optional modeled inference-avoidance estimate - never raw prompts or recalled text
token_impact_outcomeconfig key token_impact_ledger_enabled (default off)brain_token_impact action outcome (recordTokenImpactOutcome) - one first-pass/repair/retry post used only to calibrate the modeled ledger
context_pack_outcomeconfig key context_pack_outcome_enabled (default off)brain_context_pack_outcome action post (postContextPackOutcome in src/core/brain/context-pack-outcome.ts) - one compact per-sample 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), never merged; payload carries counters + an opaque sample_id only, never raw prompts or recalled text, and a caller-omitted field is not invented. One post writes THREE records, all joined on the same sample_id: this row, a token_impact_outcome calibration post, and a context_pack_evidence row (both listed separately below and in this table). The thin wrapper emitContextPackOutcome returns only this row; the two composed records are written either way
context_pack_evidenceconfig key context_pack_outcome_enabled (default off - the same gate as the outcome row it accompanies)composed by every brain_context_pack_outcome action post (recordContextPackEvidence in src/core/brain/context-pack-evidence.ts) - one row per outcome row, joined on the same sample_id, carrying the evidence the kernel reads back off disk (the context_receipt that id names: final_text_hash, item_count, final_text_chars) and a closed verdict over the acting agent's optional claim: match, mismatch, unclaimed (evidence obtained, nothing asserted), or unresolved. A mismatch lists the drifted fields and is RECORDED, never resolved - the outcome row is neither rejected nor silently corrected by it. An unresolved verdict names why through a separate token, sample_absent (the log was read and holds no such receipt) or evidence_unreadable (the log could not be read at all), so absence is never reported as inability. Payload carries those tokens + the opaque sample_id only, never raw prompts or recalled text. NOT an independent attestation: the agent id on the row is self-asserted and this same process wrote the receipt it now reads. The post response echoes the payload under evidence, so a contradicted claim is visible to the poster and not only to a later reader
ingest_dedupalways-on, fail-open; written ONLY by a run that actually dropped somethingthe three ingest runs that drop an item on an exact sha-256 dedup_hash collision: captureSessionLifecycleEvent, scanInline, importSession (emitIngestDedupReport in src/core/brain/dedup-telemetry.ts) - one record per run, payload carries the surface, the run's exact_deduped total, and a by_source map keyed by the session id or vault-relative note path being re-ingested. A run that deduped nothing writes NO record, so a real zero stays distinguishable from an absent measurement. Every count is an EXACT-hash drop; the semantic detectors nominate merge candidates and never drop, so no semantic figure appears here
session_turn, session_summary_nodealways-on within its operationsession-recall import (src/core/brain/session-recall.ts) - the write IS the operation
pre_compact_extractalways-on within its operationexplicit pre-compact extraction (src/core/brain/pre-compact-extract.ts)
source_invalidationalways-on within its operationsource drift detection (appendContinuitySourceInvalidation)
skill_rewrite_eval_gradealways-on, one record per COMPLETED benchmark-gated skill rewrite gate run (agent-presence, t_635e3e72); a run that ends before the gates complete - a promote still awaiting its single held-out check - writes nothingrecordSkillRewriteGrade in src/core/brain/skill-proposals.ts, called from o2b brain skill-proposals rewrite-score: payload carries the skill name, the spec hash and tasks hash the run verified against, the metric (tasks_passed_rate), the seed and split rule, the final verdict (promote / hold / not_enough_evidence, where a hold is also what a missed held-out check downgrades a promote to), the baseline and candidate rates, and per-task rows (task_id, passed, failed_checks) for the dev and held-out arms. Identifiers, numbers, booleans and check ids only - never task text and never transcript text; the payload is rebuilt field by field at the boundary so nothing else in the caller's object survives. Kept never-folded so an operator can audit why a rewrite landed or held; the MCP status surface reads the same rows read-only

Session lifecycle capture (captureSessionLifecycleEvent) writes audit rows and Brain log events, and no continuity record at all unless the event actually dropped a repeat - in which case it writes exactly one ingest_dedup record. Both halves of that boundary are pinned by regression tests (tests/core/brain/continuity-emit.test.ts for the silent path, tests/core/brain/dedup-telemetry.test.ts for the dedup path).

Schema version and evolution rule

Since v0.39.0 every new continuity record is stamped schema: "o2b.continuity.v1" (CONTINUITY_SCHEMA_VERSION in src/core/brain/continuity/types.ts).

  • Records written before the stamp existed carry no schema field; every reader treats undefined as v1. Existing JSONL files are never migrated.

  • Additive optional fields do NOT bump the version. Renames, removals, or semantic changes bump to o2b.continuity.v2.

  • The record id (recordId(), sha-256 over kind + createdAt + sourceRefs + payload) deliberately EXCLUDES the schema field, so identical records keep identical dedup ids across the stamp transition. Locked by a known-answer test.

  • Consumers should go through the read-model (src/core/brain/continuity/read-model.ts), which absorbs the version dispatch, lifts session_id/turn_id into first-class fields, and applies the masking policy below.

Correlation IDs

FieldWhereStable join?
id (ctn_<stamp>_<sha16>)every continuity recordyes - content-hash dedup id
createdAtevery continuity recordordering key (store sorts by createdAt, then id)
sourceRefs[].id / .pathcontinuity recordsjoins records to vault artifacts
session_id, turn_idpayloads of session-scoped kinds; lifted by the read-modelyes - the cross-surface session join
handoffKind, handoffRefgeneration_report payload handoff; lifted by the read-modelyes - joins a generation report to the write-session / context-receipt / dream run it traced
timestamp + agentBrain log eventsper-event attribution
agentId, isSidechainSessionTurn, from a transcript that records delegationyes - the only thing separating a delegated sub-agent's turns from its parent's, because a sub-agent transcript reuses the PARENT's session id

Multi-agent vaults attribute writers via per-agent identity (brain_agent_query). Delegated sub-agents are correlated as of v1.49.0, and by the additive route this sentence used to promise: SessionTurn carries an optional agentId and an optional isSidechain: true, both emitted only where the transcript records them, so a flat transcript keeps its pre-delegation shape byte for byte. A sub-agent transcript reuses its PARENT's session id, so those two fields are the only thing separating the two agents' turns. The SubagentStop hook records the close under <delegator>-sub-<agent-id>; the agent_transcript_path the event also carries is deliberately not persisted, being a machine-local host path.

Absence is its own identity. Since v1.81.0 a payload with no string session_id is a no-session record, never a member of a blank bucket: the readers no longer coerce the missing key to "", so scoped recall excludes such records (the lineage ledger already omitted blank declarations, so the scope set never held them), grouping sites skip them instead of lumping every unattributed record from every unrelated lane together, and the brain_session_summary digest omits its session_id key rather than answering "". o2b brain doctor counts the rows older writers left under the legacy coercion with the count-only, fail-soft blank-session-identity check - one issue per affected record kind, no fix field: the continuity store is append-only, and rewriting it is a backup-gated design, not a doctor step.

Payload safety

Every continuity payload passes safeContinuityPayload() (src/core/brain/continuity/redaction.ts) before it reaches disk:

  • <private>...</private> regions are stripped and replaced; the record is flagged private: true.

  • Secret-shaped tokens are redacted to ***REDACTED***; the record is flagged redacted: true.

  • The sanitized payload is deep-frozen; nothing mutates it after the flags are computed.

  • gate_telemetry never stores the raw prompt by construction - only a SHA-256 prefix (prompt_hash) and the length (prompt_chars). The test suite asserts this against the persisted files, not just returned values. negative_recall shares that construction (same module, same hash helper) and adds one rule of its own: the coverage receipt it carries drops index_path, so no host filesystem path replicates with the vault.

  • generation_report follows the same rule: the handoff prompt is hashed and counted but never persisted - only prompt_hash (full SHA-256 hex) and prompt_chars land on disk, plus token counts. The kernel never calls an LLM; the report is the agent's INBOUND record after it performed a generation for a brain handoff, so the local token estimate (local_estimate.input_tokens) is always present while the agent-reported usage block is present only when supplied - absent is reported as absent, never fabricated. A grep-guarded regression test pins that no fetch/provider HTTP call exists under src/core for this feature, and a persisted-file assertion confirms no raw prompt survives.

  • mcp_route_latency carries an optional decision_ms (wall time spent in decision-model requests during that call) only when the call made at least one; records from an install without the feature are unchanged.

  • mcp_route_latency carries an optional stages array of { name, ms } entries (v1.74.0): the wall time the call spent in each write stage, summed per name in first-seen order and rounded to 0.1 ms. Names come from the closed ROUTE_STAGE allowlist in src/core/route-scope.ts - validate, idempotency_lookup, near_duplicate_lookup, document_write, idempotency_remember, log_append, preference_write, write_receipt, lint - and emitMcpRouteLatency drops any other name, any negative value and any non-finite value before the record is written, so no path, topic or other free text can land on disk. The key is omitted when no stage ran, so a record from a route without stages is byte-identical to before. Stage timings never appear in the response of the call being measured; they live only in its continuity record, which brain_route_metrics reads back. Stages can nest, so they are not a partition of duration_ms: lint wraps the receipt lint, and the near-duplicate scoring inside that lint is also counted under near_duplicate_lookup. The same milliseconds then appear under both names, lint includes that scoring, and the stages of one record can add up to more than its duration.

  • Routes that emit stages, by the write helpers they reach: every route that writes a signal through writeSignal gets validate and document_write, plus idempotency_lookup and, when the signal is written, idempotency_remember only when the signal carries an idempotency key, which only brain_feedback accepts (its idempotency_key argument). That covers brain_feedback and brain_extract_signals; the latter writes one signal per accepted item, so its stage values are summed across items. brain_feedback adds near_duplicate_lookup (the write-conflict advisory), log_append (the feedback log line, and the second log line under force_confirmed; the advisory's own log line is not timed) and preference_write with force_confirmed; a retry that the idempotency key dedupes stops after idempotency_lookup. Every note write that ends in a write receipt - brain_create_note, brain_update_note, brain_append_note and brain_write_batch - gets write_receipt for each note it writes or rewrites, document_write for each note it creates, and, when at least one page was written, lint and near_duplicate_lookup from the receipt lint (the latter also covers the keyword-index widening when that is on). One route scope per tool call carries both decision_ms and stages; with mcp_route_metrics_enabled off no scope is opened and every stage helper reduces to a plain call.

  • Hook capture is not an MCP route: lifecycle hooks call captureSessionLifecycleEvent directly and hand the dedup walk to a detached worker, so they never pass through invokeToolHandler and carry no stage timings.

  • mcp_route_latency records the route (tool name), MCP scope, status, duration, and the sorted set of argument KEY NAMES only - the tool's own JSON-Schema property names, never operator-supplied argument values - so no prompt, note body, or preference id can land on disk. A persisted-file test asserts key names survive while values never do.

  • The masking policy lives in the read-model (src/core/brain/continuity/read-model.ts) and governs only the readers that go through it: records flagged private are dropped by default and kept only on explicit request; masked text is never un-masked. That is FOUR modules - the o2b brain continuity verbs (trajectory export and listing), the bench harness, brain_event_trace, and skill-usage aggregation. Twenty-two other modules call listContinuityRecords on the store directly and see raw records: no private drop and no schema-version dispatch. The policy is read-model-wide, NOT store-wide. Both lists are asserted by name in tests/core/brain/continuity/reader-census.test.ts, which fails when a module joins or leaves either set, and both COUNTS are pinned there as equalities rather than floors - the direct floor used to sit at twenty against a reality of twenty-one, which is how the number in this sentence came to be wrong. Auditing the direct readers is tracked separately.

  • A hook's tool_response is READ and never written. Lifecycle capture consults it to decide whether the call it is reacting to actually succeeded - a failed brain_feedback must not be replayed, because replaying it mints the record the tool declined to create - and the decline is counted rather than swallowed. The response body itself is arbitrary tool output and never reaches a payload, so declaring it in the hook contract costs nothing on disk.

  • private is the ONLY visibility signal a record carries, and it is inferred from a <private> region in the payload rather than declared. A caller-declared scope field was built for this envelope and removed before release: nothing in the codebase could supply one - no tool argument, no command-line flag, no config key and no library entry point - and its single vocabulary member would have narrowed a record's visibility rather than widened it, since a declared scope is dropped by every reader that does not name it. Adding a declaration to the envelope is a schema change; tests/core/brain/continuity/record-envelope.test.ts pins the eight fields and their order so the next attempt has to be argued for.

Fail-open rules

Telemetry must never block or fail the primary operation. The guarantee is structural, not conventional: gated surfaces route through emitGatedTelemetry(gate, build) (src/core/brain/continuity/emit.ts):

  • gate off (undefined/false/null): the build thunk is NEVER invoked - no payload object, no hash, no write on the no-consumer path;

  • a throwing thunk or store write is swallowed and reported as null; the context pack, pre-compress pack, search response, and recall-gate decision all complete normally with the telemetry field simply absent.

No-consumer regression tests cover each gated surface, so a future call site cannot silently regress to eager payload construction. Writers whose continuity append IS the primary operation (session-recall import, pre-compact extract) stay fail-fast on purpose - failing silently there would lose user data, not telemetry.

Reading the surfaces

  • brain_recall_telemetry (MCP) and o2b brain recall-telemetry list|summary|gate-list|gate-summary aggregate recall and gate records. Since v1.46.0 every recall_telemetry payload carries a channel naming the seam the recall came through - mcp, cli, or hook, a closed set. It is a property of the emit site rather than a caller argument: the MCP handlers stamp mcp, o2b brain context-pack --telemetry stamps cli, and the recall-inject hook stamps hook. Every emit site must name one, enforced at build time, so no new record lacks it. channel is clip-protected alongside session_id and agent_id, so an output-budget clip may not drop it. Filter on it with the channel argument (MCP) or --channel (CLI) on list and summary, and read the by_channel rollup on summary; a channel with no record is absent from that rollup rather than reported as 0. Records written before v1.46.0 carry no channel at all and are never migrated, so they count in total / by_mode / by_status but fall into no by_channel bucket, and any channel filter excludes them. o2b brain doctor crosses the rollup against per-channel install state and reports recall-channel-silent (installed, expected to deliver, silent for 30 days) or recall-channel-unmeasured (one half of that cross could not be read - the latter rides the uncertain stream and does not affect the exit code).

  • The recall-inject hook's optional decision-model filter (decision_model_uses: "recall_inject:shadow|enforce", see docs/decision-models/recall-inject.md) adds to the hook's recall_telemetry metadata and to its local hook-audit line a decision_model object: mode, outcome (ok, not_sent or degraded), latency_ms (the time the filter added to the prompt), notes_dropped and abstained (in shadow, what enforce would have done). Only the local audit line adds degrade_reason on a failure. In enforce a withheld brief is an abstain with reason decision_model_abstain (status empty). Its decision_model_call records carry note_paths (a note that may not leave the machine as (withheld)), helps, inject_any, abstained, notes_dropped, withheld_count, budget_dropped_count and budget_ms; an enforced brief that got shorter writes one token_impact sample with source: decision_model:recall_inject when token_impact_ledger_enabled is on. None of these carries prompt or note text. o2b decision-model report --use recall_inject reads them with p50/p95 added latency, timeout rate, abstain rate and notes dropped next to the hook's recall telemetry.

  • The recall-inject hook's local hook-audit line (action: recall_inject_decision) carries what only the hook knows, each field present only when it applies: config_invalid (the settings among recall_inject_max_notes, recall_inject_max_chars, recall_inject_time_budget_ms and recall_inject_confidence_floor whose value was rejected, so the built-in default ran; a value taken from the environment is named by its OPEN_SECOND_BRAIN_RECALL_INJECT_* variable), dedupe_sets (already_injected and digest_paths, the sizes of the session's recall set and SessionStart digest set the decision consulted, not counts of suppressed notes; absent without a session id or with recall_inject_dedupe off), ledger_recorded: false (an injected brief whose post-stdout ledger write failed, so its notes can repeat; absent when the write succeeded or no ledger applies), slices (one {name, outcome, notes} per declared slice, outcome being inject or that slice's abstain reason) slices_config: "invalid" (the vault's _brain.yaml failed to load; the search reads the same file, so that decision is an error with fault retriever_failed) and, since v1.73.0, retrieval_degraded (the retrieval trail codes of the searches behind the decision, each once in first-seen order, for example semantic-cost-unpriced when the hook's query embed was refused by the cost gate). The gap-promote hook's audit line carries the same retrieval_degraded field for its recall checks. Slice names are vault policy vocabulary and ride the local audit line only, never the synced recall_telemetry record. The abstain reasons all_already_injected and all_slices_abstained reach both surfaces through the existing reason field. See docs/decision-models/recall-inject.md.

  • brain_recall_telemetry operation: "cost" (MCP) and o2b brain recall-telemetry cost fold WRITE volume against the read telemetry above: memory-write ops (Brain-log feedback/apply-evidence/note verbs plus host_memory_write continuity records) are counted per period and reported alongside reads as a write-vs-read ratio, a write_heavy flag, and a rough weighted cost signal. Weights (--write-cost/--read-cost, default 1 each) model "some tools charge on write, some on read"; --write-heavy-ratio (default 1) sets the write-heavy threshold. Read-only; period-scoped by --since/--until. brain_create_note file creation emits no telemetry event today and is not counted.

  • brain_recall_telemetry operation: "dead_weight" (MCP) and o2b brain recall-telemetry dead-weight (since v1.80.0) read the other side of the same records: pages currently in scope that no recorded recall within the telemetry window ever surfaced. The denominator is the vault walk's included paths (current on-disk truth), not the index's document table, so a deleted-but-indexed page is never reported as dead weight; the retrieved set is the union of recorded top_artifacts and observed-use source references, windowed by --since/--until (the report states the earliest matching record). The payload carries pages_total, pages_retrieved, dead_count and a capped dead list behind the usual vault_path envelope, plus a gate_caveat: telemetry is opt-in and records only surfaced top rows, so the report claims "no recorded retrieval surface within telemetry history", never absolute never. Read-only; at remote reach hidden rows are dropped before total.

  • brain_context_receipts and o2b brain context-receipts inspect served-context receipts.

  • The score-aware tail trim (opt-in tail_trim_context_pack config key, default off) adds to a context_pack recall_telemetry record's metadata four counters, present only when the trim actually ran: tail_trim_dropped (tail items dropped whole), tail_trim_kept_by_query_terms (tail items kept because their query-term overlap nearly matched the top delivered score), tail_trim_rescued (former over-budget candidates re-admitted under the freed budget) and tail_trim_tokens_saved (tokens freed, net of the budget the rescue re-spent; never negative, because the rescue may spend only the budget the trim freed). The same counters ride the context_receipt budget block, and each dropped item's tail-trim skip reason surfaces as the telemetry gap tail_trim. An untrimmed pack carries none of these keys, so its records stay byte-identical.

  • The answer-quote-check Stop hook (since v1.81.0) is the answer-boundary counterpart of the receipts above: it verifies the quoted spans in the assistant's own answer against the evidence the turn actually had - the note a span's context names, else the injection ledger's delivered notes - and annotates the unverified ones. Verdicts: verified, not-in-note, source-truncated, note-unavailable, not-text; degradation never upgrades a verdict, so a truncated source arms source-truncated on a miss and an unreadable note arms note-unavailable, never an all-clear. The annotation rides the Stop additionalContext channel Claude Code renders; the other registered runtimes stay silent. Gated per phase by quote_check.recall|active|subagent in _brain.yaml, each off|shadow|enforce, default shadow; in enforce the turn also lands one Brain-log note event (the enforce trail in the table above), so findings survive the session. The check reads only the transcript's bounded tail window and writes nothing else; it never blocks and never edits a transcript.

  • The SessionStart hook writes one context_receipt per injection (host: "hook", trigger: "session_inject") with one item per source it injected. On a measured run its budget block carries inject_budget_chars (the configured value), budgeted_source_count and, since v1.70.0, scoped_rules_chars: the length of the rendered scoped operator rules block, 0 when none rendered. The scoped block is a budgeted source named scoped-rules, but it is subtracted from the budget rather than charged against the full ceiling: the active body was budgeted to inject_budget_chars minus scoped_rules_chars (never below zero). The budget block is present whenever the scoped block rendered, also when there is no active.md. A run that fell back to the last good cache records no budget block, as before, but keeps the standing-rules and scoped-rules items, which are produced outside the fail-open boundary. See "Scoped operator rules" in how-it-works.md.

  • With reground_parts_enabled on, the same receipt's extra.injection gains the chunked re-grounding fields: utf16_chars (the joined payload length in UTF-16 code units), part_ceiling_chars (the per-part ceiling it was cut to, null on a runtime without the reground-deliver carrier or without a session id), parts_total, parts_dropped (parts past the 8-part maximum that were never queued; when it is above zero the last delivered part ends with (context truncated: N further part(s) not delivered)) and over_budget (true when parts were dropped, when a part is still longer than the ceiling, or when the queue could not be written and the whole payload went out as one part). That last case also sets reground_fallback: "ledger_write_failed", which no other path writes, so a failed queue write is told apart from a plain over-budget payload. A part-ceiling key (reground_part_chars, reground_part_chars_claudecode, reground_part_chars_codex) whose value is out of range or not an integer falls through to the next level and is named in config_invalid (by its environment variable, such as OPEN_SECOND_BRAIN_REGROUND_PART_CHARS, when the rejected value came from the environment), present only then. With the flag off none of these fields appears and the receipt keeps its shape. Each part the reground-deliver hook hands out writes one hook-audit line (actor: reground-deliver, action: reground_part_delivered, target the host event PostToolUse or UserPromptSubmit) with part (the 1-based part number), total, epoch (the SessionStart that queued it, as <source>:<epoch-ms> with the source one of startup, resume, clear or compact, any other value recorded as unknown), bytes (UTF-8), utf16_chars, part_ceiling_chars and over_budget (the part alone is longer than the ceiling). A take that fails for a reason other than lock contention (an unreadable state directory, a full disk) writes action: reground_take_failed with ok: false, no part and epoch (the queue's SessionStart, null when unreadable), once per epoch on each machine: a state write that keeps failing leaves the queue undrained, and the repeats on later tool calls write no further line. A tool call inside a delegated sub-agent (the payload carries agent_id) takes no part and writes no line. A process-ceiling exit writes action: hook_ceiling_exceeded, target: reground.

  • brain_route_metrics (MCP, operation: "list"|"summary") reads route-level MCP tool latency. list returns raw mcp_route_latency records (filter by tool/status/since/until/limit); summary rolls each tool up into count, error count, and min/avg/max plus p50/p95/p99 latency, ordered slowest-first so an operator can identify which endpoint blocks an agent turn by route rather than by aggregate benchmark. A route whose records carry stages also gets a stages roll-up - per stage name, in ROUTE_STAGE order, the count, the average and the nearest-rank p95 in ms - and a route without stages has no stages key. Records are emitted only when mcp_route_metrics_enabled is on (default off); read-only.

  • brain_token_impact (MCP, operation: "record"|"outcome"|"list"|"summary") is the durable value-of-memory ledger. record posts one context pack's prompt-token delta (baseline_tokens − packed_tokens, labelled method: tokenizer|heuristic — how the CALLER produced the counts, never how accurate they are; the ledger counts nothing itself and verifies neither. Samples written under the retired exact/fallback labels are still read and classified) plus an optional modeled inference-avoidance estimate (modeled_avoided_inferences × modeled_tokens_per_inference); outcome posts a first_pass/repair/retry result. summary keeps the two ledgers STRICTLY SEPARATE: prompt_token_delta reports the net/saved/added tokens with a per-method split (so a tokenizer-produced count is never averaged in with a heuristic estimate), while modeled_inference_avoidance reports the raw modeled figure and a calibrated_savings_tokens dampened by the observed first-pass rate from posted outcomes (null until an outcome exists — uncalibrated, not zero). list returns raw samples (filter by pack_id/method/host/since/until/limit); summary accepts max_samples to bound aggregation. Writes are emitted only when token_impact_ledger_enabled is on (default off) and are payload-safe (counts + opaque pack_id only, never prompts or recalled text); list/summary read regardless of the gate. Aggregates are recomputed from the continuity store on every read, so they survive restarts.

  • brain_event_trace (MCP) and o2b brain event-trace join the Brain log to the continuity store: given a logged event (by day, --at stamp, kind, or --session-id), they resolve the continuity records attached to it via the stable correlation ids above (session_id, turn_id, and artifact sourceRefs), so an operator can answer "which context was supplied to this action?" from one surface. Each attached record reports joinedBy (session / turn / artifact); open the full record through the per-kind readers (context-receipts show, recall-telemetry, generation-reports show). Read-only; private records are dropped unless explicitly kept.

  • brain_generation_reports (MCP, actions record/list/summary) and o2b brain generation-reports record|list|summary|show post and read inbound LLM generation traces. summary rolls up call counts, the always-present local token estimate, agent-reported usage where present, and a per-path linkage map so a memory path resolves back to the generation reports that produced or consumed it.

  • o2b brain continuity export --format atof|atif renders the store as standard trajectory formats (ATOF JSONL event stream; ATIF v1.7 one document per session) for replay and eval tooling. Read-only; private records never reach an export file. Mapping decisions: docs/brainstorm/memory-observability-suite/atof-atif-mapping.md.

  • o2b brain bench memory measures memory quality over disposable fixture vaults and reports quality, latency, context cost, and failure modes as separate families (o2b.bench.v2 report schema). Beyond the retrieval categories it scores three failure modes the retrieval questions cannot see: proactive_recall drives decideRecallInject over the fixture and reports know_to_ask_failure_rate and false_fire_rate (both denominated in decisions, so a strategy that always injects scores worse than one that abstains correctly); write_fidelity grades the provenance that survives the shipped extraction, insertion and dedup path with no model call; source_isolation asserts on delivered context-pack contents under a requested owner scope and gates at zero. context_cost.avg_injected_tokens is estimateTokens over the strings the bench actually injected — the suite's one token estimator. Two things the numbers do NOT say: recall_inject_enabled is off by default, so the proactive metric describes a capability rather than delivered behaviour, and integrity.owner_scope_delivery is off by default, so an isolation fixture must set it to fail or the gate is vacuous. The committed baseline is tests/fixtures/bench/failure-modes.baseline.json, compared by tests/core/bench/failure-modes-baseline.test.ts inside the ordinary bun test gate. Since v1.82.0 the verb's suite registry maps a --suite name to its loader plus a declared capability set ({embeddings: boolean}; omitting --suite stays the byte-identical locomo default), the run identity folds the pinned search knobs and the proactive-recall admission floor into the checkpoint hash beside the fixture hash - a resumed run that would mix configurations fails the identity guard by name - and two suites join locomo: --suite longmemeval scores the LongMemEval dataset shape with the strict longmemeval: {recall_all_at_5, abstention_excluded} family (the committed sample fixture certifies CI, the full dataset is operator-supplied, and the judge rides bench_judge_cmd with question/answer/gold added to the stdin payload only for suites that provide them; the embeddings capability is apply-gated and its arming is API-only this wave, with a shared provider+model+text-hash vector cache under the runs dir), and --suite surfacing-replay replays a recorded prompt corpus through the shipped decision core and scores the graded surfacing gate's know_to_ask / false_fire rates, which closes the caveat that the structural gate alone was not scoreable: the graded admission key is a measured bound, and the replay suite is the instrument that re-measures it. Since v1.83.0 the capability set also declares sampling, and --sample-seed <value> plus --sample-cap <n> (a pair - either alone is refused by name; a non-positive or non-integer cap is refused naming the value; a suite without the capability is refused naming the suite, and longmemeval is the only one that has it) run a seeded, question-type-proportional subset of the dataset instead of the whole fixture: abstention questions are excluded first and their count still lands in abstention_excluded, the survivors are stratified by the dataset's own question_type values, each stratum contributes its population-proportional share of the cap by largest-remainder apportionment (remainders compared as integers, ties by stratum name), and the draw is a seeded shuffle over entries sorted by question id - no wall clock, no Math.random - so dataset file order cannot change the subset and the same seed and cap always select the same questions. Sampling happens before conversion, so a sampled run materializes notes and continuity records only for the questions it scores. A sampled run's --json report carries the sampling receipt in its longmemeval family: sample_population, sample_selected and the per-stratum sample_strata sit beside abstention_excluded, each present only on a sampled run and absent from a whole-dataset report, and the human longmemeval report line gains sample_selected N only on a sampled run; the seed and cap fold into the run identity with the same spread-omission the floor uses: run ids of runs without sampling are byte-stable, and a resumed run whose sampling options changed fails the identity guard by name (the guard's message names the sampling options beside the pinned knobs and the floor).

Consumers should treat unknown record kinds and unknown payload fields as fine - skip what you do not recognize (the store and read-model already behave this way).

512 files · 486 not served here

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

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.ts
  • CLAUDE.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-mirrors
  • CLAUDE.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-vec
  • plugins/codex/skills/embeddings-setup/SKILL.md: bun install --force
  • plugins/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-vec
  • skills/embeddings-setup/SKILL.md: bun install --force
  • 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:
  • 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 0
  • 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 0
  • 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 0
  • 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 0
  • 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 0
  • 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 0
  • 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 0
  • 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 0
  • 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 0
  • 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 0
  • 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 0
  • 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 0
  • 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 0
  • 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 0
  • 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 0
  • 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 0
  • 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 0
  • 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 0
  • 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 0
  • 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 0
  • 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 0
  • 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 0
  • 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 0
  • 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 0
  • 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 0
  • 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 0
  • 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
  • plugins/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 0
  • plugins/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 0
  • plugins/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 0
  • plugins/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 0
  • plugins/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 0
  • plugins/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 0
  • plugins/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 0
  • plugins/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 0
  • plugins/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 0
  • plugins/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 0
  • plugins/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 0
  • plugins/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 0
  • plugins/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 0
  • plugins/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 0
  • plugins/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 0
  • plugins/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 0
  • plugins/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 0
  • plugins/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 0
  • plugins/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 0
  • plugins/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 0
  • plugins/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 0
  • plugins/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 0
  • plugins/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 0
  • plugins/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 0
  • plugins/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 0
  • plugins/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 0
  • plugins/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
GitHub avatar of hermes-vps-agenthermes-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