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/updating.md

Updating Open Second Brain safely

Open Second Brain installs into version-rotating plugin caches (Claude Code: ~/.claude/plugins/cache/<marketplace>/<plugin>/<version>/; Codex: a local or git marketplace snapshot). An update must never break an existing install or the agent. This document is the contract that guarantees it, for both operators and any agent that edits the plugin.

Upgrading from 0.x to 1.0.0

1.0.0 is the first major release and ships exactly one breaking change: the 18 hidden MCP alias tools left over from the token-diet consolidation are removed. They were already absent from tools/list; since 1.0.0 calling one through tools/call answers an INVALID_PARAMS tombstone naming the replacement instead of executing. Everything else

  • CLI verbs, config keys, on-disk formats, the search schema ladder - is unchanged and now formally frozen under the stability policy (docs/stability.md).

Migrate by switching each removed name to its consolidated tool with the matching view argument; every other parameter keeps its old name and meaning:

Removed toolReplacement
brain_digestbrain_brief with view: "digest"
brain_daily_briefbrain_brief with view: "daily"
brain_morning_briefbrain_brief with view: "morning"
brain_weekly_synthesisbrain_brief with view: "weekly"
brain_monthly_reviewbrain_brief with view: "monthly"
brain_operator_summarybrain_brief with view: "operator"
brain_timelinebrain_analytics with view: "timeline"
brain_attention_flowsbrain_analytics with view: "attention_flows"
brain_belief_evolutionbrain_analytics with view: "belief_evolution"
brain_concept_synthesisbrain_analytics with view: "concept_synthesis"
get_active_schema_packschema_inspect with view: "active_pack"
list_schema_packsschema_inspect with view: "packs"
schema_statsschema_inspect with view: "stats"
schema_lintschema_inspect with view: "lint"
schema_graphschema_inspect with view: "graph"
schema_explain_typeschema_inspect with view: "explain_type"
schema_review_orphansschema_inspect with view: "orphans"
reload_schema_packschema_inspect with view: "active_pack"

o2b brain doctor scans your vault-side surfaces (Brain notes, root instruction files such as CLAUDE.md/AGENTS.md, installed .claude/skills/) and warns with the exact replacement for any stale reference it finds (removed-tool-reference).

Upgrading to 1.91.0

No step is required. This release is additive: a new one-off pledge page family, two new MCP tools, a new section on the today dashboard, and three opt-in machine-local config keys whose defaults leave an unchanged install byte-identical. Two existing outputs gain members, and one opt-in hook joins the UserPromptSubmit row set; all four changes are written here rather than left to be discovered at the terminal.

A new page family: one-off pledges at Brain/pledges/<slug>.md. A pledge is a promise read in prose: a title, a promise text, a due date, the source page it was read from, and evidence quotes the kernel verified against that source at write time, so a stored quote is one the source contained when the pledge was written. The frontmatter keys are title, created_at, due, source, evidence, promised_to, fulfilled, agent, plus visibility inherited from the cited source page when it carries one; the word commitment keeps meaning the epistemic tier and appears nowhere in this family. promised_to is a new relation token in the shared vocabulary, so the existing frontmatter-relations ingest projects counterparty edges with no new projection code. Search admission of the new directory is automatic. Removing a pledge archives it under Brain/pledges/archive/ with a collision suffix.

Two new MCP tools, brain_pledge and brain_intake_pledges. The first is direct CRUD over the family - add, fulfill, list, show, remove - mirroring the brain_obligation operation shape. The second is the extraction intake: the agent posts typed pledge extractions citing one source page, the kernel validates every item before writing anything (a batch with one invalid item writes no page), commits idempotently, anchors counterparty names server-side against canonical-scope entities, and reports names that anchor nowhere instead of storing them. The batch is capped by name: at most 100 pledges per intake, at most 32 counterparty names per pledge, and no counterparty name longer than 200 characters - a batch over a cap refuses whole. The kernel never calls a model. Both tools answer at the caller's reach: below local reach a withheld pledge page reads exactly as an absent one.

A pledges section joins the today dashboard, and two existing outputs gain members. o2b brain today and brain_brief view="today" now render a ## Pledges section between obligations and open loops - due-sorted open pledges with an overdue flag and days-until-due - and the rendered totals gain the lines Pledges: and Pledges overdue:. The --json envelopes gain the same data additively: the totals object carries two new keys, pledgesTotal and pledgesOverdue, and the brain_brief view="today" response gains a pledges member beside obligations. Consumers that enumerate these objects see the new keys; nothing existing changed value or moved. A page the caller may not read drops from the section before totals are derived, identical to absent.

