SQLite-backed MCP server for shared state across Claude.ai, Claude Code, Codex, and related local ops tools.
bridge-db replaces ad hoc edits to a shared markdown file with a structured SQLite store and a focused MCP tool surface: cross-system state, FTS5 lexical recall, shipped-event sync receipts, shipped-event dispositions, and observability over the audit and recall logs. The markdown bridge file is regenerated from the DB via export_bridge_markdown and remains available as a fallback for file-based clients.
- Python 3.12+
- uv — fast Python package manager
git clone https://github.com/saagpatel/bridge-db.git
cd bridge-db
uv sync # install all deps into .venv
uv run pytest # verify the install- Steady maintenance. Scope is cross-system state coordination, lexical
recall, and observability — not a general knowledge store. - Schema v10: context sections carry monotonic
versiontokens; stale writes and raced handoff claims produce durablewrite_conflictsreceipts. - Schema v11: a migration backfills activity
tagsintocontent_indexso lifecycle tags (SHIPPED, DECISION, ...) are recall-able on existing DBs. - FTS5
content_indexmirrors all content tables;healthandstatusverify source-row / FTS-row alignment. - 26 MCP tools across 10 modules (activity, handoffs, context, snapshots, cost, export, health, recall, audit, conflicts).
Claude.ai ──────────────────────────────────────────────┐
(direct MCP via Claude Desktop) │
(fallback: markdown file via Filesystem MCP) │
▼
CC skills ──► MCP stdio ──► bridge-db process ──► SQLite (WAL)
Codex ──► MCP stdio ──► bridge-db process ──► ~/.local/share/bridge-db/bridge.db
│
export_bridge_markdown
│
▼
~/.claude/projects/<encoded-home>/
memory/claude_ai_context.md
No shared daemon. Each MCP client spawns its own bridge-db process via stdio. WAL mode + PRAGMA busy_timeout=15000 handles concurrent writer waiting; logical stale-write protection comes from CAS on mutable context sections.
Verify the current tool count from source with
rg '@mcp\.tool' src/bridge_db -c. As of the 2026-06-20 source check, the
surface is 26 tools across these 10 modules:
| Module | Tools |
|---|---|
| activity | log_activity, get_recent_activity, get_activity_signal, get_shipped_events, confirm_shipped_sync, record_shipped_event_disposition, mark_shipped_processed |
| handoffs | create_handoff, get_pending_handoffs, pick_up_handoff, clear_handoff |
| context | update_section, get_section, get_all_sections, sync_from_file |
| snapshots | save_snapshot, get_latest_snapshot |
| cost | record_cost, get_cost_history |
| export | export_bridge_markdown |
| health | health, status |
| recall | recall, recall_stats |
| audit | audit_tail |
| conflicts | get_write_conflicts |
Write tools enforce caller ownership, so systems can only write the slices of state they own. Recent hardening also added notion_os and personal_ops as first-class activity and cost writers.
Instruction-bearing rows carry a source_trust label — operator, agent, or ingested — recording who authored the content. It lives only in the DB (schema v7+, on pending_handoffs, activity_log, context_sections, system_snapshots) and is never serialized into the markdown export, which would otherwise launder provenance.
- Writers set it via an optional
source_trustparam; the conservative default isagent(a Claude-dispatched write is agent-authored unless the operator asserts otherwise).update_sectionpreserves an existing section's label on a content-only re-sync. - The gate lives at the one dangerous transition —
pick_up_handoffmoving a handoffpending → active:operator-trust → picks up in one call (ccandcodex).cc+ non-operator→ returnsrequires_confirmationand does not transition; re-invoke withconfirm=Trueto proceed.codex+ non-operator→ refused (Codex runs withdanger-full-access;confirmcannot bypass it). Promote the handoff tooperatortrust first.
- Visibility:
get_pending_handoffs,get_section,get_all_sections,get_recent_activity,get_activity_signal,get_shipped_events,get_latest_snapshot, andrecallhits carrysource_trustplusinstruction_boundarymetadata that tells consumers returned content is stored data, not instructions. Lifecycle aggregates use a trust summary andsource_trust="mixed"when rows differ.statusreportspending_handoffs_by_trustandhealtha full per-tablesource_trust_breakdown. Each gate decision (allowed/confirmation_required/refused) is written to the audit log.
Consumers authoring an operator-directed handoff (e.g. the
vibe-code-handoffskill) should passsource_trust="operator"oncreate_handoffso it picks up without confirmation.
Context sections are the mutable bridge surface, so they carry a monotonic
version token. Consumers should read with get_section, edit locally, then
call update_section(..., if_match_version=<version>). A stale token returns
ok=false, conflict=true, and a receipt_id instead of clobbering a newer
row. if_match_updated_at remains as a compatibility guard, but version is
the preferred token because timestamps have one-second resolution.
Existing-row blind writes are temporarily allowed in canary mode and returned as
legacy_blind_write=true; set BRIDGE_DB_CONTEXT_CAS_MODE=enforce to reject
them. New section inserts without CAS remain allowed.
export_bridge_markdown records the exported version/hash for each rendered
Claude.ai-owned context section. Later sync_from_file imports a changed
fallback-file section only if the DB still matches that exported base. If the DB
has advanced, the import is rejected and recorded in write_conflicts.
Use get_write_conflicts(status="open") to inspect stale section writes,
stale markdown imports, and raced handoff claims.
uv run pytest # run all tests
uv run pyright # type check (strict mode)
uv run ruff check # lint
uv run python -m bridge_db --doctor # local environment diagnostics
uv run python -m bridge_db --status # compact operator summary
uv run python -m bridge_db --dogfood # read-only observability dogfood pass
uv run python -m bridge_db --rebuild-content-index # repair FTS recall index drift
uv run python -m bridge_db --log-session-boundary bridge-db # FTS-safe CC hook logging
uv run python -m bridge_db # start MCP server (stdio)
uv run python -m bridge_db.migration # migrate from bridge markdownReplace /path/to/bridge-db below with the absolute path to your clone of this repo
(e.g. $(pwd) if you are already inside it, or ~/Projects/bridge-db as a common convention).
Claude Code (user-scoped):
claude mcp add --scope user bridge-db -- uv run --directory /path/to/bridge-db python -m bridge_dbCodex (~/.codex/config.toml):
[mcp_servers.bridge-db]
command = "uv"
args = ["run", "--directory", "/path/to/bridge-db", "python", "-m", "bridge_db"]- DB:
~/.local/share/bridge-db/bridge.db - Bridge file:
~/.claude/projects/<encoded-home>/memory/claude_ai_context.md(Claude Code encodes your home dir path by replacing/with-; the default is derived automatically at runtime — override viaBRIDGE_FILE_PATHif needed) - Retention: 50 activity entries per source; 10 snapshots per system family (Codex operating and consulted-node snapshots are retained independently)
- Health check:
healthMCP tool oruv run python -m bridge_db --doctor - Operator summary:
uv run python -m bridge_db --status - Dogfood pass:
uv run python -m bridge_db --dogfoodbundles the status, FTS index, WAL, recall, and shipped-sync audit checks used after bridge-sync runs - FTS repair:
uv run python -m bridge_db --rebuild-content-indexrebuilds the localcontent_indexfrom source tables when health reports recall-index drift - Session boundary logging: Claude Code's SessionEnd hook should call
uv run --directory /path/to/bridge-db python -m bridge_db --log-session-boundary <project>rather than writing SQLite directly; this path adds the FTS row and does not run activity retention pruning - Migration:
uv run python -m bridge_db.migration(idempotent — safe to re-run)
activity_log retention and shipped-sync receipts are separate surfaces:
activity rows are recent context, while shipped_sync_receipts is the proof
ledger for downstream shipped-event syncs. Treat processed_shipped_without_receipt=0
and fts_missing=0 as primary clean signals, and use
actionable_unprocessed_shipped=0 when policy dispositions explain why raw
unprocessed_shipped remains nonzero.
get_recent_activity is the raw compatibility feed: it returns individual
activity rows exactly as stored, including high-volume lifecycle telemetry such
as Claude Code SessionEnd rows tagged session-boundary. Operator-facing
consumers should use get_activity_signal instead. It keeps substantive rows
visible while compressing repeated lifecycle rows into aggregates keyed by
source, project, summary family, and hour/day time bucket. Raw rows and audit
events remain available for debugging and forensic review.
Activity rows preserve two time concepts:
timestampis the caller-supplied logical activity date or timestamp. When omitted bylog_activity, it defaults to the operator-local calendar date.created_atis the UTC insertion timestamp assigned by SQLite.
For activity discovery APIs with since (get_recent_activity,
get_activity_signal, and get_shipped_events), a row is visible when either
timestamp >= since or created_at >= since (with date-only values interpreted
as UTC midnight for created_at). This keeps closeouts created just after UTC
midnight discoverable even when their logical activity date is the prior local
day.
For Notion reconciliation, treat each shipped event's notion_sync object as the
machine-readable gate:
ready: fetch the explicitnotion_page_id, update only that row, fetch it again, then callconfirm_shipped_syncwith the readback proof.meta_no_target: do not update Notion. Confirm the event withdownstream_system=policyanddownstream_refpointing to the configured policy file after verifying the policy applies to the event.unmatched,no_notion_target, orregistry_unavailable: leave the event unprocessed and repair the project registry or mapping source first.
For non-receipt handling, use record_shipped_event_disposition only when an
operator policy says the row should remain auditable but should not proceed to a
downstream receipt. The disposition appears as policy_disposition on
get_shipped_events; it does not write a receipt and does not add PROCESSED.
Do not use mark_shipped_processed for SHIPPED rows; it is retained only for
non-shipped operational events such as TASK_DONE, APPROVAL_SENT,
PLANNING_APPLIED, or REVIEW_CLOSED.
Claude.ai may still write its owned sections directly to the bridge markdown file. To keep those edits from being overwritten on the next export, sync_from_file imports the four Claude.ai-owned sections (career, speaking, research, capabilities) from BRIDGE_FILE_PATH into context_sections before bridge consumers read from SQLite.
Claude Code's /start workflow now runs mcp__bridge_db__sync_from_file() before calling bridge read tools, so file edits are pulled into the DB at session start instead of waiting for a later export cycle.
The current operating model is:
- MCP is the primary coordination path.
sync_from_fileis the compatibility safety net for Claude.ai-owned file edits.export_bridge_markdownkeeps the fallback markdown artifact in sync for file-based consumers.
integration-spec.md— Claude.ai direct MCP and fallback-file integrationROADMAP.md— Execution roadmap for the next integration phasesdocs/EXTERNAL-WRITER-AUDIT.md— Direct bridge-db writer audit outside the repodocs/BRANCH-RETIREMENT.md— Branch cleanup and stale-ref disposition policy
docs/internal/OPERATOR-CHECKLIST.md— Local verification checklistdocs/internal/POST-SYNC-REVIEW.md— Bridge-sync post-run evidence checklistdocs/internal/PHASE-3-DECISION.md— Architectural decision: watcher vs startup syncdocs/internal/codex-migration.md— Per-consumer migration instructions