A bridge between the tools you work in and your personal knowledge base. One install lets
Claude Code, Pi and Emacs talk to the same Obsidian-style markdown vault — query it,
scan the current folder for material that belongs in it, or ingest a source — from any
directory, without cd-ing into the vault.
The harness is decoupled from the vault: it carries no notes of its own, just the wiring. Point
it at any vault with setup.sh.
A personal knowledge base is only useful if it's frictionless to reach. kbridge makes the vault a first-class target for the tools you already work in:
- Claude Code — a
/kbskill. - Pi — a
/kbcommand pluskb_search/kb_synctools the model can call directly. - Emacs — a streaming, multi-turn chat with the vault (
kb.el), backed by a persistent Pi process — point it at a local model for a fully private KB. See Emacs.
All three resolve the same recorded vault path, so they never drift apart.
git clone git@github.com:bes-dev/kbridge.git
cd kbridge
./setup.sh /path/to/vault # defaults to ~/.obsidiansetup.sh is idempotent. It:
- symlinks the Claude Code skills into
~/.claude/skills/, - records the vault path (read by all three integrations),
- installs the Pi extension globally via
pi install ./pi(ifpiis onPATH).
For the Emacs layer, see Emacs — it needs only a load-path entry on top of this.
No admin rights needed. From the cloned repo, in PowerShell:
powershell -ExecutionPolicy Bypass -File setup.ps1 -VaultPath C:\path\to\vaultsetup.ps1 copies the /kb skill into %USERPROFILE%\.claude\skills\ (symlinks need
admin/Developer Mode on Windows) and records the vault path there. Re-run it after a git pull
to refresh; your local state (vault_path, sync_remote, index_chats) survives re-runs.
That's the whole setup: /kb then works in Claude Code — the CLI and the Claude desktop app
share the same ~\.claude\skills\ — using Claude's built-in cross-platform tools. Note the
skill is a Claude Code skill; the plain chat side of the desktop app doesn't load skills.
Optional extras on Windows:
- Sync & source compilation — install Git for Windows (has a
per-user, no-admin installer). Claude Code then uses Git Bash for its Bash tool, so
kb-sync(plain git remote) andkb-convertwork; add a converter (pip install --user markitdownordocling) for best results. Without Git Bash the/kbskill still ingests documents — it falls back to reading them in batches via subagents. Encryptedgcrypt::remotes are not supported on Windows. - Pi / Emacs — not covered by
setup.ps1; the Pi extension currently shells out to Unixgrep/findand is not Windows-ready.
Claude Code — from any directory:
claude "/kb что у меня есть про X" # query the vault
claude "/kb осмотрись тут, это для проекта Y" # scan the CWD, propose an ingest planPi — from any directory:
/kb <request>loads the vault rules and detects the mode automatically:- query — search and answer with
[[wikilink]]citations (read-only), - scan — survey the current folder and propose an ingest plan (waits for confirmation),
- ingest — write pages, update
index.md, append tolog.md, cross-reference.
- query — search and answer with
kb_search— a tool the model can call to find pages by content or filename, scoped to the vault, from anywhere.kb_sync— a tool to push/pull the vault to its (optionally encrypted) remote (see below)./kb-index-chats— toggle whetherchats/(saved transcripts) is included in search; off by default (see Saved chats).
Emacs — C-c k k opens the chat; select a region and C-c k i files it into the vault;
C-c k b / C-c k d attach the current buffer or directory as context. Full tour below.
The vault's own AGENTS.md is the source of truth for conventions — kbridge points the agent at
it rather than duplicating the rules.
Dropping a huge PDF straight into a chat makes any agent slow and dumb: the raw pages flood the context window, and every later answer degrades. kbridge instead compiles a source once — NotebookLM-style, but in plain markdown — and then every question is cheap:
- Convert + split —
kb-convert <file-or-url> [name]converts with the best locally-available tool (docling / markitdown / pandoc / pdftotext, each optional —kb-convert checkshows what you have) and splits by structure into<vault>/raw/<name>/NNN-<section>.md, keeping the untouchedoriginal.*alongside. Files are the chunks, the filesystem is the index — no vector store, plain grep. - Distill a map —
kb-convert distill <name>writesraw/<name>/_map.md: per-section summaries produced by one-shot LLM calls in clean contexts (defaultpi --print, override with$KB_DISTILL_CMD). Agents with subagents (Claude Code) can build the map themselves in parallel instead. - Chat — agents follow a map-first contract: read
_map.md, grep the sections, read only what matched. The contract ships in the/kbskill and the Pi kickoff (akb_converttool mirrors the script), so it works in Claude Code, Pi and the Emacs chat alike.
Any format works — PDF, DOCX, PPTX, HTML, URLs, plain text; only the conversion step knows the difference. And since the compiled source is just markdown in the vault, you can compile once with your strongest agent (e.g. Claude Code, with parallel subagents) and query from anywhere — including a fully-local model in Pi/Emacs.
emacs/kb.el adds an agent layer on top of your editor. Browsing, reading, editing and
[[wikilinks]] are already handled far better by obsidian.el/markdown-mode — this module
adds what they can't: a streaming, multi-turn chat with the vault, backed by a persistent
pi --mode rpc process. That buys you:
- privacy — run a local model; the vault never leaves your machine,
- memory — the session survives across turns, so the conversation has context,
- liveness — tokens stream in as they're generated; the single-threaded Emacs UI never blocks while the model thinks.
Requirements:
- Emacs 28+ (uses
json-serialize/json-parse-string; developed on 30). pionPATH— for GUI Emacs on macOS,exec-path-from-shellimports it.- kbridge installed (
./setup.sh), so~/.claude/skills/kb/vault_pathexists. - a Pi provider/model configured — point Pi at a local model for a fully private KB.
Minimal (vanilla). kb.el ships commands only — bind them under a free prefix (C-c k keeps
clear of obsidian.el's C-c n map):
(add-to-list 'load-path "/path/to/kbridge/emacs")
(require 'kb)
(setq kb-pi-model '("--model" "qwen/qwen3.6-27b")) ; your local model; omit for Pi's default
(global-set-key (kbd "C-c k k") #'kb-chat)
(global-set-key (kbd "C-c k i") #'kb-ingest-region)
(global-set-key (kbd "C-c k b") #'kb-discuss-buffer)
(global-set-key (kbd "C-c k d") #'kb-discuss-directory)
(global-set-key (kbd "C-c k s") #'kb-sync)With use-package + straight.el (deferred — loads on first use):
(use-package kb
:straight nil
:load-path "/path/to/kbridge/emacs"
:commands (kb-open-page)
:bind (("C-c k k" . kb-chat) ; open the chat (alias: M-x kb-ask)
("C-c k i" . kb-ingest-region) ; file the region into the vault
("C-c k b" . kb-discuss-buffer) ; attach this buffer/region (read-only)
("C-c k d" . kb-discuss-directory) ; attach this dir (works in dired)
("C-c k s" . kb-sync)) ; push / pull / status
:init (setq kb-pi-model '("--model" "qwen/qwen3.6-27b")))C-c k kkb-chat— open the chat (a normal, splittable window by default). Type a message,RETsends,C-jinserts a newline,C-c C-kaborts the current turn (the session stays alive). Answers stream in live, get lightweight markdown rendering (headings, bold, italics, inline code, bullets), and[[wikilinks]]are clickable — they open the vault page.C-c k bkb-discuss-buffer— attach the current buffer (or active region) to the chat as read-only context. The chat shows🔒 read-only; pressC-c C-eto flip to✎ edit— the source file getsauto-revertand the agent may edit it in place (your next message carries the grant).C-c C-eagain returns to read-only.C-c k dkb-discuss-directory— attach the current directory (a dired folder, elsedefault-directory) plus a one-level listing, so the agent works in it by absolute path.C-c k ikb-ingest-region— hand the region to the chat agent to file into the vault.C-c k skb-sync— push / pull / status via the samekb-syncscript, asynchronously.
C-c C-m— switch the Pi model live, mid-conversation, no process restart: pick from Pi's available models with completion.- The mode line shows the active model and its context budget (e.g.
Qwen 3.6 27B (12K/131K)), a spinner while a turn streams, and the tool currently running (kb_search,read, …) — tool activity stays out of the transcript. - The header line is a legend of the active keys, including
chats:on/off—C-c C-itoggles whetherchats/is searched (the flag is shared with Pi and Claude). C-x C-s— save the transcript into the vault'schats/, suggesting a<date>-<slug>.mdname derived from your first message; after the first save the buffer visits that file, so further saves just update it.
Attached context shows as a 📎 line and rides your next message, so the transcript shows
only your question.
The chat opens in a normal, splittable window by default. Route it wherever you like with
display-buffer-alist (or set kb-chat-display-action) — e.g. a right side window:
(add-to-list 'display-buffer-alist
'("\\*kb-chat\\*"
(display-buffer-in-side-window) (side . right) (window-width . 0.5)))The process lifecycle is automatic: the persistent pi --mode rpc starts on first use, survives
across turns (keeping conversation memory), and is killed when you close the *kb-chat* buffer
or quit Emacs — no orphaned processes.
The vault is resolved like everywhere else in kbridge — kb-vault-directory if you set it, else
the recorded vault_path, else obsidian.el's obsidian-directory as a last resort — so
Emacs, Pi and Claude always operate on the same vault. Browsing/editing stay with
obsidian.el/markdown-mode.
Chats are ephemeral by default — the value is what you distill into the vault, not the raw
transcript. But you can keep a session when you want: in the Emacs chat, C-x C-s writes the
buffer to <vault>/chats/ (created on first save), suggesting a <date>-<slug>.md name derived
from your first message.
chats/ is a manual drop-spot, not a managed layer — and it's excluded from search by
default so old chatter doesn't dilute retrieval. Toggle it into the index when you want to recall
from past chats, via a shared flag (~/.claude/skills/kb/index_chats) that all three agents read:
- Pi —
/kb-index-chats - Emacs —
C-c C-iin the chat (header showschats:on/off) - Claude — ask
/kbto include/exclude chats
By default the vault is plain markdown on local disk — convenient locally, but risky if you sync
it to Dropbox/gdrive in the clear. kbridge can instead back the vault with an end-to-end-encrypted
remote via git-remote-gcrypt: the local copy
stays plaintext (so the agents keep reading/grepping it natively), and only what is pushed to the
remote is ciphertext — filenames and history included.
Export an existing vault to an encrypted remote (one-time):
# requires: brew install git-remote-gcrypt gnupg
kb-sync init gcrypt::<remote-url> [gpg-key-id] # omit the key id for a symmetric passphrase<remote-url> is any git/rsync/sftp target — e.g. a folder your native Dropbox/gdrive client
already syncs (gcrypt::/Users/me/Dropbox/kb.git). Then keep it in sync:
kb-sync push # commit local changes and upload
kb-sync pull # download remote changes
kb-sync status # show mode (plain / encrypted) and remoteAll three integrations expose this: a kb_sync tool and /kb-sync command in Pi, C-c k s
(kb-sync) in Emacs, and the same ~/.claude/skills/kb/kb-sync script driven from the Claude
/kb skill.
Who holds the password: nobody in kbridge. The passphrase/key lives in gpg-agent and, on macOS,
the Keychain (via pinentry-mac) — the harness only runs git push/pull.
Threat model: this protects the remote (the cloud sees only ciphertext). Local device loss is covered by full-disk encryption (FileVault), not by this layer. The working copy is necessarily plaintext while you work on it.
The vault location is resolved in this order:
$KB_VAULTenvironment variable (override for a single session),- the
vault_pathrecorded bysetup.sh(shared by all integrations), - the globally-installed Claude skill's
vault_path.
In Emacs the same chain applies via kb-vault-path-file, overridable with kb-vault-directory
(and falling back to obsidian-directory if you use obsidian.el).
The sync remote is resolved the same way: $KB_SYNC_REMOTE, else the sync_remote file that
kb-sync init records next to vault_path. A gcrypt:: URL means encrypted; absent means plain
local-only.
kbridge/
├── setup.sh # installs the harnesses, records vault_path
├── setup.ps1 # Windows: copies the Claude skill per-user, no admin
├── .claude/skills/kb/
│ ├── SKILL.md # Claude Code /kb skill
│ ├── kb-convert # any document/URL → greppable raw/ sections + _map.md
│ ├── kb-sync # shared sync script (plain / gcrypt-encrypted remote)
│ ├── vault_path # recorded vault path (gitignored; read by all)
│ ├── sync_remote # recorded remote URL (gitignored; read by all)
│ └── index_chats # flag: include chats/ in search (gitignored; toggle)
├── pi/
│ ├── package.json
│ └── kb-harness.mjs # Pi ext: /kb /kb-sync commands + kb_search/kb_sync tools
└── emacs/
└── kb.el # Emacs: streaming chat, live model switch, context attach
Apache-2.0