The counterparty brief hook is opt-in and off by default. With counterparty_brief_enabled: "true" in the machine-local main config (or the env override OPEN_SECOND_BRAIN_COUNTERPARTY_BRIEF_ENABLED; the default is off), a UserPromptSubmit hook reads each submitted prompt and, when it fires, injects one additionalContext block: at most three shareable items about a counterparty the prompt names - open pledges promised to that entity first, then recent non-private preference pages whose bodies anchor it - each carrying its source page. It fires only when ALL of the following hold: the prompt contains at least one token from the operator-declared counterparty_brief_triggers list (a comma-separated machine-local main config key in the operator's own language; nothing ships a built-in word list, and an absent or malformed list keeps the brief inert); exactly one canonical-scope entity matches the prompt by name or alias (zero or several matches abstain); and it is the first qualifying prompt of the session - the injection ledger suppresses repeats, so later turns in the same session emit nothing. A page whose visibility carries the reserved private token never surfaces whatever the caller's reach, and a prompt without a real session id never writes the ledger. The hook is registered for Claude Code, mirrored for Codex, and included in the Grok hook install; flag off keeps its stdout byte-identical to an install without it. To turn the brief off, set counterparty_brief_enabled: "false" or clear the trigger list; per-hook detail lives in hooks/README.md. One limit stated plainly, the same one brain_context_pack already carries: a readable page may name a private page, and no per-page gate rewrites prose.

brain_context_pack gains an opt-in entity_facts section, off by default. With entity_facts_context_pack: "true" in the machine-local main config (or the env override OPEN_SECOND_BRAIN_ENTITY_FACTS_CONTEXT_PACK; the default is off), the pack response gains a conditional entity_facts member: the canonical-scope entities anchored from the pack query, at most three, each listing only the ids of candidates the pack already kept whose bodies anchor it, so the section is a rollup over work the pack already did and never a second recall lane. An entity page the caller's view refuses reads as absent, so a withheld entity never surfaces its rollup. With the key off - the default - packs are byte-identical to the previous release, and the other consumers of the same core are untouched.

Upgrading to 1.84.0

No step is required. Hybrid search now starts its semantic lane before the keyword lanes run and joins it where the serial await stood, so the query embed is hidden behind the keyword work; results, warnings and the query-cache keys are byte-identical, and no configuration changes. The new surfaces are additive options: search --explain reports overlapped_ms (how much of the embed round trip the keyword lanes hid) only while the flag is on, and the bench latency family reports the same figure only for runs whose questions attempted the semantic lane. One absence is expected: a warm query-cache hit serves the answer before the lanes start, so the timing sink never fires and a cached answer's --explain output carries neither overlapped_ms nor semantic_attempted; a fresh keyword-only run still reports overlapped_ms: 0 and the semantic attempt as false.

brain_context_pack accepts packing_policy, strictly opt-in. The new MCP argument lets a caller name which candidate population claims the pack's existing max_tokens budget first - the preference-derived pages (facts_first) or the retired pages (retired_first) - and a policy against a vault where one lane holds no candidate is refused by name. Omitted, it changes no bytes of any response, receipt, telemetry record or prompt-prefix metric; named, the response gains a packing accounting object and the receipt and telemetry budget metadata gain a packing_policy member of the frozen two-value vocabulary. There is no CLI flag and no config key - the knob rides the MCP tool only, following the query_mode parity precedent.

Standing-instruction facets are opt-in on the attention flow evaluation, strictly read-only. Callers that evaluate the standing_query action keep the legacy pref-<slug> (scope) output byte for byte unless they name standingInstructionFacets; named, a surfaced preference whose evidenced_by evidence joins to a signal session_ref gains a first-mention annotation, and preferences whose join does not resolve stay unannotated rather than implying an origin. No preference is admitted that the plain lane would not already deliver, and the lane performs zero writes. The new standing_instruction bench category exists to measure that join; runs without such questions produce byte-identical reports and output.

Upgrading to 1.83.0

Nothing below needs a migration step. Every new surface in this release

  • the per-source-session search pool cap, the usage-ordered skill offers, the seeded LongMemEval sampling flags - ships default off and is byte-identical when off, so an existing install behaves exactly as before until an operator opts in. The one exception is written here rather than left to be discovered at the terminal.

The first o2b install --check --target opencode after the host writes an mcp.servers object reports drift, and the first re-apply is refused. OpenCode V2 hosts read the native mcp.servers config shape, and the opencode adapter now registers there when that object exists instead of always writing the legacy top-level mcp key. An install that never sees an mcp.servers object is unaffected - the legacy shape stays byte for byte what earlier releases wrote. When the host (or the operator) adds the object to a config this integration previously owned, verification - which works by re-construction - no longer reproduces what is on disk, so the check drifts once; and the first apply after that is refused by the hand-edited-file guard rather than silently overwriting a file something else just changed. Fix: o2b install --target opencode --apply --force (or o2b update --force). The forced re-apply registers the two servers under mcp.servers and removes the shadowed legacy copies of its own two keys from the top-level block; user-authored config is never touched. Details in install/opencode.md.

Upgrading to 1.76.0

No step is required. Searches and session starts now keep the search index current on their own: an index older than a minute gets one background incremental run. If you scheduled o2b search index or o2b search reindex only to keep keyword search fresh, that job can go; keep it if it computes embeddings. To turn the background runs off, set search_freshen_interval_s: "0".

Upgrading to 1.73.0

No step is required unless you set a positive embedding_cost_gate_usd on a model with no known price, run a model the curated table does not list, or want to send extra request fields to your embedding endpoint.

Hooks and MCP searches under a positive gate fall back to keyword-only and say so. The query embed of a search that is not at local reach (every MCP tool and the recall-inject and gap-promote hooks) now passes the same price gate as the embedding runs of 1.72.0. With a positive embedding_cost_gate_usd on a model with no known price, a hybrid search answers keyword-only and records semantic-cost-unpriced in its trail, a hook names the code on its local audit line (retrieval_degraded), and an explicit semantic search fails with EMBEDDING_COST_UNPRICED. o2b search at the CLI runs at local reach and is unchanged. Declare the price of a self-hosted model to keep the semantic lane:

embedding_price_model: nomic-embed-text:latest
embedding_price_usd_per_mtok: 0

brain_tune now refuses to save a winner measured without the semantic lane, and recall feedback on such a search no longer moves the learned weights.

Declare the input window of an uncurated model. A query longer than the model's input window is now cut to it before the embed and the trail records semantic-query-truncated. For a model the curated table does not list the window is unknown and nothing is cut; set embedding_input_window_tokens to the model's window (in its own tokens) so long queries are cut and disclosed instead of truncated or refused silently by the server. The key is refused for the local provider.

Extra request fields need openai-compat. embedding_extra_body takes one JSON object, is sent only by the openai-compat provider (refused for others, inert for disabled), refuses model, input and encoding_format, and a dimensions field needs embedding_dimension set to the same width.

Upgrades refuse a file edited after the plan was read. o2b brain upgrade --apply and the automatic upgrade worker now apply the plan they computed. When a managed file changed in between, nothing is overwritten: re-run o2b brain upgrade --dry-run and apply the new plan. Under --apply --json a refusal prints { "ok": false, "error", "run_id", "drifted" }. In the --dry-run --json plan, a file that does not exist yet still reports before_size: 0.

brain_recall_feedback gains an additive degraded array, and brain_context_pack in semantic mode resolves an omitted reach to remote and counts the instruction prefix in query_tokens.

Upgrading to 1.72.0

No step is required unless you set a positive embedding_cost_gate_usd, left a numeric search setting blank, or an integration reads the JSON embedding cost estimates, which can now be null whatever the gate (see below).

A positive cost gate now refuses a model with no known price. Before this release, a model missing from the built-in price table was priced at $0, so a gate never stopped it. Now its price is unknown, and an embedding reindex, maintenance reindex or vector backfill with pending chunks on that model is refused with EMBEDDING_COST_UNPRICED while the gate is positive. This affects local servers (Ollama, LM Studio, llama.cpp) and any model the table does not list. Run o2b search check: it names the model and the keys to set. Declare the price once to restore unattended runs:

embedding_price_model: nomic-embed-text:latest
embedding_price_usd_per_mtok: 0

0 declares the model free; use the provider's real rate otherwise. --force-cost (MCP force_cost) passes a single run without a declaration. With the gate at 0, the default, nothing is refused.

Unknown prices read as unknown. Where an estimate used to print $0.0000 for an unlisted model, the maintenance banner, the backfill dry run and search status now print price unknown, and the JSON estimates (estimated_cost_usd, estimated_usd, the status refresh estimate) are null. A consumer that read 0 as free must treat null as unknown. The backfill's estimated_cost_usd used to be omitted for an unknown price and is now always present. Spend receipts, the maintenance_spend metric, the backfill JSON and the status JSON gain price_source (refresh_price_source on status); journal rows written before this release list their price source as unrecorded.

Edits re-embed less. An edited note keeps the vectors of its unchanged chunks, so the next embedding pass pays only for what changed. Nothing needs reindexing to benefit.

o2b search vector-backfill gains a repeatable --path, and brain_context_pack gains query_mode: "semantic"; both are additive.

Blank numeric settings and long feedback queries are refused. A whitespace-only embedding_cost_gate_usd or search_rerank_min_score (in config or through its env twin) used to read as 0; it now fails config resolution with INVALID_INPUT, so remove the key or give it a number. brain_recall_feedback now enforces its advertised 2000-character query cap and refuses a longer query with INVALID_PARAMS.

Upgrading to 1.71.0

No step is required. Three changes are visible to an operator who runs with recall_inject_enabled on, and one to every Claude Code and Codex install.

The recall brief no longer repeats notes within a session. recall_inject_dedupe (env OPEN_SECOND_BRAIN_RECALL_INJECT_DEDUPE) defaults to true: with a host that sends a session_id, a note span already shown in the session, by an earlier brief or by the SessionStart digest, is not injected again until the next SessionStart, and a prompt whose candidates were all shown before abstains with all_already_injected. Set recall_inject_dedupe: "false" (or "0") to restore the previous behaviour. See "Session dedupe and slices" in decision-models/recall-inject.md.

The recall caps are configurable. recall_inject_max_notes, recall_inject_max_chars, recall_inject_time_budget_ms and recall_inject_confidence_floor keep their previous values as defaults; an out-of-range value keeps the default and is named (by its env variable when it came from the env) as config_invalid on the audit line.

Recall slices are opt-in vault policy. A recall_inject: block in Brain/_brain.yaml is new and absent from the generated template; a vault without it recalls exactly as before. A malformed block makes _brain.yaml fail to load, and the recall decision then ends in error with fault retriever_failed until the file is fixed. The contract is in cli-reference.md.

A new reground-deliver hook is registered. The Claude Code and Codex hook manifests run it on every PostToolUse and UserPromptSubmit event. It does nothing unless reground_parts_enabled is on (default off), and it checks that flag before resolving the vault. Turn the flag on to have an oversized SessionStart payload split into parts instead of emitted whole; the keys and the receipt fields are in cli-reference.md and observability.md.

Hook-state files are private and some are renamed. The per-session files under .open-second-brain/hook-state/ are now written with mode 0600, because the re-grounding queue holds parts of the SessionStart payload. Existing files become private on their next write; a file never written again stays as it was until the prune removes it. A session id that is not already a lowercase slug (one with upper case, separators or more than 64 characters) now gets a file name with a hash suffix, so two such ids no longer share a file. Claude Code and Codex session ids keep their file names. A host that sends other ids starts its hook state afresh once after the upgrade: once-per-session nudges can repeat in an open session, and the old file is pruned after seven days.

The last-good SessionStart snapshot under .open-second-brain/inject-cache/ and the search session focus file are written with mode 0600 as well, and setting a focus answers INVALID_INPUT when a directory on the way to the focus file is a symbolic link. A search path_prefix that starts with a drive letter is now refused with INVALID_INPUT, like one containing .. or starting with /.

Upgrading to 1.70.0

One step is required for Hermes users: update the o2b CLI and the Hermes plugin together. Seven changes are visible to an operator or a client.

Update o2b and the Hermes plugin together. The Hermes plugin now launches its MCP bridge as o2b mcp --harness hermes, and an o2b older than 1.70.0 refuses --harness as an unknown flag, so the bridge does not start; the bridge's stderr in the gateway log names the flag. The o2b links that scripts/o2b install-cli creates follow the plugin; an o2b installed some other way must be updated as well. The Claude Code plugin's two MCP registrations gain --harness claude-code in the same release, so they need no step.

Hermes settings come from the profile on a multiplexed gateway. With gateway.multiplex_profiles: true, VAULT_DIR, VAULT_AGENT_NAME, VAULT_TIMEZONE, OPEN_SECOND_BRAIN_CONFIG and OPEN_SECOND_BRAIN_MCP_TIMEOUT are read from each turn's profile scope (the profile's .env), no longer from the gateway process environment. A profile that relied on the launch profile's environment now falls through to the Open Second Brain config chain instead; move such a setting into that profile's .env. The gateway log names each ignored variable once with a WARNING. A single-profile gateway is unchanged. See "Multiple Hermes profiles" in install/hermes.md.

