A host that answers `vaults` with an error is a staged copy older than the popup, which looked exactly like "you only have one vault" — the picker silently fell back to its single default option. A missing host stays silent, since pressing Fill already explains that one. Also document that a second vault needs a ~/.obsidian-wiki/config.<name> profile before it can appear in the list.
9.9 KiB
Browser Extension
extensions/brain/ is a zero-build Chrome extension that connects your browser
to your vault in both directions:
- Capture — save the current page, or a selection, into
_raw/as markdown. - Fill form — answer the form on the current page from what your vault already knows, using the Claude Code or Codex CLI you already pay for.
Capture needs nothing but the extension. Fill also needs a small native messaging host, because no reasoning model can run inside a browser extension.
Install
- Open
chrome://extensionsand enable Developer mode. - Load unpacked → select
extensions/brain.
Installed from PyPI rather than a checkout? The extension ships in the package. Ask for its path:
obsidian-wiki info # → extension: …/obsidian_wiki/_data/extension
and load that folder unpacked instead.
That is all capture needs. For form filling, also run:
extensions/brain/host/install.sh
then fully quit Chrome (⌘Q — closing the window is not enough) and reopen it, so it reads the new host manifest.
The extension carries a fixed public key, so its unpacked ID is stable and the
host can be registered before the extension has ever been loaded. A Web Store
listing gets a different ID; pass it in with install.sh <extension-id>.
Capture
Pick your vault's _raw folder once:
awk -F= '/^OBSIDIAN_VAULT_PATH=/{print $2 "/_raw"; exit}' "$(git rev-parse --show-toplevel)/.env"
Then Capture current page, or right-click a page or selection and use the
context menu. Each capture is written as YYYY-MM-DD-page-title.md with YAML
frontmatter (title, source URL, timestamps), your optional note, any selected
text, and the readable page text capped at 140,000 characters.
Captures are staged, not finished pages. Promote them with:
/wiki-ingest promote my raw pages
Fill form
Open a form, switch to the Fill form tab, pick a vault and an engine, click Fill from brain.
content script popup + native host native host (python)
────────────────── ─────────────────── ─────────────────────────────
extract form schema → sendNativeMessage → 1. obsidian-wiki context-pack --json
(label, type, deterministic, no LLM
options, maxLength) 2. claude -p / codex exec
subscription-billed
apply values ← {fills:[…]} ← 3. validated {id, value,
+ outline each field confidence, source}
The split is the point. Retrieval never calls a model — context-pack is
plain ranking over the vault, the same machinery wiki-context-pack uses. The
model is handed the pages and does one narrow job: map facts onto fields. That
keeps each fill to a single CLI invocation and means the model never gets to
decide what "searching your vault" means.
Filled fields are outlined green (confidence ≥ 0.7) or red (below it, worth re-reading) and carry a tooltip naming their source page. The popup lists every value with its confidence and origin.
It never submits. Filling is the entire feature.
Choosing which vault answers
The Answer from dropdown lists every vault profile wiki-switch knows about
— Default vault (whatever ~/.obsidian-wiki/config currently points at) plus
one entry per ~/.obsidian-wiki/config.<name>. It is the extension's equivalent
of the @name override: a job application answers from personal, a vendor
questionnaire from work, without re-pointing the symlink for everything else.
A second vault only shows up once it has a profile file. Owning the folder is
not enough — create one with /wiki-switch new <name>, or by hand:
printf '# My other vault\nOBSIDIAN_VAULT_PATH="%s"\n' ~/Knowledge \
> ~/.obsidian-wiki/config.knowledge
If the dropdown shows only Default vault when profiles do exist, the staged
host predates this feature — re-run install.sh and restart Chrome. The popup
says so explicitly rather than pretending you have one vault.
The choice is remembered per browser profile, so a vault picked once stays
picked. A profile whose OBSIDIAN_VAULT_PATH no longer exists is listed as
(missing) and cannot be selected — and if it was the remembered one, the popup
says so and falls back to the default rather than quietly answering from a vault
you did not choose. Only the fill side reads this; capture writes to the _raw
folder you picked with the folder picker.
What it will not fill
- Anything the retrieved pages do not support — omitted, never guessed.
- Personal identity fields (name, email, phone, address, ID numbers) unless the vault explicitly contains that exact value.
- Passwords and file inputs, which are never read into the schema at all.
- Any value that fails validation against the real form — an option absent from
the
<select>, a field id the page never advertised — is dropped by the host before it reaches the page.
A field asking for several things at once ("name, email, and company") is answered with the parts the vault supports, at reduced confidence, rather than skipped outright.
Getting good fills
Two properties of retrieval decide almost all of the quality.
Put what matters at the top of the page. When many pages compete for the
token budget, context-pack compresses each one to roughly its opening block.
Anything below that — a table halfway down, a detail in the last section — is
invisible to the model even though the page was retrieved. Keep fillable facts
in the summary: frontmatter and the first paragraph.
Keep one profile page for personal details. Something like
entities/<you>.md, tagged visibility/pii so it stays out of public-facing
queries, whose opening paragraph states the values in prose:
---
title: Your Name
tags: [owner, profile, visibility/pii]
summary: "Vault owner: <name>, email <email>, GitHub <handle>, site <url>."
---
# Your Name
First name **X**. Email **y@example.com**. GitHub **github.com/z**.
Leave unknown fields absent rather than filling them with placeholders. A
TODO is a value the model can autofill into a real form; an absent field simply
stays blank. Listing the missing field names under a "Not recorded" heading gives
you the checklist without the risk.
Forms whose questions your vault genuinely does not cover will come back mostly empty. That is the system working — the alternative is plausible fiction in a form you are about to submit.
Security model
Vault excerpts and scraped form labels are both untrusted input. The context
pack carries an instruction_policy string that is forwarded into the prompt
verbatim, and the model is told to treat all page content as data rather than
instruction. The host then re-validates every returned fill against the form
schema, so an injection that convinces the model to emit a value still cannot
write to a field the page never offered.
Nothing leaves your machine except the model call itself, which goes through the same CLI — and the same account — you already use in the terminal.
Why the host is staged outside the repo
install.sh copies the host and a snapshot of the obsidian_wiki package into
~/.obsidian-wiki/brain-fill/ rather than running them from your checkout.
Three things force this, all variations on "Chrome's child processes do not
inherit your shell":
- macOS TCC.
~/Documentsis mode 700 with a TCC ACL. A native messaging host inherits Chrome's TCC context, so a checkout living there cannot even be exec'd. Chrome reports this asNative host has exited, which is indistinguishable from a crash. - PATH. Chrome hands children a minimal PATH, so
claudeandcodexin~/.local/binare invisible. The launcher bakes in the install-time PATH. - USER / LOGNAME. Claude Code reads its OAuth credentials from the macOS
login Keychain and needs these set to find them. Without them it reports
Not logged in · Please run /logindespite a perfectly valid session.
Re-run install.sh after editing the host or updating the package — the staged
copy does not track the repo.
Cost
Retrieval is free. Each fill is one claude -p or codex exec invocation
billed to your existing subscription. Cost scales with the size of the context
pack, so a tighter --budget and a well-summarised vault make fills cheaper as
well as better.
Troubleshooting
Logs live at ~/.obsidian-wiki/brain-fill.log. Every run brackets itself with
── start and ── exit, which is what separates the failure modes:
| Symptom | Meaning |
|---|---|
Native host has exited, no new ── start |
Chrome could not launch the process at all — path or permissions. Re-run install.sh. |
── start with no ── exit |
It launched and died; a fatal: traceback follows. |
── start … ── exit: clean but the popup errors |
The pipe closed before the answer was ready. |
| Message | Fix |
|---|---|
Specified native messaging host not found |
Not registered. Run install.sh, ⌘Q Chrome, reopen. |
claude: Not logged in |
Launcher missing USER/LOGNAME. Re-run install.sh. |
No obsidian-wiki with context-pack support |
Staged package snapshot is stale. Re-run install.sh — it verifies the subcommand and refuses to install without it. |
Read N pages, but none of them answer these fields |
Retrieval worked; the vault does not cover it. See Getting good fills. |
Drive the host without Chrome at all:
python3 extensions/brain/host/test_host.py claude # or: codex
python3 extensions/brain/host/test_host.py claude work # answer from config.work
python3 extensions/brain/host/test_host.py vaults # list what the popup would offer