hermes open-second-brain config prints the settings source first. The first line is now settings_source: ..., naming the source this command resolved from (profile scope (multiplexed gateway), or process environment with a note that a multiplexed gateway reads each profile's .env), and config_path: follows it; when no profile scope is bound, the named error replaces the path. A script that reads config_path: from the first line must look for it by name. On a multiplexed gateway each profile identity (vault, agent name, timezone, config path and timeout) gets its own o2b mcp child, so the gateway runs more child processes, and a child whose identity changed (for example after an edited .env) stays alive until the gateway exits. A call with no bound profile scope reports a ProfileScopeError naming the variable, and prefetch omits the vault reminder for that turn with one WARNING.

Scoped operator rules. Files under Brain/standing-rules/project/, Brain/standing-rules/harness/ and Brain/standing-rules/host/ are now rendered for the matching project, harness and device, after the operator standing rules, in the SessionStart hook (project and host only) and in brain_context, for a local caller only. A vault without the directory renders exactly as before. The block is charged against active.inject_budget_chars and capped by the new active.scoped_rules_max_chars (default 2,000); the hook receipt's budget block gains scoped_rules_chars, and brain_context gains an optional scoped_rules key. Every write path now refuses paths inside Brain/standing-rules/. See "Scoped operator rules" in how-it-works.md.

o2b mcp --harness <id>. A new optional flag naming the harness; an unknown value exits 2. --host-target now also stands in for it when it is absent, and a refused --host-target value is now echoed as a JSON string with its control characters escaped, as a refused --harness value is; a script that matches the raw value on stderr must match the quoted one.

More brief and doctor counts answer at the caller's reach. Below local reach brain_brief view="today" no longer lists an open loop or obligation from a page the caller cannot read, view="monthly" counts from the events the caller may see, view="operator" computes its doctor and digest counts, top actions, verification entries and trust verdict from what the caller may see, and the brain_doctor removed-tool warning cap, uncertain cap, stale-dependency counts, concept-gap and contradiction detectors and instruction-file warnings leave out what the caller cannot read, and the brain_doctor repair preview plans from the same checks. brain_obligation, brain_intention, brain_health, brain_trigger scans, brain_stale_scan, brain_review_candidates, brain_intent_review, brain_retention, brain_tension and brain_context_receipts answer the same way. A local caller and the CLI see no change.

A remote client runs only a dry dream. Below local reach (an HTTP bind on a non-loopback interface) brain_dream serves a dry run and refuses a real pass, a step and the staged lifecycle, and brain_maintenance refuses run; schedule them on the vault's own host (o2b brain dream, o2b brain maintenance run, or a stdio or loopback client). The lifecycle (including the chain tip), expire, evidence, feedback, derived-fact, decision, label, scaffold, dead-end, diarize, hygiene refresh and anticipatory-context tools treat a page the caller cannot read as absent, and a write aimed at such a page is refused before anything is written.

Derived pages keep the visibility of their sources. A preference the dream pass drafts from signals reserved with visibility, or that supersedes or rebuts a reserved record, now carries the strictest visibility of those sources, and a tension page carries the stricter visibility of its two source notes, refreshed when it is detected again. Such pages were written without a visibility line before, so they may now be withheld from a remote caller.

Upgrading to 1.69.0

No step below is required. Ten changes are visible to an operator or a client.

CSV, TSV and HTML files are planned by default. o2b brain batch-plan and brain_ingest_batch_plan now discover .csv, .tsv, .html and .htm files beside the Markdown and plain-text ones, and a planned file of one of these formats carries its format. The plan id of a tree holding such files therefore changes, and a resume checkpoint written for that tree before the upgrade no longer matches it; a tree of Markdown and plain-text files plans exactly as before. An extensions override is honoured as before.

PDF, Office, EPUB, RTF and image files are named, not counted. A .png, .pdf, .docx or another file of a format the registry names but does not extract moves from the unclassifiable counts to skipped_non_extractable, one entry per file with reason format-not-extractable and the format as detail, so skip_reason_counts gains the format-not-extractable key and the plan id of a tree holding such files changes too. A reader that summed unclassifiable sees fewer files there and finds them in skipped_non_extractable.

Ingesting an HTML, CSV or TSV source adds a derived section. brain_ingest_source writes a ## Parts section (HTML headings and their line spans) or a ## Table section (the rows as fenced plain text) onto the summary page of an in-vault source of these formats, with source_format, source_content_hash and, for a table, five table_* frontmatter keys, and returns parts or table. The first ingest after the upgrade of a CSV, TSV or HTML source already ingested rewrites its page once with the new section and keys. Text and Markdown sources are unchanged. A leading frontmatter block in such a source is not data: it is never rendered as a table row or as text. Table cells and headings pass the redactor; see mcp.md for what it catches and what it does not.

A summary page carries its source's visibility, and a re-ingest keeps the operator's. A summary page with a derived section takes on its source's visibility, so a source an operator reserved yields a summary page reserved the same way. When both the page and the source declare tokens, the page gets the audience both the operator and the source allow; when they share none, the page is withheld below local reach. An operator-set visibility on any summary page used to be dropped when the source was ingested again; it is now kept, the way created_at is, for every source format. A byte-order mark before a frontmatter block no longer hides that block, on any page.

brain_recall_gate accepts turn_id and bounds its correlation arguments. The optional argument (at most 512 characters) the Hermes plugin sends was refused by the closed input schema; it is now accepted and recorded on the gate_telemetry record. A turn_id, session_id or telemetry_host longer than its bound (512, 512 and 200 characters) is now refused with INVALID_PARAMS before the gate runs, with telemetry on or off; it used to be dropped from the telemetry record without a word, and not checked at all with telemetry off.

Multi-line prompts that start with a command name are now admitted. The recall surfacing gate skipped a prompt whose first word is a command name such as git or ls as a shell command, even when the prompt went on over more lines. Such a multi-line prompt now retrieves. A single-line prompt that starts with a command name is still skipped as shell_command.

The daily and weekly briefs and the recall verdict answer at the caller's reach. Below local reach the daily and weekly brain_brief views leave out rows and source pointers that only a record the caller cannot read accounts for, recompute events_by_kind and vault_delta from the events the caller may see, take no report snapshot and show no delta. The empty-search coverage verdict of brain_search and brain_recall_gate counts a root as reached only through a page the caller can read, so it no longer flips on a root that holds only pages the caller cannot read.

More readers treat a page or record the caller cannot read at its reach as absent. Below local reach the daily log pages are no longer served by the generic page readers (brain_search and every tool that reads a page by path). brain_analytics view="timeline", view="belief_evolution" and view="concept_synthesis", the today, digest and brain_event_trace views, brain_claims, brain_backlinks, the osb://backlinks, osb://log, osb://topic and osb://preference resources, brain_query with since, topic or preference, brain_moc_audit and every brain_doctor finding answer at the caller's reach. brain_search_by_source and brain_delete_by_source honour the reach of a derived summary page, and a brain_ingest_source below local reach leaves a summary page the caller cannot read untouched. An entity intake of a source reserved against remote reads records no source_content_hash. The brain_doctor findings orphan-evidence and malformed-evidence-range gain sources, and removed-tool-reference gains path (vault-relative on the wire); the messages are unchanged. A local caller and the CLI see no change. See mcp.md for the full list.

Private regions open more readily. An open <private> tag whose attributes hold a <, or that never closes, now opens a region, in the redactor, the session-recall externalisation and the continuity redaction alike; the text after it is hidden or flagged private where it used to pass. Private regions and table cells are now redacted in linear time.

Architecture notes name the edges a link cannot carry. o2b brain architect no longer says that no module depends on another when every declared edge touches a module whose name a link cannot carry: it lists those edges as code spans, so an overview or module note in that state reports updated once.

Upgrading to 1.68.0

No step below is required. Six changes are visible to an operator or a client.

Architecture notes gain dependency regions and one owned frontmatter key. o2b brain architect now reads dependency manifests at the root and at each module. On an existing project the first run appends a module-dependencies region to the overview and a dependencies region to each module note, so it reports those notes updated once. A module note whose manifest depends on another module gets a depends_on frontmatter key that the generator owns and rewrites on every run; a value typed under that key is replaced. --json gains a manifests list. A malformed root package.json, which used to be read as no manifest at all, is now reported malformed, and the detail of every malformed manifest is a fixed reason such as invalid JSON, not the parser's message. A dependency name that cannot be written into a note is counted as unrepresentable instead of listed, and a module whose directory name a link cannot carry is named once on the overview and left out of every link and edge. See how-it-works.md.

The pre-extractor reads Terraform. o2b brain pre-extract and brain_ingest_source with pre_extract now return seeds for .tf and .tfvars files, which used to report extracted: false.

Credentials in import specifiers are redacted. In every family's imports seeds, the userinfo of an http(s) or git:: specifier (a user:password pair such as https://user:pass@host/m.js, or a bare token such as git::https://<token>@github.com/o/r.git) and the value of a named credential query parameter (sshkey, token, access_token, password, signature, the S3 and GCS access-key and signing parameters) are replaced, the query parameter also on a specifier that is not a URL, such as git@host:org/repo.git?sshkey=.... A conventional login such as ssh://git@ is kept. A credential in a path segment, a fragment or an unnamed query parameter is not recognised. Specifiers without credentials are unchanged; a specifier longer than 2048 characters is replaced whole. With pre_extract, brain_ingest_source skips a source larger than 1 MiB and says so.

A remote search with no match no longer states index counts. The answer of an empty brain_search and of brain_recall_gate below local reach carries no coverage receipt, since its counts include pages the caller cannot read: a not_found names the index time only, and an unknown gives a fixed reason for its unknown_reason. A local caller and the CLI keep the receipt.

More tools treat a record the caller cannot read at its reach as absent. A remote client now gets, for such a record, exactly the answer an absent one gets from brain_context, the osb://preferences/active resource, brain_pre_compress_pack, brain_brief view="digest" and view="morning", the osb://digest/latest and osb://lessons resources, brain_health and the brain_doctor repair plan and apply. The first three now always hand a remote client a render stamped with the file's generated_at instead of the file's bytes. A remote view="digest" takes no report snapshot and shows no delta, and a remote view="morning" shows no pending-triggers section and marks no trigger delivered. The osb://preferences/active resource also follows the owner gate under integrity.owner_scope_delivery: fail, as brain_context does. A local caller and the CLI see no change.

Remote search no longer returns the compiled digest pages. Brain/active.md and Brain/lessons.md compile preferences and dead-ends that may reserve themselves against remote reads, so brain_search and the other generic page readers withhold both pages from a remote client. The active digest stays available remotely through brain_context, the osb://preferences/active resource and brain_pre_compress_pack, each rendered without the records that client cannot read. Local search is unchanged. See the 1.68.0 entry in CHANGELOG.md.

Upgrading to 1.67.0

No step below is required. Six changes are visible to an operator or a client.

A distilled quote that does not verify loses its quotation marks. brain_distill_source and o2b brain distill now check every quoted span in a claim against the block it cites, or the whole source. A span the source does not hold is written without its two quotation marks and named in the result's quotes report; the words stay and the write lands. Pass strict_quotes: true (or --strict-quotes) to refuse such a write instead, with error.data.code quote_unverified over MCP and exit 1 on the CLI. See "Source distillation" in mcp.md.

New frontmatter keys appear only on pages that are not full-local. capture_scope, capture_scopes and excerpt_hash are written for a source that is bounded-local or url-only, and quotes_verified / quotes_unquoted only when a claim holds a quoted span. A distillation, ingest summary or research report over local sources is written byte-identical to before.

A claim or a research title that spans more than one line is refused. A distillation claim holding a line break, or more than 1000 claims in one call, is refused, and so is a brain_research_report title holding a line break. Both used to be written as given.

Links inside a fenced block are masked up to its own closing fence. Wikilink extraction and search indexing close a fenced block only on a run of its own character at least as long. Links inside a tilde fence are no longer extracted, and a longer fence holding a shorter backtick run no longer leaks the rest of its content as links. An inline code span closes only on a backtick run of its own length, and a backtick run with no closer is plain text that hides no link after it. Such a vault may show different backlinks and link hits after its next index pass.

The capture-scope hygiene detector is on by default. A default brain_hygiene or o2b brain hygiene scan may report new warnings on an existing vault where a distillation, ingest summary, research report or canonical entity cites only URLs. The findings propose review and never change a page. Callers that pin the default detector list see the new id at the end. See cli-reference.md.

More tools treat a page the caller cannot read at its reach as absent. A remote client now gets, for such a page, exactly the answer an absent page gets: brain_delete_by_source, brain_note_lifecycle, brain_append_note, brain_update_note, brain_write_batch, brain_note_history, brain_tiers, brain_event_trace, brain_agent_query, brain_agent_diff, brain_diarize, brain_design_note, brain_dream, brain_idea_discovery, brain_procedural_memory, brain_doctor, schema_inspect and brain_writes plan_revert leave it out of their lists and counts or refuse it as missing, and the brain_hygiene link_integrity block reports measured: false with reason reach. A local caller and the CLI see no change. See the 1.67.0 entry in CHANGELOG.md.

Upgrading to 1.66.0

No step below is required. Three changes are visible to a client or an operator.

Every JSON-RPC error answer now carries error.data. It is always an object with a string code (for example unknown_argument, vault_frozen or the default invalid_params), and an isError tool result carries its code on _meta["open-second-brain/error"].code. The error messages and text blocks are unchanged, so a client that matched prose keeps working; a client that asserted "data" not in error sees the new member. The Hermes bridge exposes the code as BridgeError.code. See "Tool errors" in mcp.md.

An install with an enabled openai-compat rerank sees one query-cache miss per query. The cache key now carries the rerank sunset survey's review date, and an answer whose rerank endpoint failed is no longer cached, so the next identical query asks the endpoint again. Every other install keeps its cache keys.

o2b brain doctor checks the rerank configuration. With search_rerank_enabled on and the openai-compat kind, a missing or blank base URL, model or key now fails the doctor as rerank-endpoint-unconfigured, and a model with an announced decommission date warns. A model outside the shipped survey is reported as uncertain (rerank-model-sunset-unsurveyed), which says only that no statement was made about it. The check reads configuration only and sends no request. See the rerank doctor table in cli-reference.md.

Upgrading to 1.65.0

No step below is required, and an existing install sees no change in what the maintenance lane runs.

Custom maintenance lane tasks are a new opt-in. An install can declare its own upkeep commands in the machine config file (maintenance_custom_<name>: <command>, with optional _cwd and _timeout_seconds keys); they run only once maintenance_custom_tasks: true (env OPEN_SECOND_BRAIN_MAINTENANCE_CUSTOM_TASKS) is set, after the four built-in tasks and under the same gates. Without the switch nothing runs, and o2b brain maintenance status says so when keys are declared. A task runs from the running user's home directory unless it declares _cwd (the vault only when named), its environment drops every variable whose name declares a credential (*_API_KEY, *_TOKEN, *_SECRET and the like) while keeping PATH, HOME, LANG, LC_*, TZ, TMPDIR and O2B_VAULT, and the declared timeouts (default 120 s each) together stay within 1200 s of the 30-minute lease. See the maintenance lane in cli-reference.md.

The lane can print its own schedule. o2b brain maintenance run --cron-template prints a cron recipe, or a systemd user timer with --format systemd; it installs nothing, so an existing hand-written cron line keeps working as it is.

o2b discipline install writes the Hermes jobs file of the running user. The default moved from a fixed path under the root user's home to ~/.hermes/cron/jobs.json. On a host where Hermes runs as root the path is the same; on any other host the install used to fail and now works. OSB_HERMES_JOBS still overrides it.

A served o2b mcp exits 70 on an uncaught exception and survives an unhandled promise rejection, naming it on stderr. A supervisor that treated every exit of the server the same way can now tell a fault (70) from a signal (130 / 143). See "Background faults" in mcp.md.

Upgrading to 1.64.0

No step below is required. Two changes are visible to a nightly cron, and one is an on-disk migration that applies itself.

The maintenance lane's reindex can spend embedding budget, but only when you opt in. By default the quiet-window reindex stays keyword-only and contacts no provider, exactly as before. Set maintenance_embeddings: true (env OPEN_SECOND_BRAIN_MAINTENANCE_EMBEDDINGS) and the reindex requests the embedding phase whenever the resolved semantic config can reach a provider. The spend is announced before the pass (the pending-spend estimate), receipted after it (the maintenance_spend metrics surface and the task row), and a positive embedding_cost_gate_usd refuses the phase unless --force-cost (MCP force_cost) is set for that run. The gate's default is 0, which means OFF - a vault that wants the leash must set a positive value. Leaving maintenance_embeddings unset (or false) keeps the old behaviour outright, whatever provider the config can reach.

A maintenance task killed at its safeguard deadline exits 6, not 1. A cron script that treated every non-zero exit as a broken pass now sees 6 for a pass that merely did not finish; 1 still means a task failed and 7 still means a streak refusal, with a proved failure outranking a timeout and a timeout outranking a refusal in the same run.

The search index migrates to schema v13 on the next open. Two nullable columns (event_time_min / event_time_max) and a pinned flag join the documents table; the migration is additive and no reindex is required. Existing rows keep NULLs until their next content change refreshes them, and a o2b search reindex fills every row eagerly. A vault that reindexes nothing behaves byte-identically.

Upgrading to 1.58.0

No step below is required for a vault that uses none of the named features. Each one refuses something an earlier release accepted, so a script, a config or an agent routine that relied on it stops with a named error rather than a silent change.

Plain-http embedding and rerank endpoints are refused. The embedding and rerank base URLs must be https, except for a loopback host. A local server on another machine (LM Studio on the Windows host seen from WSL, Ollama on the LAN, a tailnet address) needs the per-endpoint opt-out in the operator config or environment:

embedding_allow_insecure_http: true # OPEN_SECOND_BRAIN_EMBEDDING_ALLOW_INSECURE_HTTP
search_rerank_allow_insecure_http: true # OPEN_SECOND_BRAIN_SEARCH_RERANK_ALLOW_INSECURE_HTTP

Both default to false, apply only to a base URL from the config or the environment (never to one from the in-vault provider registry), and warn once per endpoint. The refusal names the switch.

An unknown tool profile stops the server. o2b mcp with a --tool-profile or mcp_tool_profile naming a profile that does not exist exits 2, naming the value, where it came from and the known profiles. It used to serve the full tool surface. Fix: correct the profile name.

bank-import resets every preference row by default. Untrusted is the default: each row lands unconfirmed, unpinned, at low confidence, on a fresh trial window dated from the restore, including a row the bundle already marked unconfirmed, and the run names every row it reset. Fix: for a backup you vouch for, pass --trusted-restore.

OKF import strips machinery frontmatter unless --trusted. The bundle's producer field no longer decides it. Without --trusted, _status, owner, origin_channel and the other machinery keys are stripped, and only --trusted admits the Brain/sources/ and Brain/reports/ lanes. A round trip of your own export needs --trusted to stay lossless.

Labels and attributes refuse Brain/ machinery and non-Markdown files. o2b brain label, o2b brain attr, brain_labels and marker write-back accept a Markdown note, and under Brain/ only pages in sources/, reports/ and distillations/. A marker whose target is refused is reported as refused and left unconsumed.

Write sessions commit only into agent page lanes. brain_write_session and o2b brain session open --target accept a target under Brain/sources/, Brain/reports/, Brain/distillations/, Brain/notes/ or Brain/decisions/panels/. Any other path under Brain/ is refused with target-reserved, and the commit refuses a target that a symbolic link carries outside those lanes. Fix: point the session at one of those lanes.

A folder named brain in any letter case is treated as Brain/. Note writes and graph import compare the first path segment case-folded, the way macOS and Windows resolve it, and refuse it as they refuse the machinery root. On a case-sensitive filesystem a separate top-level user folder spelled brain or BRAIN is refused for writes. Fix: rename that folder.

Remote callers no longer see private session rows. Over a remote transport, brain_session_grep, brain_session_describe, brain_session_expand and brain_idea_lineage answer a continuity row flagged private as if it did not exist, and the three session recall tools do the same for a session summary built from one. Local callers are unchanged. Fix: read those rows from a local session.

Every search index is rebuilt once. The chunker now counts Chinese, Japanese, Korean, Thai and similar text per character, and the index records the chunker_version it was cut under. The first start after the update rebuilds each existing index in the background, as described under Vault state migrates itself. Short notes in any language chunk as before. A note long enough to span several chunks may split at slightly different points in every language, because the overlap between chunks now counts toward the chunk cap. With embeddings on, the chunks that changed are re-embedded, so expect one round of embedding calls for multi-chunk notes and for text in the scripts above. Nothing to run by hand.

brain_secrets run allowlist patterns match the argv element by element. Each space-separated token of an --allow pattern matches one argument, a * inside a token globs within that argument, and a trailing * token stands for one or more further arguments. A pattern that relied on matching across the joined command line, such as tool* for tool sub --flag, now matches only a single-element argv. Fix: write it as tool *.

brain_update_note refuses visibility and origin_channel. A frontmatter update carrying either key fails with reserved_frontmatter_key. Edit them in the file.

Upgrading to 1.50.0

Nothing below needs a migration step. Two of the four change what an existing script or supervisor sees, so they are written here rather than left to be discovered at the terminal.

The first o2b install --check after upgrading reports drift on a target installed by an earlier release. A generated registration now carries --host-target <runtime> on both server entries, and Cursor's also carries --tool-profile catalog. Verification works by re-construction, so an entry written by 1.49.0 no longer equals what this build would write. Seven targets gain the flags — codex, copilot-cli, cursor, gemini-cli, grok, kiro, opencode — and the ones whose verify compares the entry byte for byte will say so. (aider, generic and pi write no MCP command line and are unaffected; a Codex installed through codex mcp add is also unaffected, because there verify checks only that the two tables are still declared, the CLI's own serialisation having no published grammar to compare against.) Fix: o2b install --target <name> --apply, or o2b update, which now hashes the payload AS WRITTEN and so stops reporting "up-to-date, payload unchanged" for a target whose host dimensions moved. The Cursor case is not cosmetic: without the profile that host silently drops every tool past its 40-tool ceiling.

codex joins copilot-cli as a target that can exit 5. o2b install --check --target codex now asks codex mcp list what the host has registered instead of only reading $CODEX_HOME/config.toml, so a Codex that has not loaded a correct configuration is reported as mcp-unreachable and exits 5 rather than 0. A script that treated a zero exit as "everything is fine" for Codex was reading a file comparison as a liveness answer. Fix: restart Codex when the configuration is right (the check says so); --apply when it is not.

A running o2b mcp server no longer dies instantly on SIGTERM. Both transports now stop accepting, wait for in-flight requests to a 10 000 ms deadline, close, and exit 130/143. A supervisor with a shorter kill timeout than that will escalate to SIGKILL where it previously saw a clean exit; the GET /health body gains "status": "draining" and in_flight so the wait is observable. Fix: raise the supervisor's stop timeout, or lower the deadline with O2B_MCP_DRAIN_MS (a non-negative number of milliseconds; a value that is not one is refused rather than silently defaulted).

o2b brain export --format accepts a third value. json and llms-txt are unchanged byte for byte. transcripts-jsonl is additive and requires --transcripts <file|dir>; a --format typo is still exit 2, now listing three names instead of two.

Upgrading to 1.49.0

1.49.0 is a MINOR release that nevertheless changes behaviour for input and vaults that 1.48.0 accepted. docs/stability.md lists "tightening validation so previously accepted input is rejected" as a breaking change, so each one is written here rather than left to be discovered at the terminal. Nothing below needs a migration step; they all need a reader.

A recall-gate call that passed scores alone is now refused. brain_recall_gate and brain_context_pack decide the adequacy level from match_quality (a search outcome's idf_weighted_coverage) and no longer derive one from the scores, because the keyword lane is min-max normalised inside its own candidate set and its top row is pinned at the configured weight however well it matched. The two arguments now stand or fall together: scores without match_quality, or match_quality without scores, answers INVALID_PARAMS naming both. The tool schema states the pairing as dependentRequired, and the verdict now echoes match_quality back so the quantity that decided the level travels with it. Fix: send both, taking match_quality from the search outcome's idf_weighted_coverage. Omit both to get the structural gate alone, which is unchanged.

An unreadable preference file now fails the verb instead of being skipped. collectExportRows used to continue past a pref-*.md it could not parse, so an export silently omitted rules - and an export that omits a rule reads exactly like a vault that never had it. It now collects every failure in the directory (one run names them all) and refuses. Three verbs that exited 0 on such a vault now exit 1 with N preference file(s) could not be parsed …:

  • o2b brain export (the two preference formats, json and llms-txt, with or without --out; the transcripts-jsonl format added in 1.50.0 reads no vault and so reads no preference file either)

  • o2b brain bank-export

  • o2b brain explorer --export <path> - and nothing is written

o2b brain doctor reports the same files. Fix: repair or remove the named files, then re-run. A vault with no malformed preference is unaffected.

o2b brain bank-import is deliberately NOT in that list. It calls the same collector against the DESTINATION vault, to learn which topics its existing rules already claim. A pre-existing malformed rule there has nothing to do with the bundle being carried, so the import proceeds on the rules it could read and prints topic-key check incomplete: <path> … for each one it could not - the topic-key collision list beside it is a partial answer, not an empty one. The rows still restore, and the exit code is still governed by the bundle's own failed rows.

o2b brain explorer (live mode) refuses to start on an unparseable Brain file. The live server re-reads the vault on every request, so the refusal is applied once at boot: a browser showing a silently smaller graph is the same lie as a written file. Previously the server started and served the incomplete graph. Fix: the refusal names every file; repair or remove them.

o2b brain explorer --export output content changes. The exported HTML now passes the shared egress redactor before the template substitution, so a preference whose text contains a credential-shaped value exports with a placeholder in place of it, and stderr carries a one-line notice when anything was removed. Byte-for-byte comparisons of exports across this boundary will differ on vaults that contain such values; a vault that contains none produces the same document.

o2b brain feedback --json mirror fields. When a shared namespace is configured, the payload carried a single mirror string. It now carries mirror plus an optional mirror_reason, and the vocabulary changed:

1.48.01.49.0
mirror: "ok"unchanged
mirror: "failed" for an I/O failureunchanged, plus mirror_reason
mirror: "failed" for a self-mirrormirror: "misconfigured" plus mirror_reason
mirror: "off"the key is absent entirely

A self-mirror - shared_namespace resolving to the origin vault - is decided before any I/O and is repaired by editing one config key, while failed is a write that was attempted and raised; reporting them identically sent the operator after the wrong fault. off was never producible: no caller ever emitted it, and the absence of a mirror is expressed by the key not being there. Fix: a consumer switching on mirror must handle misconfigured and must not expect off.

Four thresholds now measure something different for an unchanged config. No threshold VALUE changed; what each compares against did. The recall-inject floor (RECALL_INJECT_CONFIDENCE_FLOOR), the recall-adequacy verdict (recall_adequacy_sufficient / recall_adequacy_weak), the gap-loop auto-close floor (GAP_LOOP_AUTO_CLOSE_FLOOR) and the cross-vault chain stop (search_chain_stop_score) all compared their constant against a result SCORE. The keyword lane is min-max normalised within its candidate set, so the top row of any non-empty recall scored exactly search_keyword_weight - shipped at 0.6, identical to recall_adequacy_sufficient. Every keyword recall graded sufficient / proceed, weak and insufficient were unreachable, a gap task auto-closed on a hit of any quality, and the chain stop meant opposite things under the two fusion modes (unreachable in linear, always met in rrf). All four now read IDF-weighted coverage, which is absolute and pool-independent. Expect different outcomes from the same numbers: recall-inject abstains more often on weak matches, adequacy verdicts span all three levels, gap tasks stay open on a poor hit, and a configured chain stop starts firing (or stops firing) where it did not. If you tuned any of these against the old behaviour, re-tune them against idf_weighted_coverage, which o2b search --evidence-pack reports.

For operators

Normal updates need no manual steps:

# Claude Code
claude plugin update open-second-brain@open-second-brain   # restart to apply

# Codex
codex plugin marketplace upgrade        # git marketplaces
codex plugin add open-second-brain@open-second-brain   # re-stage local marketplace

# Hermes (server: the plugin dir is a symlink to the checkout)
hermes plugins update open-second-brain

A Codex marketplace add or upgrade replaces the clone wholesale, so the clone arrives without node_modules. No step follows: the first o2b call notices the missing dependency tree and runs bun install --frozen-lockfile inside the clone once, announced by a one-line notice on stderr. If that install cannot run - no network, or a clone the launcher cannot write - the launcher exits nonzero and names the manual command; running bun install --frozen-lockfile inside the clone yourself is the fallback. See install/codex.md.

You do not need to delete or re-create any ~/.local/bin symlink by hand. The hooks resolve the current version on their own, and the global o2b/o2b-hook/vault-log symlinks self-heal on the next session start. If you ever want to force a re-point explicitly:

o2b install-cli      # idempotent: re-points dangling or stale OSB symlinks

o2b install-cli only touches its own (Open Second Brain) symlinks or dangling links; it never overwrites a real file or a symlink owned by another tool.

Vault state migrates itself

You also do not need to run o2b search reindex or o2b brain upgrade after an update. On the next start of any runtime (the o2b mcp server boot, which Hermes/Claude Code/Codex all spawn, and the Claude Code SessionStart hook), ensureVaultCurrent brings an already-initialised vault current, hands-off:

  • a stale _brain.yaml / _BRAIN.md is migrated (snapshot-backed, additive; user content untouched) by a detached worker, so the server answers its first request and the hook returns without waiting for the planning, the pre-apply snapshot or the rewrite. One worker runs per vault per machine: the caller claims a lock file before the spawn and the worker releases it. A failed attempt is recorded (o2b doctor check self_heal_upgrade, o2b brain status, o2b brain upgrade --dry-run) and the next automatic attempt waits a cooldown (one hour, doubling per consecutive failure, at most 24 hours) instead of running again on every start;

  • a stale-schema or missing search index is rebuilt in the background (a detached reindex), so startup never blocks; and as a safety net the search read path self-heals a stale/missing index on first query;

  • an index whose chunks were cut by older chunking rules (its index_state chunker_version is missing or older than the running chunker's) is rebuilt the same way, once. Unchanged notes are otherwise never re-chunked, so without it a chunking fix would never reach them. The first start after the update that introduced the stamp (the #186 fix for Chinese, Japanese, Korean, Thai and similar text) rebuilds every existing index once; with embeddings on, that re-embeds whichever chunks changed.

A schema bump makes every session that starts afterwards find the index stale at once, so the spawn is checked against the index writer lock first: a rebuild that can already be seen to lose is not started at all. That check reads an instant and cannot exclude anything - the writer lock still does that - so a child can still lose a real race, and it now says so. Every detached rebuild records what it decided and how it ended on the self_heal_reindex metrics surface (docs/metrics.md), because the child runs with its streams pointed at nothing and a failure would otherwise be reported nowhere.

It is state-driven, not version-stamped. Each step keys off actual on-disk state - the search index schema_version and chunker_version, the _brain.yaml pending-changes plan, directory existence - rather than a "last version" marker (both index cells live in the per-device index, not the vault, and describe what that index holds). This is deliberate: a vault is often synced across devices (Syncthing), so a stamp written into the vault would let one device mark the work done and make another skip its own per-device step (the search index is per-device). State checks are cheap reads on every start; only a real migration does work, and the approach also handles interrupted migrations and downgrades. Any per-device version note lives outside the vault and is for logging only, never for gating. The one per-device record that does gate, the failed-upgrade marker under .open-second-brain/, only ever delays a retry after a failure; it never marks an upgrade done.

The manual o2b search reindex / o2b brain upgrade commands still exist for explicit use; auto-migration just reuses their logic.

Why updates used to break (and no longer do)

hooks/hooks.json previously invoked the bare o2b-hook launcher through a ~/.local/bin symlink that o2b install-cli pinned to a versioned cache directory. When Claude Code rotated that directory on update, the symlink dangled, the launcher resolved an old checkout, and it exit 2-ed — the one hook exit code that blocks the agent. Every prompt was rejected until the user deleted the symlink by hand. That manual step is exactly what this design removes.

Invariants — REQUIRED for any change to hooks, the launcher, or install-cli

If you are an agent (or human) editing hooks/, scripts/o2b-hook, scripts/o2b, or src/cli/install-cli.ts, you MUST preserve all of these. A later update must always be able to repair an install made by an earlier version — never assume a clean prior state.

  1. Resolve version-currently, never version-pinned. Resolve the launcher via $CLAUDE_PLUGIN_ROOT first (Claude Code sets it to the active version and re-reads hooks.json from that version every session — so a correct command shape here repairs an already-broken install on the next update), then the script's own realpath, then $OSB_PLUGIN_ROOT. Never hard-code a version, a cache path, or a stored absolute path that a future update will orphan.

  2. Hooks fail soft — never block. scripts/o2b-hook and every hooks.json command must exit 0 on any internal error (unresolved hook, missing Bun, missing launcher). Never exit 2; never let a hard command not found be the only outcome. A broken hook must degrade to a no-op, not a bricked agent.

  3. Self-heal conservatively. Symlink repair (healCliSymlinks, and install-cli's reclaim path) may re-point only dangling links or stale Open Second Brain scripts/<name> links; it must never touch a real file, a foreign tool's symlink, or a stable-directory install (e.g. a server checkout under /srv/... shared by Hermes/Codex).

  4. Codex has no plugin-root env var. Keep the PATH o2b-hook fallback so Codex (and any runtime without CLAUDE_PLUGIN_ROOT) still works.

  5. Post-upgrade migration is state-driven and best-effort. ensureVaultCurrent must key off actual on-disk state (index schema_version, _brain.yaml pending plan, dir existence), never a stamp written into the (possibly synced) vault. It must never throw and never block startup - slow work (reindex) runs detached in the background. Any new manual-after-upgrade step a future change introduces must be folded into ensureVaultCurrent so the user never runs it.

These properties are locked by tests; do not weaken them:

  • tests/hooks/o2b-hook.test.ts — fail-soft + $CLAUDE_PLUGIN_ROOT-first resolution.

  • tests/hooks/hooks-json-shape.test.ts — every command is version-current, has a PATH fallback, and ends with exit 0.

  • tests/cli/install-cli.test.ts — idempotent reclaim and conservative self-heal.

  • tests/scripts/launcher-bootstrap.test.ts — the launchers re-install the dependency tree a marketplace replacement removed, degrade to a named manual command when the install cannot run, and add nothing when the tree is present.

  • tests/core/search/self-heal.test.ts — search rebuilds a stale/missing index on read.

  • tests/core/maintenance/ensure-current.test.ts — state-driven migration, idempotent, never throws.

  • tests/core/maintenance/self-heal-reindex.test.ts — the herd is thinned against the writer lock, and a detached rebuild's outcome is readable.

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