obsidian-mcp-router
by tboome33
README.md
<p align="center">
<img src="./docs/assets/logo.png" alt="obsidian-mcp-router â multi-vault MCP server" width="540">
</p>
<p align="center">
<a href="https://github.com/tboome33/obsidian-mcp-router/actions/workflows/test.yml"><img src="https://github.com/tboome33/obsidian-mcp-router/actions/workflows/test.yml/badge.svg" alt="tests"></a>
<a href="./LICENSE"><img src="https://img.shields.io/badge/license-Apache%202.0-blue.svg" alt="license"></a>
<a href="https://nodejs.org"><img src="https://img.shields.io/badge/node-%E2%89%A520.19.0-brightgreen.svg" alt="node"></a>
<a href="./CHANGELOG.md"><img src="https://img.shields.io/badge/version-0.97.0-blueviolet.svg" alt="version"></a>
</p>
# obsidian-mcp-router
> *đŹđ§ English version below â [đ«đ· version française](#-version-française)*
> An MCP server that routes Claude tool calls to **multiple** Obsidian vaults â local or remote â over the [Local REST API](https://github.com/coddingtonbear/obsidian-local-rest-api) plugin.
Instead of registering one MCP per vault (one process, one port, one API key), this router exposes a single MCP that knows about every vault you've configured. Each tool takes a `vault` parameter (or uses your default), and the router fans out the HTTPS call to the right Obsidian instance.
## Why
If you keep more than one Obsidian vault â local or remote, in any combination â you don't want to register a separate MCP server per vault and switch context every time. This router is one process that knows about all of them and routes each tool call to the right one based on a `vault` parameter.
What you get:
- **One install** â the Claude Code plugin ships and launches the server (one `~/.claude.json` entry on dev setups) â all vaults visible from any Claude Desktop/Code session.
- **Local + remote vaults**, treated identically. Drop the URL + API key into the config; the router doesn't care where the vault actually runs.
- **Cross-vault search**: pass `vault: "*"` to the `search` tool to fan-out across every vault in parallel.
## Capabilities
| Tool surface | Coverage |
|---|---|
| Vault discovery | `list_vaults`, `list_files` |
| Reads | `get_file`, `search`, `search_smart`, `get_frontmatter` |
| Writes | `write_file`, `append_to_file`, `patch_file`, `delete_file`, `set_frontmatter`, `merge_frontmatter` â `write_file`/`patch_file`/`delete_file`/`merge_frontmatter` accept **`ifMatch`**: replay `get_file`'s `contentSha256` and the write is refused with a 409 if the file changed since you read it (optimistic concurrency â stops parallel sessions from silently clobbering each other) |
| File management | `move_file` (also accepts `ifMatch`, checked against the source) |
| Templater | `execute_template` |
| Router state | `lock_vault`, `unlock_vaults`, `set_auto_enrich_mode` |
| Workspace binding | `confirm_workspace_binding` (bind this workspace to a primary vault, add secondaries, refuse a proposal), `set_secondary_vault_mode` (a secondary's write tier: `locked` / `soft` / `writable`) |
| Vault provisioning | `plan_vault`, `provision_vault` â defaults-first vault-creation wizard engine; `register_remote_vault` records an already-running remote vault (URL + key) straight from a conversation |
| Conversion | `pdf_to_markdown`, `docx_to_markdown`, `xlsx_to_markdown`, `pptx_to_markdown`, `image_to_markdown`, `audio_to_markdown`, `youtube_to_markdown`, `bing_search_to_markdown`, `webpage_to_markdown`, `git_repo_to_markdown`, plus `pdf_to_markdown_docling` (opt-in high-fidelity PDF via [Docling](https://github.com/docling-project/docling), MIT) â port of [zcaceres/markdownify-mcp](https://github.com/zcaceres/markdownify-mcp) (MIT). Also `pptx_extract_assets` (a deck's embedded images to disk, per slide â no Python), `pdf_to_images` (render PDF pages to PNG the model can *see*) and `filter_relevant_blocks` (BM25 relevance filter over already-acquired markdown). |
| Web/page metadata | `extract_page_metadata`, `propose_linked_sources`, `download_page_assets` |
| Wiki maintenance & sources | `write_bundle` (journaled multi-file bundle â all-or-nothing apply with rollback), `refresh_okf_projections` (regenerate the generated OKF navigation), `build_search_index` (local BM25 search tier, works on every vault), `record_source` / `audit_sources` (provenance ledger for ingested content), `audit_vault_conventions` (which conventions a vault is really under â ambiguous files, missing recommended ones, template copies; read-only, repairs proposed never applied) |
| Context & graph | `get_wiki_context_pack`, `build_wiki_graph`, `build_wiki_tour`, `get_page_neighbors`, `wiki_path`, `find_boundary_pages`, `find_twin_pages`, `build_open_link`, `open_in_obsidian`, `get_view_link` |
| Cross-vault | every tool accepts `vault: "*"` for fan-out |
Semantic search (`search_smart`), Templater execution (`execute_template`) and click-to-open links (`build_open_link`, `open_in_obsidian`, the auto-emitted `clickToOpenUrl` on write results) require the [`obsidian-mcp-router-bridge`](https://github.com/tboome33/obsidian-mcp-router-bridge) plugin to be installed in each target vault â it registers the matching `/search/smart`, `/templates/execute` and `/open/*` routes on Local REST API. Bridge **â„ 0.7.0** also registers `PUT /vault-cas/*`, which makes `ifMatch` writes **atomic** (read-compare-write inside the Obsidian process); without it, `ifMatch` still works everywhere through a checked â but non-atomic â GET-compare fallback. Bridge **â„ 0.9.0** additionally serves `GET /smart-env/sources` â the Smart Connections vector store, a dot-directory Local REST API itself will not serve â which is what lets `find_twin_pages` run against a **remote** vault; and its loopback-only `GET /ping?v=<vault>` answers 200 only for the vault actually listening on that port, the one-click self-test behind click-to-open port checks. The conversion tools require Python 3.10+ on `PATH` plus an explicit `npm run install-markitdown` (opt-in) â see the **Conversion tools â runtime dependencies** section below. Everything else works against the standard Local REST API endpoints alone.
## Limits â what the router does **not** do
Worth reading before you design anything on top of it. None of these are bugs; they are the boundaries of the design.
**It does not read your vault from disk.** Every call goes over HTTPS to the Local REST API of a **running** Obsidian. Close Obsidian and the vault is simply unreachable (`ECONNREFUSED`) â the router will not fall back to the filesystem, deliberately, because the filesystem cannot honour the plugin's locks, the bridge's atomic writes, or the vault's own indexes. A vault whose Obsidian is closed is not "degraded"; it is offline.
**It does not run, install, sync or deploy Obsidian itself.** It configures and talks to vaults. Provisioning a vault (`provision_vault`) writes a vault skeleton; it does not install Obsidian, open it, or keep two machines in sync. There is no git integration and no conflict *resolution* â when two writers collide the router **refuses**, it never merges.
**A repository's dotenv file has no authority.** It may *propose* â seven keys, no more; anything else in it is ignored and named on the router's stderr. Your own router config (`~/.claude/obsidian-mcp-router/config.json`) decides. This is the whole grid: **the dotenv file proposes, the config decides.** A cloned repository can therefore never redirect your writes; at most it can raise a question, once.
**Secondary vaults are read-only by default.** A vault a workspace declares under `also`, without appearing in any tier list, opens at the `soft` tier: reads work, and a write is refused until it is confirmed once (`confirmSecondaryWrite: true`) or the vault is promoted permanently (`set_secondary_vault_mode({ mode: "writable" })`, or the config's `alsoWritable`). `alsoLocked` is stronger still â a **server-side** refusal no parameter can override.
**A vault two workspaces declare requires a precondition on every write.** The requirement is *computed* from the binding registry, never declared, so it can switch on without you editing anything. `write_file` and its siblings then refuse a blind call and want `ifMatch` (or `ifNew`, a per-step precondition, or an approved-plan seal). This protects writers who go through the router **from each other** â it cannot see an edit made directly in Obsidian's UI.
**`ifMatch` is only atomic with the bridge.** With `obsidian-mcp-router-bridge` â„ 0.7.0 the compare-and-write happens inside the Obsidian process. Without it the check still runs everywhere, but as a GET-compare-then-write: a narrow window remains.
**Reachability is opt-in, and cuts hard when on.** With `vaultReach: "declared"` a workspace reaches only the vaults it declares, plus `openVaults`. An unbound session â Claude Desktop chat, for instance â sees only `openVaults`. That is the intent; it is also the fastest way to make every vault vanish at once if you set it without an exception list.
**On a gated deployment, nothing persists.** When `OBSIDIAN_ROUTER_READONLY`, `OBSIDIAN_ROUTER_ALLOWED_VAULTS` or `OBSIDIAN_ROUTER_USER_ID` is set, one directory serves many callers, so a persisted answer would speak for all of them: the binding tools, `register_remote_vault`, and the `persist` form of `lock_vault` / `unlock_vaults` / `set_auto_enrich_mode` are refused or hidden. Session-only forms still work.
**Optional pieces are genuinely optional.** Semantic search (`search_smart`), Templater (`execute_template`) and click-to-open need the bridge plugin in each target vault. The conversion tools need Python 3.10+ and an explicit `npm run install-markitdown`. Docling is a separate opt-in again. Without them those tools report their missing dependency rather than guessing.
## Deployment modes
The router runs in two modes, controlled entirely by environment variables â **no code change**, **no separate binary**:
### Local mode (default)
No env vars set. Single process, stdio MCP transport, launched by the Claude Code plugin (or registered once in `~/.claude.json` user scope on dev setups). The router sees every vault listed in `~/.claude/obsidian-mcp-router/config.json`. This is what you get when you follow the install steps below.
### Multi-tenant mode (opt-in)
Three independent env vars turn the router into a scoped instance â useful when you run multiple copies behind a hub (MCPHub, `mcpo`, a custom proxy) and want each instance to expose a different subset of vaults to a different user.
| Env var | What it does | Default when unset |
|---|---|---|
| `OBSIDIAN_ROUTER_ALLOWED_VAULTS=a,b,c` | Whitelist of vault names this instance sees. Comma-separated, spaces tolerated. Vaults outside the list are moved to `skipped[]` with reason `"not in OBSIDIAN_ROUTER_ALLOWED_VAULTS whitelist"`. Applied **before** default-vault resolution, so `defaultVault` falls through to the filtered set. | All vaults visible |
| `VAULT_<NAME>=<JSON>` | A vault defined entirely in an env var (JSON) â editable from the MCPHub dashboard. A 3rd config source merged after `portRegistry` + `remoteVaults` (overrides any same-name vault). Required: `name`, `baseUrl`, `apiKey` (the **bare token**). Optional: `description`, `tlsInsecure`, `timeoutMs`. Malformed entries are skipped with a redacted warning. See "[`VAULT_*` env-var config](#vault_-env-var-config-dashboard-editable)" below. | (none) |
| `OBSIDIAN_ROUTER_READONLY=true` | Disable write tools. The 18 write tools (`write_file`, `append_to_file`, `patch_file`, `set_frontmatter`, `merge_frontmatter`, `move_file`, `delete_file`, `execute_template`, `download_page_assets`, `pptx_extract_assets`, `build_wiki_graph`, `provision_vault`, `register_remote_vault`, `refresh_okf_projections`, `write_bundle`, `record_source`, `install_conventions`, `build_search_index`) are filtered from `ListTools` **and** refused at `CallTool` time â even when a client knows the name and calls it directly. Truthy tokens: `true` / `1` / `yes` / `on` (case-insensitive). | Write tools enabled |
| `OBSIDIAN_ROUTER_USER_ID=<slug>` | Audit log: every **successful** write call appends a line `[claude-write by <slug>] YYYY-MM-DD HH:MM â <tool> path="<path>"` to the touched vault's `wiki-meta/journal.md`. Best-effort (audit failure logs to stderr, never blocks the write). Uses the REST client directly to avoid the recursion that would happen via the `append_to_file` tool wrapper. Setting it also marks the deployment as **gated**, which hides the local-only `plan_vault` / `provision_vault` tools. | No audit log |
The three vars compose freely: an instance can be scoped to one vault (`ALLOWED_VAULTS=karine`) AND read-only (`READONLY=true`) AND attribute writes (`USER_ID=karine-guest`). Setting none = local mode exactly.
Concrete deployment example (MCPHub `mcp_settings.json` entry):
```json
"obsidian-router-roland": {
"command": "obsidian-mcp-router",
"env": {
"OBSIDIAN_ROUTER_ALLOWED_VAULTS": "roland,tribu,projects",
"OBSIDIAN_ROUTER_USER_ID": "roland"
}
}
```
See `wiki/obsidian-mcp-router sur Dedibox et MCPHub/` in the project's meta vault for the complete multi-tenant deployment recipe (bundle `.mcpb`, MCPHub Keys with Access Scope, NPM front, Self-hosted LiveSync, etc.).
### Served mode â reaching the local router from a remote session
A remote Claude Code session (a dev box over SSH) can't just run the router: 11 of its modules legitimately touch the vaults' **disk**, so porting it ships it half-broken. Instead the router stays home and is **served** over an authenticated streamable-HTTP endpoint, reached through the existing SSH tunnel:
```bash
node scripts/serve-http.mjs [--port 27300] [--session-timeout-min 240]
```
It binds `127.0.0.1` only (deliberately not configurable), requires a bearer token on **every** verb (`OBSIDIAN_ROUTER_HTTP_TOKEN`, or `~/.claude/obsidian-mcp-router/serve-http.token`), and refuses to start without one. Each MCP session gets **its own child router process**, so a vault lock taken by one session is invisible to another â exactly the isolation stdio sessions already have.
| Flag | What it does | Default |
|---|---|---|
| `--port <n>` | Listen port on loopback. | `27300` |
| `--session-timeout-min <n>` | Idle threshold before a session is reaped and its child killed. Minimum 1. | **240** (4 h) |
**Why the timeout defaults to four hours and not thirty minutes.** A dropped tunnel is not a `DELETE` â without reaping, vanished clients leave zombie children (six were measured in the 2026-08-28 spike), so the reaper is mandatory. But its *scale* matters more than its existence: a threshold shorter than an ordinary human pause harvests **live** sessions. With a 30-minute threshold, a multi-hour session loses the router mid-flight while the user is merely running a script on their own machine â and Claude Code does not restore an MCP server that dies mid-session, so the tools are gone for the rest of the sitting. The two failure modes are not comparable: too short costs the user their tools for hours with no in-session recovery, too long costs one dormant child process until the threshold. Lower it if you serve many clients from one host and dormant children are your dominant cost.
**An expired session answers `404`, and that is on purpose.** The server never silently respawns a child for an unknown session id. It would look seamless and it would be a lie: the per-session state (vault lock, auto-enrichment mode, the once-per-session conformance pass) would have reset under an id the client believes is stable. Recovering from the `404` by re-initializing is the client's job.
## Slash commands & skills (Claude Code plugin)
The repo doubles as a **Claude Code plugin marketplace** that exposes **54 slash commands** under the `/obsidian-router:*` namespace. Type `/obsidian-router:` in Claude Code â the autocomplete shows everything. Every slash command also auto-triggers on natural-language phrasing (EN + FR) so you rarely have to remember the exact name â just describe what you want.
> đ **Quick reference PDF** (router overview + setup + config + every slash command with NL trigger phrases) â [English](./docs/quick-reference-en.pdf) · [Français](./docs/quick-reference-fr.pdf). Printable, accessible font sizes â for paper or screen reference.
> đ **Feature guide (prose, by category)** â the tables in this README are a reference card; for a readable walkthrough of every feature (the need it answers, what it does, how to use it), see [`docs/features/`](./docs/features/README.md) (13 categorized pages, French).
### đ§ 17 MCP wrappers â one per core vault tool
#### `discover/` (2)
| Command | Effect | Trigger phrasings |
|---|---|---|
| `/obsidian-router:discover-list-vaults` | List every configured vault (local + remote) with online/offline/latency, default-vault, lock state | *"list my vaults"*, *"are my vaults online"* / *"liste mes vaults"*, *"mes vaults sont-ils en ligne"* |
| `/obsidian-router:discover-list-files` | List files and subdirectories of a vault path | *"list files in Sessions"*, *"what's in <folder>"* / *"liste les fichiers de Sessions"*, *"qu'est-ce qu'il y a dans <dossier>"* |
#### `read/` (4)
| Command | Effect | Trigger phrasings |
|---|---|---|
| `/obsidian-router:read-get` | Read a file in full (markdown + frontmatter + meta); returns `contentSha256`, the `ifMatch` token for conditional writes | *"show me X"*, *"open the file X"* / *"montre-moi X"*, *"ouvre le fichier X"* |
| `/obsidian-router:read-search` | Plain-text (substring) search with surrounding context | *"find <text> in my vault"*, *"grep for X"* / *"trouve <texte> dans mon vault"*, *"grep <X>"* |
| `/obsidian-router:read-search-smart` | Semantic search via Smart Connections (cosine scores + breadcrumbs) | *"find notes about X"*, *"semantic search for X"* / *"trouve mes notes sur X"*, *"recherche sémantique sur X"* |
| `/obsidian-router:read-frontmatter` | Read frontmatter (whole object or one key, types preserved) | *"what's the status of X"*, *"show me the metadata of X"* / *"quel est le statut de X"*, *"montre les méta de X"* |
#### `write/` (6)
| Command | Effect | Trigger phrasings |
|---|---|---|
| `/obsidian-router:write-create-or-replace` | PUT â create a new file or replace an existing one; optional `ifMatch` = atomic compare-and-swap (refused if the file changed since you read it) | *"create a note X"*, *"save this as X.md"* / *"crĂ©e une note X"*, *"enregistre ça comme X.md"* |
| `/obsidian-router:write-append` | POST â append to an existing file (auto-creates if missing) | *"append to my journal"*, *"add a line to X"* / *"ajoute Ă X"*, *"rajoute Ă la fin de X"* |
| `/obsidian-router:write-patch` | Surgical PATCH on heading / block / frontmatter | *"edit the X section in Y"*, *"replace the content under X"* / *"édite la section X dans Y"*, *"remplace le contenu sous X"* |
| `/obsidian-router:write-frontmatter-set` | Set/replace a single frontmatter key | *"set status to closed on X"*, *"tag this with X"* / *"passe le statut de X à closed"*, *"tag ça avec X"* |
| `/obsidian-router:write-frontmatter-merge` | Apply multiple frontmatter updates in sequence | *"on X set status=closed outcome=tp1"* / *"sur X mets status=closed outcome=tp1"* |
| `/obsidian-router:write-bundle` | Journaled multi-file bundle â all-or-nothing apply with rollback | *"write these 4 pages atomically"* / *"Ă©cris ces pages d'un bloc"* |
#### `manage/` (2)
| Command | Effect | Trigger phrasings |
|---|---|---|
| `/obsidian-router:manage-move` | Move or rename a file (GET â PUT â DELETE); optional `ifMatch` guards the source | *"rename X to Y"*, *"move X into <folder>"* / *"renomme X en Y"*, *"dĂ©place X dans <dossier>"* |
| `/obsidian-router:manage-delete` | Delete a file (two-step confirm guard; optional `ifMatch` refuses if it changed since read) | *"delete X"* (preview), *"yes confirm=true"* (proceed) / *"supprime X"* puis *"oui confirm=true"* |
#### `template/` (1)
| Command | Effect | Trigger phrasings |
|---|---|---|
| `/obsidian-router:template-execute` | Execute a Templater template (preview or save) | *"render Templates/X.md with arg1=v1"*, *"run the daily template"* / *"rends Templates/X.md avec arg1=v1"*, *"exécute le template daily"* |
#### `convert/` (2)
| Command | Effect | Trigger phrasings |
|---|---|---|
| `/obsidian-router:pdf-to-markdown` | Convert a local PDF to markdown via the bundled MarkItDown CLI (fast, plain-text extraction) | *"convert this PDF to markdown"*, *"markdown of X.pdf"* / *"convertis ce PDF en markdown"*, *"markdown de X.pdf"* |
| `/obsidian-router:pdf-to-markdown-docling` | High-fidelity PDF â markdown via Docling (layout + table-structure recognition, ~10Ă slower â needs the opt-in Docling install) | *"convert this PDF with docling"*, *"high-fidelity conversion of X.pdf"* / *"convertis ce PDF avec docling"*, *"conversion haute fidĂ©litĂ© de X.pdf"* |
### đ 4 router-state commands (lock + auto-enrichment + port base)
| Command | Effect | Trigger phrasings |
|---|---|---|
| `/obsidian-router:lock` | Restrict the router to a single vault for the session (volatile or `--persist` to write to `.env`) | *"lock to tradingview"*, *"I only want to work on tradingview"*, *"isolate to tradingview permanently"* / *"verrouille sur tradingview"*, *"je ne veux travailler que sur tradingview"*, *"verrouille sur tradingview de maniĂšre permanente"* |
| `/obsidian-router:unlock` | Lift the lock and restore multi-vault routing (`--persist` to also clean `.env`) | *"unlock vaults"*, *"give me back access to all vaults"* / *"déverrouille les vaults"*, *"je veux pouvoir avoir accÚs à tous les vaults"* |
| `/obsidian-router:auto-mode` | Set the wiki auto-enrichment mode (`ClaudeAsk` / `Hybrid` / `FullAuto` / `off`); `--persist` writes to `.env`, except `FullAuto` â see below | *"switch to Hybrid mode"*, *"save everything automatically"* (â FullAuto), *"stop auto-saving"* (â off) / *"passe en mode Hybrid"*, *"sauve tout automatiquement"*, *"arrĂȘte de sauver auto"* |
| `/obsidian-router:force-new-port-start` | Draw a new allocation base for **future** vaults only â sealed two-phase plan, zero existing ports touched, `installId` untouched | *"draw a new port base"*, *"my new vaults keep colliding with something"* / *"tire une nouvelle base de ports"*, *"les nouveaux vaults tombent sur des ports dĂ©jĂ pris"* |
See [Lock mode (single-vault isolation)](#lock-mode-single-vault-isolation), the auto-enrichment callout below, and [Vault identity and port ownership](#vault-identity-and-port-ownership) for the full designs and concrete use cases.
### đ 2 workspace-binding commands (which vault this project writes to)
| Command | Effect | Trigger phrasings |
|---|---|---|
| `/obsidian-router:bind-workspace` | Deterministic wizard: bind this workspace to a **primary** vault (detects the open vaults, asks, confirms, binds), then optionally to **secondary** vaults with a write tier each. Also names a proposal the project's dotenv file made that nobody answered â and a "no" is recorded as a refusal | *"which vault is this project attached to"*, *"bind this workspace to a vault"* / *"Ă quel vault ce projet est-il rattachĂ©"*, *"rattache ce workspace Ă un vault"* |
| `/obsidian-router:configure-secondary-vaults` | Same wizard, entered at its **secondary vaults** step: declare the secondaries and pick each one's write tier â read-only strict, read-only with writes on request, or read-write | *"add a secondary vault"*, *"make X read-only for this project"* / *"ajoute un vault secondaire"*, *"mets X en lecture seule pour ce projet"* |
Both write to **your own router config**, for this workspace only â never into the repository. See [Which vaults a workspace may reach, and which it may write](#which-vaults-a-workspace-may-reach-and-which-it-may-write).
### đ©ș 7 conversational helpers
| Command | Effect | Trigger phrasings |
|---|---|---|
| `/obsidian-router:meta-setup` | Walk through the MANUAL install (clone, npm link, register MCP) â dev path; normal installs get the server from the plugin | *"install the router"*, *"bootstrap obsidian-mcp-router on this machine"* / *"installe le router"*, *"setup obsidian-mcp-router sur cette machine"* |
| `/obsidian-router:meta-attach-vault` | Interactive wizard to attach a vault to a workspace (default), bootstrap a standalone vault, or register a remote vault. Provisions plugins + scaffolds wiki + binds `.env` + edits `.gitignore` + conventions picker. | *"set up Obsidian for this project"*, *"attach a vault to this workspace"*, *"connect my remote vault"* / *"configure Obsidian pour ce projet"*, *"attache un vault Ă ce workspace"*, *"connecte mon vault distant"* |
| `/obsidian-router:meta-status` | Health-check every vault with per-issue fix hints | *"diagnose the router"*, *"are my vaults reachable"* / *"diagnostique le router"*, *"mes vaults sont-ils accessibles"* |
| `/obsidian-router:meta-sync-template` | Propagate the reference vault's plugins/snippets/docs to one or more vaults (interactive picker) | *"sync the template to all vaults"*, *"push reference plugins to X"* / *"synchronise le template vers tous les vaults"*, *"pousse les plugins de référence vers X"* |
| `/obsidian-router:sync-from-github` | Update one vault or the whole fleet directly from the GitHub skeleton (plugins, themes, snippets, docs) â no local dev repo needed. Same guards as `--sync-plugins` plus hardened archive extraction | *"sync my vaults from github"*, *"update the fleet from github"* / *"synchronise mes vaults depuis github"*, *"mets Ă jour la flotte depuis github"* |
| `/obsidian-router:meta-audit-bridge-readiness` | Audit click-to-open readiness across vaults (bridge â„0.2.0, REST API â„4.0.0, insecure HTTP, live `/open` probe) | *"audit bridge readiness"*, *"is click-to-open ready"* / *"audite la disponibilitĂ© du bridge"*, *"le click-to-open est-il prĂȘt"* |
| `/obsidian-router:conventions` | Install / remove / status / propagate CLAUDE.md conventions (source-type, languages, heading-hierarchy, ...) across vaults | *"install source-type convention on X"*, *"list conventions"* / *"installe la convention source-type sur X"*, *"liste les conventions"* |
### đ 24 knowledge-management commands (Karpathy-style LLM-wiki)
A small workflow on top of the router for an LLM-maintained, structured markdown knowledge base where pages reference each other and grow with use.
| Command | Effect | Trigger phrasings |
|---|---|---|
| `/obsidian-router:wiki` | Scaffold `wiki/` inside a vault (index, log, hot, overview + CLAUDE.md update) | *"set up a wiki"*, *"scaffold a knowledge base"* / *"scaffold un wiki"*, *"crée une base de connaissances"* |
| `/obsidian-router:wiki-ingest` | Ingest a source (URL/file/text) â entity & concept pages + cross-refs | *"ingest this URL"*, *"absorb this article"* / *"ingĂšre cette URL"*, *"absorbe cet article"* |
| `/obsidian-router:wiki-query` | Three-tier RAG (hot.md â catalog.md â drill into pages), wiki-only (no web) | *"based on my notes, ..."*, *"what does my wiki say about X"* / *"d'aprĂšs mes notes, ..."*, *"que dit mon wiki sur X"* |
| `/obsidian-router:wiki-lint` | Health check (orphans, dead wikilinks, index drift, frontmatter gaps) | *"lint the wiki"*, *"audit my wiki"* / *"lint le wiki"*, *"audit mon wiki"* |
| `/obsidian-router:wiki-fold` | Idempotent rollup of log entries under `wiki/folds/` | *"fold the log"*, *"roll up recent activity"* / *"compacte le journal"*, *"résume l'activité wiki de cette semaine"* |
| `/obsidian-router:hot-compact` | Compact an oversized `wiki-meta/hot.md` back to its cache contract (verified full backup â thin state-first rewrite â log trace) | *"compact the hot cache"*, *"hot.md is over limit"* / *"compacte le hot"*, *"hot.md dĂ©passe la limite"* |
| `/obsidian-router:save` | File the current conversation as a typed wiki note (session/answer/decision/ADR/...) | *"save this"*, *"file this conversation"* / *"sauvegarde ça"*, *"archive cette conversation"* |
| `/obsidian-router:decision-consolidate` | Compress a settled decision page to its essentials and move the full deliberation history to a verified archive note | *"consolidate this decision"*, *"archive the deliberation of X"* / *"consolide cette décision"*, *"archive la délibération de X"* |
| `/obsidian-router:autoresearch` | Autonomous webâsynthâfile loop bounded by a research program | *"research X on the web"*, *"go investigate X online"* / *"fais une recherche web sur X"*, *"investigue X en ligne"* |
| `/obsidian-router:canvas` | Create/edit Obsidian `.canvas` files (visual layer for wiki pages, images, PDFs) | *"create a canvas for X"*, *"add to my canvas"* / *"crée un canvas pour X"*, *"ajoute à mon canvas"* |
| `/obsidian-router:defuddle` | Strip noise from webpages (ads, nav, footers) before ingestion | *"defuddle <url>"*, *"clean this page"* / *"nettoie cette page"*, *"extrais la version lisible de <url>"* |
| `/obsidian-router:obsidian-bases` | Create/edit Obsidian `.base` files (database-like views over frontmatter) | *"create a base for X"*, *"task tracker base"* / *"crée une base pour X"*, *"base task tracker"* |
| `/obsidian-router:wiki-graph` | Build a typed knowledge-graph JSON from the vault (Understand-Anything schema; feeds the native graph viewer) | *"build the wiki graph"*, *"generate the knowledge graph"* / *"construis le graphe du wiki"*, *"génÚre le knowledge graph"* |
| `/obsidian-router:wiki-tour` | Generate an ordered pedagogical reading tour from the vault's link topology | *"give me a tour of this vault"*, *"where do I start"* / *"fais-moi un tour du vault"*, *"par oĂč je commence"* |
| `/obsidian-router:wiki-neighbors` | Show one page's neighbours from the knowledge graph â what it links to, what links to it (backlinks), or both | *"what links to X"*, *"show me the backlinks of X"* / *"quelles pages sont liĂ©es Ă X"*, *"voisins de X"* |
| `/obsidian-router:wiki-path` | Find the shortest chain of links between two pages ("how are A and B connected?") | *"how is X connected to Y"*, *"path between X and Y"* / *"quel rapport entre X et Y"*, *"chemin entre X et Y"* |
| `/obsidian-router:wiki-export` | Export the vault as a portable single file (`llms.txt` / `llms-full.txt`) or as an **OKF knowledge bundle** (Google's Open Knowledge Format v0.1, shareable with any OKF-aware agent) | *"export the wiki as llms.txt"*, *"export as an OKF bundle"* / *"exporte le wiki en llms.txt"*, *"exporte en bundle OKF"* |
| `/obsidian-router:okf-export` | Export a wiki subset as a shareable **OKF v0.1 knowledge bundle** â slugified filenames, relative links, per-folder indexes, conformance self-checked, optional agent README | *"export this folder as an OKF bundle"*, *"publish my wiki as a knowledge bundle"* / *"exporte ce dossier en bundle OKF"*, *"publie mon wiki en bundle"* |
| `/obsidian-router:okf-projections` | Regenerate the **generated OKF navigation** inside `wiki/` â root `index.md` (`okf_version` only), one `index.md` per directory, newest-first `log.md`; auto-refreshed ~15 s after each write once initialised; `--check` = drift report | *"refresh the OKF projections"*, *"rebuild the wiki indexes"* / *"rafraĂźchis les projections OKF"*, *"regĂ©nĂšre les index du wiki"* |
| `/obsidian-router:okf-check` | Validate an OKF bundle (ours or third-party) against the Open Knowledge Format v0.1 conformance rules â one of the ecosystem's first OKF validators | *"validate this OKF bundle"*, *"is this bundle conformant?"* / *"valide ce bundle OKF"*, *"ce bundle est-il conforme ?"* |
| `/obsidian-router:build-search-index` | Build/refresh the local BM25 search index â a plugin-free search tier that works on every vault, idempotent | *"build the search index"* / *"construis l'index de recherche"* |
| `/obsidian-router:wiki-boundary` | Rank heavily-linked-but-thin "frontier" pages â the ones worth writing next | *"what should I write next"* / *"pages frontiĂšre du wiki"* |
| `/obsidian-router:wiki-refresh-digests` | Regenerate the per-page digest sidecars (concepts/claims/keywords) used by `wiki-lint --deep` and the graph | *"refresh the digests"*, *"rebuild page digests"* / *"rafraßchis les digests"*, *"régénÚre les digests de page"* |
| `/obsidian-router:who-is-speaking` | Identify the current family member in a shared vault and lock routing per-member | *"who is speaking"*, *"it's Karine"* / *"qui parle"*, *"c'est Karine"* |
Plus one Obsidian-specific reference skill (no slash command â knowledge surfaced when other skills run): `obsidian-markdown` (Obsidian Flavored Markdown reference for wikilinks, embeds, callouts, properties, etc.). Note that `obsidian-bases` is BOTH a reference skill AND has its own slash command above â other skills consult it when they need to generate `.base` files, and you can also invoke it directly.
**Two parallel sub-agents** for batch work:
- `wiki-ingest` agent â fan out one source per agent, parallel
- `wiki-lint` agent â read-only diagnostic in a separate context
**Hooks** â **11 cross-platform Node hooks**. The split: installing the plugin activates exactly three of them (`hot-cache-load` + `decisions-recall` + `workspace-briefing`, declared in `hooks/hooks.json`); the other eight fire only when wired via `setup-vault.mjs` â vault bootstrap auto-wires them into `~/.claude/settings.json` (opt out with `--no-hooks`), or run `node scripts/setup-vault.mjs --install-hooks` standalone. See [Which hooks the plugin turns on by itself](#which-hooks-the-plugin-turns-on-by-itself):
- `session-auto-journal` â auto-journals each Claude session under `wiki-meta/Sessions/` + a 2-line recap to `wiki-meta/journal.md` (self-healing reconciliation)
- `hot-cache-load` â loads `wiki-meta/hot.md` into context at SessionStart / PostCompact
- `hot-cache-update-prompt` â deterministic guard: **blocks the turn** (exit 2) until `wiki-meta/hot.md` is refreshed when this session wrote a `wiki/` note (per-vault, transcript-scoped; opt-out `OBSIDIAN_ROUTER_NO_HOT_CACHE_GUARD`)
- `wiki-autocommit` â auto-commits `wiki/`, `wiki-meta/`, `.raw/`, `.vault-meta/` to git after writes
- `wiki-query-first-nudge` â nudges Claude to check the vault before answering (+ injects PATH RESOLUTION RULES)
- `decisions-recall` â surfaces the **already-settled decisions** touching the prompt, so an option ruled out months ago isn't re-proposed. Deterministic and model-free: settled status (`accepted`, plus the legacy synonyms the linter still tolerates) then token overlap, with peripheral matches on vault-wide vocabulary demoted so a word like "router" can't surface everything. Silent when nothing matches; bounded by a wall-clock budget so a vault on a virtual drive can't stall a prompt. A `review_after:` that has passed â or that can't be parsed â is shown as *to re-evaluate*, never as a constraint. Injected as cited data, never as instructions (opt-out `OBSIDIAN_ROUTER_NO_DECISIONS_RECALL`)
- `vault-link-linter` â catches broken/phantom vault links before they reach you
- `doc-propagation-checker` â flags docs drifting from shipped code
- `vault-doc-startup-check` â surfaces vault & doc health at session start
- `check-router-update` â 24h GitHub version check
- `workspace-briefing` â opens each session with a few lines saying which vault(s) this workspace is bound to (one, several, or all), what its `.env` proposed and was refused, the auto-enrichment mode and its range, and the two calls that change any of it. It also reports, from the bound vaults' own disk (so it works with Obsidian closed), when Smart Connections is installed-but-not-enabled or enabled-with-an-empty-index â the two states in which `search_smart` can only answer from its BM25 fallback and `find_twin_pages` cannot answer at all (opt-out `OBSIDIAN_ROUTER_NO_SEMANTIC_READINESS`, from the host only). Read-only, pings nothing (opt-out `OBSIDIAN_ROUTER_NO_BINDING_BRIEFING`, **from the host only** â a project file may not switch off the report about itself)
The hooks ship in [`hooks/`](./hooks/); `setup-vault.mjs` wires them automatically at bootstrap.
**Auto-enrichment** â Claude proactively suggests wiki saves at three natural moments: **validation** (you say "OK" / "valide" â inline pin), **result obtained** (commit pushed, tests green â digest of candidates), and **topic switch** (mandatory checkpoint before Claude responds to the new topic). Domain-agnostic: works for development, personal life, research, family planning, anything.
**Four modes** (`/obsidian-router:auto-mode <Mode>` to switch, `--persist` to write to `.env` â with one exception: since v0.89.0 `FullAuto` is neither written to a workspace `.env` nor read back from one, because that mode is standing permission to write into a vault without asking and the `.env` a cloned repository carries must not grant it; it still comes from the MCP host's server declaration or from a call during the session, and `--persist` applies it to the session and says so. Stated honestly, this closes the `.env` door only: a repository's `CLAUDE.md` can still *ask Claude* to call `set_auto_enrich_mode`, and the router cannot tell that call from yours â so the `auto-mode` skill tells Claude to set `FullAuto` on your request in the conversation, never on a workspace file's instruction):
| Mode | Behavior | Best for |
|---|---|---|
| `ClaudeAsk` (default) | Propose, always confirm | Discovering the feature · long mixed-importance sessions · vaults where false positives would hurt · the calibration period (1-2 weeks) before trusting auto-save |
| `Hybrid` | Auto-save type-safe items (facts, URLs, preferences); ask on high-stakes (decisions, ADRs, rules, techniques) | Power-user sweet spot after calibration · active dev with frequent URL ingestion · research where citations pile up but conclusions need vetting |
| `FullAuto` | Auto-save everything; audit log in `wiki-meta/journal.md` + sensitivity filter (never auto-save credentials/medical/financial) + hard cap (degrades to `ClaudeAsk` after 5 saves/session) | High-trust sessions · personal journal / family chronicle · long unsupervised flows (autoresearch, batch ingestion) · solo brain-dumps where the wiki IS the conversation log |
| `off` | No auto-suggestions; manual `/save` only | Debugging sessions you don't want polluting the wiki · sensitive conversations · default for legal/medical/financial vaults · control-freak preference |
**Placement** â the consigne ships in the vault `CLAUDE.md` template, but is also configurable as Claude Desktop **Project instructions** (elegant pattern: a "Trading Journal" project always saves to `tradingview`, a "Personal" project to `personal`). See [`docs/auto-enrichment.md`](./docs/auto-enrichment.md) for the five placement channels (vault CLAUDE.md, Project instructions, Memory, global CLAUDE.md, bound-repo CLAUDE.md), the binding that gates them all, the activation rules, and concrete copy-paste boilerplates per channel.
Install steps are in the [Install](#install) section below.
## Skill capability contracts (`contracts/skill-capabilities.json`)
Every shipped skill has a machine-readable declaration of what it **reads**, what it **writes**, and what it **requires** â a shell? the network? a third-party Obsidian plugin? The file is `contracts/skill-capabilities.json`, one entry per skill, closed vocabularies throughout so a policy engine can consume it without parsing prose. It exists so that a deployment can answer "what does granting this skill actually allow?" before granting it, and so that doc, manifest and code cannot drift apart unnoticed.
```bash
npm run validate
```
The validator (`scripts/validate-capabilities.mjs`, also asserted by `npm test` and run as its own CI step) fails when the three tellings disagree:
| Leg | What it is |
|---|---|
| **Code** | the router's MCP tool catalog (`TOOLS` in `src/index.mjs`) and the sub-agent tool allowlists (`agents/*.md` frontmatter) â the only two things enforced at runtime |
| **Doc** | each `SKILL.md`, plus the artifact counters published in `README.md` and `docs/architecture.md` |
| **Manifest** | `contracts/skill-capabilities.json` and `.claude-plugin/{plugin,marketplace}.json` |
What it catches: a skill that ships undeclared · a declaration whose skill was deleted or renamed · a published counter that no longer matches reality · a declared tool that is not in the catalog · a tool a `SKILL.md` names that the contract does not account for · a sub-agent allowlist granting more than its own skill's contract · a `writeMode` that contradicts the declared writes.
**The honesty rule.** A capability with no behavioral verifier must *say so*, never quietly promote itself. Each entry carries a `verification` block with exactly two possible states, and the validator refuses either one it cannot substantiate:
- `verified` â requires `evidence` naming test files that **exist** and that **mention the skill**. Citing an unrelated suite is rejected.
- `declared` â requires a written `reason` naming the specific residual uncertainty.
**All 49 skills are `declared` today**, and that is not a backlog item: a skill is markdown interpreted by a model, and no harness executes one deterministically, so there is nothing a behavioral verifier could hook onto. There is deliberately no middle tier â "enforced by the sub-agent allowlist" was considered and rejected, because the allowlist only binds the batch path while the ordinary in-process path is bound by nothing.
**Bootstrapping.** `npm run capabilities:bootstrap` derives a proposal from the code (which tools each `SKILL.md` names, what those tools imply). It previews by default and writes nothing; `--missing-only --write` adds entries for new skills without touching reviewed ones. Every generated entry is stamped `UNREVIEWED-BOOTSTRAP`, **which the validator rejects** â so a generated file cannot go green until a human has read the page and replaced the reason. That mechanism is the point: the seeding pass is a proposal, and on the first run it was wrong often enough to prove it (it read the pure-reader `read-get` as `destructive`, `autoresearch` as offline, and `defuddle`'s prose-only `filter_relevant_blocks` mention as a call).
*Scope note:* the counter check watches an explicit allowlist of **current-state** sentences. Historical documents (`docs/announcements.md`, `docs/v0.10.2-skills-promotion.md`, `ROADMAP.md`, `CHANGELOG.md`) record what a past version shipped and are deliberately excluded â a blanket scan would demand rewriting the past to make the present pass.
## Working from another agent host (`AGENTS.md`, `npm run install:agent-rules`)
The MCP tools are universal â any client that speaks MCP can call them. The **know-how** was not: how to run an ingestion, which disciplines apply, which traps have already been paid for, all of that lived only in Claude Code's skill format. An agent arriving through Codex or Gemini had the commands without the manual.
**[`AGENTS.md`](./AGENTS.md)** at the repository root is the host-neutral half of the answer: the operating contract in plain markdown, read natively by Codex, Gemini CLI, Cursor and Windsurf. It is treated as code, not documentation, because it is an input to third-party models that act on it â every path it names is resolved against the filesystem and every command against `package.json` by `tests/agents-md-contract.test.mjs`, which fails the suite when one goes stale. A wrong line in a README costs a reader ten seconds; a wrong line here is executed by every agent, on every host, in every session.
**`npm run install:agent-rules`** is the other half: it puts an **index of skills** â name, one sentence, path to the `SKILL.md` â into the rule file each host actually reads.
> **It installs an index, not the skills.** Nothing here makes Codex or Cursor *execute* a `SKILL.md`. What travels is a catalogue of pointers plus the rule that says to read the pointed-at page in full before acting. That distinction is the whole honest description of the feature: the manuals stay where they are, and the foreign host is told they exist and where. Calling it "installing the skills" would promise an execution semantics no line of this code provides.
```bash
npm run install:agent-rules # status / preview of every target (writes nothing)
npm run install:agent-rules -- --host codex --apply
npm run install:agent-rules -- --skills wiki-ingest,wiki-lint --apply
npm run install:agent-rules -- --uninstall --apply
```
**Preview is also the status command.** There is no separate `--status`: run it with no flags and it reports, per target, the file, whether a managed block is there, and whether it is current (`installed` · `already-installed` · `upgraded` · `ambiguous-state` · `over-budget`). "What is installed on my machine?" and "what would this do?" are the same question asked of the same code, so they get the same answer rather than two implementations that can disagree.
**Future work â a native Agent Skills adapter.** The index is a bridge, not the destination. Hosts are converging on a real skills directory, and an adapter that emits conforming skill folders would give actual progressive disclosure instead of a pointer list. The empirical hook is already on disk: **codex-cli 0.146.0 scans `%USERPROFILE%\.agents\skills\` at startup**. Measured on this machine: that directory exists and holds 9 top-level entries (7 active, 2 `_disabled_*`) containing **35 `SKILL.md` files, of which 15 have no YAML frontmatter** â which is exactly why codex logs parse errors for them at launch. Emitting the router's 49 skills into that tree is the natural next step, and it is the reason `npm run audit:skills-portability` exists now rather than later: an adapter can only emit what already conforms.
Seven targets across five host entries, all declared in `contracts/agent-host-targets.json` rather than hardcoded, each carrying the **provenance** of its path so the preview can say which location was confirmed and which is taken on a vendor's word. Same HTML-comment markers as `--install-global-convention`, and the same refusal: markers that do not form exactly one well-formed block are reported as `ambiguous-state` and left alone, because an installer that guesses where a half-deleted block ended eats the paragraph after it.
Re-runs are no-ops. `--uninstall` returns the file to its original bytes **when the block is where an install put it â at the end**; if you have moved the block and text now follows it, head and tail are rejoined verbatim and the separator blank line may remain. That distinction is stated because an uninstaller that normalises newlines across the whole file, or collapses blank lines inside fenced code blocks, is making exactly the kind of unrequested edit an uninstaller must never make.
**Uninstall removes the block, never the file.** If the installer created the file itself, uninstalling leaves it behind â empty for `AGENTS.md` / `GEMINI.md` / the Windsurf rules file, or holding just its Cursor frontmatter for the `.mdc`. This is deliberate: the tool keeps no receipt of what it authored, and deleting a file it cannot prove it created is not a call it should make. Remove the leftovers by hand if you want them gone. The preview says so before you apply.
Two details that fell out of reading the hosts' own limits rather than assuming them. Windsurf caps global rules at 6,000 characters, which the full index does not fit â so the renderer has a **compact** mode, and a target that cannot fit even that is **refused** rather than truncated (the skills past the cut would look like skills that do not exist). And mutations are made **atomically** (temp file in the same directory, then rename), with a **timestamped `.bak-skills-index-*` sidecar** written before any upgrade or removal, and the exact text of a removal shown **verbatim** before it happens â the same discipline the `conventions` skill imposes on its own `remove`.
**About `.codex/config.toml`** â gitignored, holding a live token, once shipped inside a released bundle. Every target path is built by joining a contract base with a contract filename, so no path comes from user input; the resolved extension *and* basename are re-checked before any open; a target that is itself a symlink is refused; and `--project` / `CODEX_HOME` are rejected when they resolve to a filesystem root or a system directory. Stated precisely, because the wider version would be false: **no code path here can name a file the contract does not name**. It is not a sandbox â the symlink check covers the final path component only, so a reparse point on a parent directory is not caught, and the check-then-write window is not closed.
**`npm run audit:skills-portability`** measures the frontmatter side. Per the [Agent Skills specification](https://agentskills.io/specification) the format admits exactly six keys (`allowed-tools`, `compatibility`, `description`, `license`, `metadata`, `name`); Claude Code accepts about twenty and ignores the rest, while spec distribution paths reject the whole file on the first unknown key. So an extra key costs nothing until it costs everything.
The limit that matters is **`description`: max 1024 characters**, quoted from that spec and pinned in `contracts/agent-host-targets.json` with its access date. This is not the 1,536-character figure in the Claude Code docs â that one is where Claude Code *truncates its skill listing*, a host display budget, not a validity rule. Pinning the looser number is what let the audit report a clean run over 49 skills while 3 of them were in fact invalid; **those three descriptions have been shortened**, with the displaced text moved into the skill bodies where progressive disclosure wants it anyway.
Measured on this repository: **44/49 skills carry spec-only frontmatter**, longest description **1010/1024**. The other 5 use `argument-hint`, declared in the contract as an accepted Claude Code extension with its reason. Undeclared keys are errors, declared ones are warnings, and `-- --strict` collapses the two to show the spec-distribution view. Scope is printed with every run: this measures **frontmatter portability only** â whether a page's metadata can be *read* elsewhere, not whether its workflow would *execute* there, which is what `contracts/skill-capabilities.json` records.
## Reclaiming the plugin cache (`npm run purge:plugin-cache`)
Every plugin update copies a new version into `~/.claude/plugins/cache/<marketplace>/<plugin>/<version>/` and removes nothing. Measured on 2026-08-02: **eight versions, ~1.2 GB**, of which ~900 MB was dead.
```bash
npm run purge:plugin-cache
```
Preview by default â it prints what it would remove, how much that frees, and a seal; nothing is deleted until you pass that seal back with `--confirm <seal>`. The apply re-derives the plan from the *current* state and aborts on any drift, so a snapshot that went live in between stops the whole operation instead of being deleted under that session. Every update also computes this plan and returns it, but never applies it: that path is a silent `SessionStart` hook, and deleting ~800 MB unannounced is not something this repo does. `OBSIDIAN_ROUTER_AUTO_PURGE_CACHE=1` opts in.
**Never removed**: the current version · anything `installed_plugins.json` or `~/.claude/settings.json` names (including a `scope: project` entry from another workspace) · the **N-1 rollback** snapshot · the snapshot this process is running from · any snapshot a running process is serving from.
That last one is the reason the whole thing is careful. A session started before an update stays pinned to its snapshot until `/reload-plugins`, and the manifest has already moved on â so a purge keyed on the manifest alone deletes a directory out from under a live MCP server. Not hypothetical: while this was written, one node process was serving a snapshot the manifest no longer named.
**What the liveness check does *not* promise.** It is a best-effort process scan, not a lock. A process reaching its snapshot by a route the scan cannot see (an 8.3 short path, a mapped drive, a truncated command line) is missed, and there is an unavoidable race between the scan and the delete â the seal narrows that window but does not close it. So the honest claim is *"nothing a manifest names, never the rollback, and no snapshot this scan can see in use"*, not *"never a running snapshot"*. If the scan cannot run at all, nothing is purged and the reason is printed.
## The three pieces and how they depend on each other
Three components, two repos, one dependency chain. Reading bottom-up â each layer talks to the one above it:
```
Obsidian â Local REST API (community plugin) â BRIDGE (mcp-router-bridge)
â HTTP per vault (port + apiKey from the Local REST API plugin)
MCP SERVER (obsidian-mcp-router) â Node process on the PC
â MCP over stdio, spawned by Claude Code
CLAUDE CODE PLUGIN (obsidian-router) â commands + skills + agents + hooks,
and it SHIPS AND LAUNCHES the server itself
```
- **The bridge** runs *inside Obsidian*. It requires Obsidian plus the Local REST API plugin: it registers extra routes on Local REST API's HTTP server (`/search/smart`, `/templates/execute`, `/open/*`, presence heartbeat). The server's `search_smart`, `execute_template` and click-to-open links depend on it. **Without it**, the server's core file CRUD still works (plain Local REST API routes) â smart search, Templater execution and clickable links do not. It updates itself via BRAT from GitHub releases.
- **The MCP server** runs on the PC, spawned by Claude Code â via the plugin (the normal case), or via a manual `~/.claude.json` entry on dev setups. It requires Node â„ 20.19.0, the Local REST API plugin in each vault (mandatory), the bridge in each vault (optional â needed for smart search / Templater / click-to-open), and its registry at `~/.claude/obsidian-mcp-router/config.json` (maintained by `setup-vault.mjs`). The Claude Code plugin's commands and skills orchestrate its MCP tools.
- **The Claude Code plugin** runs in Claude Code and **ships the server** (one install = everything; one update = everything). Its skills and commands drive the server's tools; two hooks (`hot-cache-load`, `decisions-recall`) read vault files directly from disk, no server involved. The tool-name prefix depends on how the server was registered â see [Tool names depend on how the server was registered](#tool-names-depend-on-how-the-server-was-registered).
## Prerequisites
| Plugin (per vault) | Required for | Where to get it |
|---|---|---|
| **Local REST API** | All tools | Community plugins â "Local REST API" by Adam Coddington |
| **MCP Router Bridge** | `search_smart`, `execute_template`, click-to-open links (`build_open_link`, `open_in_obsidian`, the auto-emitted `clickToOpenUrl`) | Install from [`tboome33/obsidian-mcp-router-bridge`](https://github.com/tboome33/obsidian-mcp-router-bridge) â registers the `/search/smart`, `/templates/execute` and `/open/*` REST routes that this router calls (`meta-audit-bridge-readiness` probes the latter). |
| **Smart Connections** | `search_smart` | Community plugins â "Smart Connections" â the embeddings backend |
| **Smart Lookup** | *nothing â human-facing only* | Community plugins â "Smart Lookup". Since Smart Connections 4.7 the search half ships as its own plugin: it answers a typed query, where Smart Connections answers "what resembles the note I have open". Both read the same `.smart-env` index, so `search_smart` needs **Smart Connections alone** â this row is here because the reference vault distributes Smart Lookup too, and its absence is not a router fault. |
| **Templater** | `execute_template` | Community plugins â "Templater" by SilentVoid13 |
You also need:
- **Node.js â„ 20.19.0** (`undici@7` requires 20.18.1; the extra patch is Node's `--permission` flag, renamed from `--experimental-permission` in 20.19.0, which the test suite uses to prove no tool needs the vault's disk)
- **On Windows**, the two asset writers (`pptx_extract_assets`, `download_page_assets`) use **koffi** â an FFI installed with the other npm dependencies â to ask Windows where the output directory they hold really is. Without it they refuse to write rather than write unpinned; they also refuse a volume that does not answer NTFS (a FAT32 or cloud-drive letter, for instance).
- At least one vault provisioned in `~/.claude/obsidian-mcp-router/config.json`. If you've never set this up, run `npm run setup-vault -- "<vault-path>"` from a clone of this repo, or invoke [`scripts/setup-vault.mjs`](./scripts/setup-vault.mjs) directly â it'll bootstrap the config interactively. Schema reference: [`examples/config.example.json`](./examples/config.example.json).
- A **reference vault** registered with the router. It holds the canonical plugin set + config that `setup-vault.mjs` clones into every new vault. Fast path: `node scripts/setup-vault.mjs --bootstrap-reference <path>` scaffolds it from the shipped skeleton ([`templates/reference-vault-skeleton/`](./templates/reference-vault-skeleton/)) and auto-downloads the bridge plugin. Full procedure (manual + troubleshooting): [`docs/reference-vault-setup.md`](./docs/reference-vault-setup.md).
> đ§ **Guided vault-creation wizard.** Creating a new vault is defaults-first: the engine computes a complete default plan, shows it in one line, and you accept it as-is (happy path = 1 interaction) or adjust any point (name · location · template source · plugins · theme · wiki mode). It works from **any LLM harness** via the `plan_vault` (read-only) + `provision_vault` MCP tools â not just the CLI. In Claude Code: the [`meta-attach-vault`](./skills/meta-attach-vault/SKILL.md) skill. From any other agent (Codex, Hermes, a raw MCP client): the [`docs/vault-wizard.md`](./docs/vault-wizard.md) playbook. Directly: `node scripts/setup-vault.mjs "<vault-path>" --dry-run --json` to preview, then without `--dry-run` to apply (`--help` lists all wizard flags). The two tools are LOCAL-ONLY (hidden on gated deployments); `provision_vault` refuses paths outside known vault roots; `--from-vault` copies config only (secrets always regenerated).
> đ **The vault already exists? Don't run the wizard â attach it.** One idempotent command, from the workspace directory:
>
> ```bash
> obsidian-mcp-router --attach <vault-slug> [--also <other-slug>]...
> ```
>
> It provisions nothing (every slug must already be registered) and does the four workspace-side writes: the `.env` binding, `.claude/settings.json` to **enable the router plugin â without it the `.env` is inert and no hook runs**, a `CLAUDE.md` block naming the vaults, and `.gitignore`. Flags: `--workspace <path>` (defaults to the cwd), `--no-plugin` / `--no-claude-md` / `--no-gitignore`. It lives on the binary rather than in the plugin on purpose: it is the command you need *before* the router has any presence in the workspace, and the plugin is enabled by one of the writes it performs. **Multi-vault**: the router binds ONE vault per workspace â `--also` vaults are documented in the generated block and addressed explicitly with `vault: "<slug>"`, never auto-loaded. Then restart Claude Code in that workspace.
> **CSS snippets are cloned automatically.** Every `setup-vault.mjs` invocation also copies `<referenceVault>/.obsidian/snippets/*.css` into the target vault and merges the basenames into `<target>/.obsidian/appearance.json` `enabledCssSnippets`. The shipped skeleton ships `no-task-strikethrough.css` (kills Obsidian's default `text-decoration: line-through` on `- [x]` items, aligned with the [`roadmap-discipline`](./skills/conventions/snippets/roadmap-discipline.md) §2bis convention). Opt-out per vault in Settings â Appearance â CSS snippets. To push a snippet (or plugin) update to ALL configured vaults at once: `node scripts/setup-vault.mjs --sync-all` (idempotent; add `--force` to re-clone existing files).
## Install
> đ **Reference vault required for `setup-vault.mjs`** â to bootstrap new vaults via the script (which most users will want), you first need a one-time-configured reference vault holding the canonical plugin set. Easiest path: `node scripts/setup-vault.mjs --bootstrap-reference <path>` (scaffolds the skeleton + downloads bridge plugin in one command, then guides you through installing the marketplace plugins via Obsidian). Full doc with troubleshooting: [`docs/reference-vault-setup.md`](./docs/reference-vault-setup.md).
**The plugin carries the MCP server.** Installing the plugin gets you the server, the slash commands, the skills and the hooks together; updating the plugin updates all of them at once. Go to Step 2 and skip Step 1 â it is only for people who want to run the server from a checkout.
### Step 1 â Install the MCP server *(optional â the plugin already ships it)*
Only needed if you are developing on the router, or if you deliberately want the server registered independently of the plugin.
```bash
git clone https://github.com/tboome33/obsidian-mcp-router.git
cd obsidian-mcp-router
npm install
npm link # makes the `obsidian-mcp-router` binary available globally
```
Register it in `~/.claude.json` (user scope) as `obsidian-router`:
```json
{
"mcpServers": {
"obsidian-router": {
"type": "stdio",
"command": "obsidian-mcp-router"
}
}
}
```
The router reads `~/.claude/obsidian-mcp-router/config.json` on start (the same file that `setup-vault.mjs` maintains) and exposes every vault automatically.
> â ïž **Do not do both without meaning to.** A hand-registered server and the plugin-provided one are two different commands, so Claude Code does not treat them as duplicates: you get **two server processes and two copies of every tool**. Pick one. To move a hand-registered install onto the plugin, remove your `obsidian-router` entry from `~/.claude.json`.
### Step 2 â Install the plugin
**Register the marketplace globally** in `~/.claude/settings.json`:
```json
{
"extraKnownMarketplaces": {
"obsidian-mcp-router-marketplace": {
"source": {
"source": "github",
"repo": "tboome33/obsidian-mcp-router"
}
}
}
}
```
**Then enable the plugin per-workspace**, NOT globally. The plugin loads 54 slash commands and 49 skills (~10k context tokens per session) â you only want that overhead on workspaces that actually use Obsidian. For each vault directory and each app workspace that consumes the router, drop a `.claude/settings.json` file at the workspace root:
```json
{
"enabledPlugins": {
"obsidian-router@obsidian-mcp-router-marketplace": true
}
}
```
For vaults bootstrapped via `setup-vault.mjs`, this file is **cloned automatically** from `.template/.claude/settings.json` â you don't have to write it by hand. For non-vault workspaces (dev repos that work with vault content), copy the snippet above into `<workspace>/.claude/settings.json`.
Restart Claude Code. From a workspace with the plugin enabled, type `/obsidian-router:` â the 54 slash commands should appear. From a workspace without, the namespace stays clean.
> **Why not enable it globally?** If you put `enabledPlugins` in `~/.claude/settings.json` instead of per-workspace, the plugin loads in EVERY Claude Code session â random scripts, debug sessions, unrelated repos â paying ~10k tokens for commands those sessions will never use. Project-scope keeps the budget tight.
> **Bump the skill-listing budget (recommended).** The router contributes 49 skills to Claude Code's skill listing. On a default install (`skillListingBudgetFraction: 0.01`, i.e. 1% of the context window), this often pushes the listing past the budget â descriptions are truncated, and natural-language triggering for `/save`, `/wiki`, `/autoresearch` etc. silently breaks. **Recommended**: raise to `0.05` in `~/.claude/settings.json` (~6k extra tokens per session). The diagnostic message *"Skill listing will be truncated â N descriptions dropped"* at session start is the symptom this fixes.
>
> ```json
> { "skillListingBudgetFraction": 0.05 }
> ```
>
> The bundled `meta-setup` skill detects an under-budgeted setup and offers to apply this change interactively.
A normal install is Step 2 alone. If you're taking the dev path (Step 1 â clone + `npm link` + `~/.claude.json` entry), the bundled `meta-setup` skill can walk you through it interactively: ask Claude *"set up the obsidian-mcp-router on this machine"*.
### Tool names depend on how the server was registered
The server always declares bare tool names (`get_file`, `write_file`, âŠ). The prefix comes from the registration, so the same tool has different full names:
| How the server is registered | Full tool name |
| --- | --- |
| Provided by the plugin (the default) | `mcp__plugin_obsidian-router_router__get_file` |
| Registered by hand in `~/.claude.json` | `mcp__obsidian-router__get_file` |
| Behind MCPHub | `mcp__<id>__obsidian-router-<vault>-get_file` |
Documentation and skills use the short `mcp__obsidian-router__*` form for readability â Claude calls whichever name is actually in its tool list. Hooks match these tools **by suffix** (`hooks/_helpers/tool-names.mjs`) precisely so they keep firing under all three forms.
**When two prefixes are present at once, prefer the plugin's.** A Claude Code session inside the desktop app sees both its own plugin server and any server the desktop app itself declares, and they do not answer the same way about *this* workspace: an MCP server is started with its launcher's working directory, and only the plugin's is started in the workspace. A server declared in the desktop app's own config starts in the app's directory, so it belongs to no workspace â `workspaceBinding` is `null` and the default vault is the config-wide one. That answer is correct for a server with no workspace, and wrong for the project you are sitting in. Measured 2026-09-04.
### Which vault is this project attached to?
Every session opens by telling you, in a few lines â that is the
`workspace-briefing` hook. There are **three** states, not two:
| State | What it means |
| --- | --- |
| **one vault** | This directory is bound to it. It is the session default, and `list_vaults` shows `workspaceBinding` with an empty `also`. |
| **several** | A primary plus secondaries (`also`), all bound and addressable by name. Only the primary is the default. |
| **all** | No binding (`workspaceBinding: null`). With `vaultReach` unset, registered vaults are available; with `vaultReach: "declared"`, only `openVaults` remain reachable, possibly none. The cascade chooses among reachable vaults. |
**Where the binding lives, and why there.** In *your* `config.json`, under
`workspaceBindings`, keyed by the directory's canonical path. That file is
never synchronised between machines â it holds your vault paths and API keys â
so one machine's decision never binds another's, and nothing in a repository
can put an entry there.
**What the project's `.env` is for now.** It is a *portable hint*. A workspace
is very often a cloned repository, and until this release the
`OBSIDIAN_ROUTER_DEFAULT_VAULT` line it carried decided which of your vaults
the session read, locked and wrote into â a file you may never have written,
choosing where a year of notes go. It is now reported and not applied:
`list_vaults` carries it as `bindingHint`, and the briefing names it. Its
usefulness is unchanged for the case it was good at â arriving on your *second*
machine and proposing the right answer there, once.
**Changing it**, from a conversation or a terminal:
```bash
node scripts/setup-vault.mjs --attach <vault> --also <other>
```
or ask Claude, which calls `confirm_workspace_binding`: `{ vault }` to bind,
`{ vault, also: [...] }` for several, `{ locked: true }` to restrict the
session to it, `{ clear: true }` to remove the binding. Reachability still follows `vaultReach`: when `"declared"`, only `openVaults` remain reachable (possibly none); when unset, registered vaults are available. A bound vault whose
Obsidian is not running is opened for you â a closed vault does not answer, so
a binding to one would be a promise that does not work.
**Upgrading from an earlier version.** The first time the router starts in a
workspace that already had a hint, it imports it as a binding â once, and it
says so at the top of every session until you either adopt it
(`confirm_workspace_binding({ vault })`) or undo it (`{ clear: true }`, which
sticks). A `OBSIDIAN_ROUTER_LOCKED` line an earlier `lock_vault --persist`
wrote is carried across too, as `locked: true` on the imported binding, so an
isolation you had set up does not quietly disappear on upgrade.
The import is bounded by the dotenv file's own modification time against the
moment you upgraded, so a repository you **clone** after upgrading is never
imported: `git clone` writes its files now, and that is what separates a
workspace you attached last year from one that arrived this morning. Two limits
worth knowing, because a timestamp is the only signal the disk carries:
unpacking an **archive** (`tar x`, an unzip that restores timestamps, GitHub's
source zipball, `rsync -a`) keeps the recorded mtime, so a project obtained
that way *can* be imported; and on a router whose very first start ever is on
this version there is no "moment you upgraded" to compare against, so anything
already on disk counts as older. Both cases are announced by the session
briefing like any other import, which is what makes them cost one sentence to
undo rather than a year of misfiled notes.
One more thing to know on that path. If you run the router from a checkout
rather than the plugin and wired your hooks before this version, re-run
`node scripts/setup-vault.mjs --install-hooks` once: the import runs inside the
router for everyone, but the briefing that announces it is a hook your older
`settings.json` does not carry.
### Saying no to a proposal
A hint you do not want used to have no exit â you adopted it or you endured it,
re-announced at every session, because nothing could record that the question
had been answered. It can now be **refused**, and the refusal is written on two
sides that do different jobs:
| Where | What it is | Effect |
| --- | --- | --- |
| your `config.json`, under `workspaceRefusals` | the **authority** | silence: the hint reads `refused`, the briefing says nothing, the one-time import never binds that vault |
| the project's `.env`, as `OBSIDIAN_ROUTER_REFUSED_VAULT` | a **portable hint** | silences nobody; it survives uninstalling the router, and after a reinstall the question is asked *once* more, with that context |
```
confirm_workspace_binding({ refuse: "<vault>" }) # record the no
confirm_workspace_binding({ retract: "<vault>" }) # take it back
```
A refusal names **one vault**, so a `.env` that later proposes a different one
is still reported. Binding a vault drops its refusal â binding is adopting. The
vault a workspace is bound to cannot be refused at all: the binding is already
the opposite answer. And the `.env` half is written **only into the file that
proposed the vault**, by its `OBSIDIAN_ROUTER_DEFAULT_VAULT` or its
`OBSIDIAN_ROUTER_LOCKED` line â a proposal that came from your shell or your MCP
host leaves the project file untouched. So a repository that gets committed with
a refusal in it can, at worst, make a colleague be asked a question once.
On a multi-tenant deployment (`OBSIDIAN_ROUTER_READONLY`,
`OBSIDIAN_ROUTER_ALLOWED_VAULTS` or `OBSIDIAN_ROUTER_USER_ID` set) none of the
binding tools are available: the workspace there is the server's own directory,
shared by every caller, so one answer would stand for all of them.
### Which vaults a workspace may reach, and which it may write
Two mechanisms, and they do **not** have the same default. Reachability is
opt-in and changes nothing until you set it. The write tier is **on**, and if
you already had secondaries it changes what they accept â see the upgrade note
at the end of this section.
**Reachability.** With `"vaultReach": "declared"` in `config.json`, a registered
vault answers a session only if that workspace's binding names it (as `vault` or
in `also`), or if it is listed in **`openVaults`** â the exception list that
keeps a personal vault reachable from everywhere, including the Desktop chat,
which has no workspace at all. `list_vaults` keeps *showing* an unreachable
vault, in `disabled[]`, with the reason.
**Write tiers.** A workspace's secondaries (`also`) are read-only by default.
Three tiers, per workspace, so the same vault can be strict in one project and
read-write in another:
| Tier | What a write does |
| --- | --- |
| `locked` | refused while this tier is in force; write parameters cannot override it. Change a binding-local tier with `set_secondary_vault_mode`, or clear the binding before re-binding. Global `alsoLocked` entries require a config edit to lift the secondary restriction. |
| `soft` *(default)* | refused unless the call carries `confirmSecondaryWrite: true`, which Claude may set only after you have said yes |
| `writable` | goes through, no friction |
The primary is always read-write. Record a tier with
`set_secondary_vault_mode({ vault, mode })`, or let the **`/bind-workspace`**
wizard walk you through the whole thing â where we are, the primary, the
secondaries, one tier question per secondary, then a table of what it recorded.
> **Upgrading, and you already had secondaries.** This is one of the two
> defaults this release reverses (the other is a vault two workspaces declare,
> which stops accepting blind writes â see *Vaults two workspaces share*
> below): before this release a vault in `also` accepted writes like any
> other; from now on it is `soft`, and a write that does not carry
> `confirmSecondaryWrite: true` is refused â with no `vaultReach` set and no
> tier list in your config. Two ways back, per vault:
> `set_secondary_vault_mode({ vault, mode: "writable" })` for this workspace,
> or the `alsoWritable` list in `config.json` for every workspace at once.
> Reads are unaffected, and the refusal names both remedies, so nothing is lost
> if you meet it before reading this.
### Vaults two workspaces share
When more than one workspace declares the same vault, a blind write to it is
refused: the call must carry a **precondition**. `write_file` takes `ifMatch`
(the `contentSha256` a read returned) or `ifNew: true`; `patch_file`,
`append_to_file`, `set_frontmatter`, `merge_frontmatter`, `move_file` and
`delete_file` take `ifMatch` (`delete_file` also accepts the seal its
`preview: true` call returned); a `write_bundle` needs one **per step**
(`ifMatch`, or `ifNew: true`
on a write step), or the `approvedPlanSha256` a `preview: true` call returned â
`expect` is the precondition of a *recovery* run, not of an ordinary bundle;
`download_page_assets` and `pptx_extract_assets` take `createOnly`; `execute_template` is create-only at
the bridge. `list_vaults`
reports it per vault as `writesRequireIfMatch` and `sharingReason`, so you can
see which vaults are in that state rather than discovering it from a refusal.
> **Upgrading, and a vault of yours is already shared.** This is the second
> default this release reverses, and it needs no configuration change to reach
> you: the requirement is *computed* from your binding registry, so if two
> workspaces already declare one vault, a `write_file` that worked yesterday is
> refused today. A vault in `openVaults` counts as shared by hypothesis, since
> its readership cannot be known. Nothing is lost when you meet it â the
> refusal names what satisfies it â but a caller that never passed `ifMatch`
> now has to. If a vault is shared only by accident, the way back is to stop
> declaring it from the second workspace (`confirm_workspace_binding`); there
> is deliberately no switch to turn the requirement off, because a switch would
> read as "this vault is safe to overwrite blindly", which is the belief that
> loses a note.
### Remote vaults, from a conversation
`register_remote_vault({ name, baseUrl, apiKey })` adds a vault served over the
network to your own config without editing JSON by hand â the conversational
half of the `remoteVaults` block documented below. It is hidden on multi-tenant
deployments, where the config is shared. An optional absolute `localPath` says
the vault's files also sit on this machine (Obsidian in a container, say); the
tool stores it as declared, and `obsidian-mcp-router --attach <name> --local-path
<dir>` verifies it against the vault before recording it. Notes still travel over
REST only â see [`docs/remote-vaults.md`](docs/remote-vaults.md) for what the
field enables.
### Which hooks the plugin turns on by itself
Installing the plugin activates exactly three hooks, with no opt-in step, because Claude Code runs whatever a plugin declares in `hooks/hooks.json`:
| Hook | What it does | Turn it off with |
| --- | --- | --- |
| `hot-cache-load` | On session start, prints your vault's `wiki-meta/hot.md` into the session context. Read-only. | `OBSIDIAN_ROUTER_NO_HOT_CACHE_LOAD=1` |
| `decisions-recall` | On a prompt that matches a settled decision page, cites it. Read-only. | `OBSIDIAN_ROUTER_NO_DECISIONS_RECALL=1` |
| `workspace-briefing` | On session start, says which vault(s) this workspace is bound to and how to change it, and flags a bound vault whose Smart Connections is installed-but-disabled or has an empty index. Read-only, no network. | `OBSIDIAN_ROUTER_NO_BINDING_BRIEFING=1` â **from the host only**; `OBSIDIAN_ROUTER_NO_SEMANTIC_READINESS=1` for the semantic check alone |
All three are silent no-ops if no vault is configured. `workspace-briefing` ships here rather than opt-in on purpose: it is the disclosure that makes the binding registry visible, and a binding the router imported from a project's `.env` is only safe to import because it announces itself at the start of every session. Its opt-out is the one the workspace `.env` cannot set â a file that could silence the report about itself would be the hole this whole feature closes. **The other eight hooks stay opt-in** via `node scripts/setup-vault.mjs --install-hooks`, because they commit to git, write session transcripts into a vault, block the end of a turn, or call the network â none of which is a defensible default for someone who just installed a plugin. `--hooks-status` shows which are wired, which come from the plugin, and warns if any is doing both (which would fire it twice per event).
### Automatic vault maintenance (and its three knobs)
After a write, and again on first contact with a vault in a session, the router runs one **maintenance pass**: it regenerates the vault's OKF navigation projections, then rebuilds the local BM25 search index â both inside a single hold of that vault's lock, so two sessions can never interleave halfway through. The pass is debounced, so a burst of writes costs one pass rather than one per file, and it never runs against a vault that is read-only, unreachable, or below the write tier the workspace declared.
| Variable | Effect | Default |
| --- | --- | --- |
| `OBSIDIAN_ROUTER_NO_AUTO_CONFORMANCE` | Switches the whole maintenance pass off. Reads and writes still work; the projections and the index simply stop being refreshed for you â call `refresh_okf_projections` and `build_search_index` yourself when you want them | off (pass enabled) |
| `OBSIDIAN_ROUTER_NO_OKF_PROJECTIONS` | Keeps the pass but skips the OKF projection refresh, leaving the BM25 index rebuild | off (projections enabled) |
| `OBSIDIAN_ROUTER_PROJECTIONS_DEBOUNCE_MS` | How long the router waits after the last write before flushing. A positive integer; anything else falls back to the default | `15000` (15 s) |
The first two accept any of `true` / `1` / `yes` / `on`. Both are `OBSIDIAN_ROUTER_NO_*` opt-outs, so a workspace dotenv file may set them â they switch a convenience off, never a guard.
### Staying up to date
The router ships a SessionStart hook (`hooks/check-router-update.mjs`) that checks GitHub once per 24 hours and surfaces a notice if a newer version is available. The notice tells Claude to relay it on its first response of the session, so you find out without having to remember to check.
**It is opt-in, not plugin-activated** â wire it with `node scripts/setup-vault.mjs --install-hooks`. It stays out of `hooks/hooks.json` deliberately: it makes a network call, and a plugin should not phone home on install without being asked. If you skip it, `/plugin update` remains the normal way to upgrade.
If `/plugin update obsidian-router@obsidian-mcp-router-marketplace` is available in your Claude Code environment, that's the one-liner upgrade path. If it isn't (some environments don't expose the `/plugin` slash command), see [`docs/how-to-update.md`](./docs/how-to-update.md) for the 5-step manual filesystem equivalent (bash + PowerShell recipes).
Opt-out â set either of these env vars and the check is skipped:
- `OBSIDIAN_ROUTER_NO_UPDATE_CHECK=true` (any truthy value)
- `OBSIDIAN_ROUTER_USER_ID=<slug>` (multi-tenant deployments â assumes the sysadmin manages updates centrally)
The check is a single GET to `raw.githubusercontent.com`. No payload, no telemetry â source is [`hooks/check-router-update.mjs`](./hooks/check-router-update.mjs).
### CLI flags
```bash
obsidian-mcp-router --version
obsidian-mcp-router --help
obsidian-mcp-router --config /custom/path/config.json
obsidian-mcp-router --no-watch # disable hot-reload of the config file
obsidian-mcp-router --plugin-health <vault> [--json] # plugin code on disk vs. loaded by Obsidian
obsidian-mcp-router --install-plugins <vault> --dry-run # sealed plan; apply with --approved-plan-sha256 <seal>
```
Both plugin commands work on the vault's folder (a local vault, or a remote one that declares `localPath`); details in [feature sheet 13](docs/features/13-installation-et-administration.md) (French).
By default, the router watches the config file and reloads automatically when it changes â useful when paired with `setup-vault.mjs` adding new vaults, or with the future `Obsidian Cloudflare Tunnel` plugin auto-writing tunnel URLs into `remoteVaults`.
### Building your own macros on top (advanced)
The 54 plugin commands above are domain-agnostic on purpose â they work for any vault. If you want **macros** that chain multiple tools or bake in your vault's conventions (daily notes, capture inbox, weekly rollups, etc.), build them as your own slash commands in `~/.claude/commands/<name>.md` â not as PRs on this repo. The router stays neutral; the macros are yours.
See [`docs/building-commands.md`](./docs/building-commands.md) for the pattern and three illustrative starting-point examples.
### Disabling a vault temporarily
To hide a vault from `list_vaults` without removing it from the config, either:
```jsonc
{
// Global blacklist (works for both local and remote vaults, by name):
"disabledVaults": ["template", "experimental-vps"],
// Or per-remote-vault flag (only for entries in remoteVaults):
"remoteVaults": [
{ "name": "qnap", "baseUrl": "...", "apiKey": "...", "enabled": false }
]
}
```
Disabled vaults appear in the boot log as `(N disabled: ...)` for visibility, but they don't show up in `list_vaults` and aren't pingable.
### Default vault resolution
When a tool call omits the `vault` argument (e.g., `read-search "trading risk"`), the router has to pick one. **The same call can resolve to different vaults depending on which workspace you launch Claude from.**
Resolution cascade, highest priority first:
0. **The workspace's confirmed binding** â what *you* attached this directory to, recorded in your own `config.json` under `workspaceBindings` and keyed by the directory's canonical path. The only tier that cannot arrive with a `git clone`, which is why it outranks the environment. See [Which vault is this project attached to?](#which-vault-is-this-project-attached-to).
1. **`OBSIDIAN_ROUTER_DEFAULT_VAULT` env var** â explicit per-process override, **from the host only**: your MCP server declaration, a launcher, your shell. The same variable in a project's `.env` is a *proposal*: it is reported and never applied, because a workspace is very often a cloned repository and its `.env` came with it. Confirm it once and it becomes the binding above.
2. **`VAULT_PATH` env var** â auto-detection. If `VAULT_PATH` matches a path registered in your `portRegistry`, that vault becomes the default. From a project's `.env` this is honoured **only when it names that same directory** â the "this folder IS a vault" case, which is exactly what `setup-vault.mjs` writes into every bootstrapped vault's `.env`, so opening Claude Code in a vault directory still "just works". A project file pointing `VAULT_PATH` at some *other* vault of yours is a proposal like any other.
3. **`config.defaultVault`** â explicit global default in `~/.claude/obsidian-mcp-router/config.json`.
4. **First healthy local vault** â historical fallback.
5. **First active vault of any type** â last resort.
The router auto-loads `.env` from the cwd at startup, so steps 1 and 2 work without any other tooling â subject to the origin rule above. Existing env vars in the parent process win over `.env`, and the router records which of the two a value came from: that record is what tells a proposal from a decision, and `list_vaults` reports it as `bindingHint.origin`.
#### Three concrete cases
**Case 1 â your project IS a vault (the common case).**
```
cd C:\VAULTS\TradingView\
claude
```
`.env` (written by `setup-vault.mjs` when you bootstrapped the vault) contains:
```
VAULT_PATH=C:\VAULTS\TradingView
OBSIDIAN_API_KEY=...
OBSIDIAN_BASE_URL=https://127.0.0.1:27125
```
Auto-detection (step 2) matches `VAULT_PATH` against your `portRegistry` â default = `tradingview`. **No config needed.** Tools that omit `vault` operate on `tradingview`.
**Case 2 â your project is NOT a vault, but works with one.**
```
cd C:\Code\my-app\
claude
```
This isn't a vault directory, so `VAULT_PATH` isn't set. Without intervention, the router falls back to `config.defaultVault` (probably `tradingview`). If you want this project to default to a different vault â say `recherche` for note-taking â add to `C:\Code\my-app\.env`:
```
OBSIDIAN_ROUTER_DEFAULT_VAULT=recherche
```
Step 1 wins â default = `recherche` for this project only.
**Case 3 â your project IS a vault, but you want a different default.**
You opened Claude Code in `C:\VAULTS\.template\` because you're documenting it, but you want vault tool calls without explicit `vault=` to operate on `tradingview` instead of `template`. Add to `C:\VAULTS\.template\.env`:
```
OBSIDIAN_ROUTER_DEFAULT_VAULT=tradingview
```
Step 1 overrides the auto-detection of step 2.
#### Verifying which default the router picked
Call `list_vaults` â the result has a `defaultVault` field showing which name resolved.
```bash
# from any project, in Claude Code:
"list my vaults"
```
If the `defaultVault` is wrong for what you expected, check (in order): your project's `.env`, the parent process's env, and `~/.claude/obsidian-mcp-router/config.json`'s `defaultVault` field.
#### Override didn't take effect?
If you set `OBSIDIAN_ROUTER_DEFAULT_VAULT="something"` and the router can't find that name in the active set (typo, vault disabled, vault removed), the cascade falls through to step 2/3/4/5 AND emits a one-line warning to stderr:
```
[registry] OBSIDIAN_ROUTER_DEFAULT_VAULT="recherchee" does not match any active vault â falling through to other resolution tiers. Active vaults: template, tradingview.
```
### Lock mode (single-vault isolation)
By default the router is in **multi-vault mode**: any tool call can target any registered vault via the `vault` parameter, and `vault: "*"` fans out across all of them. This is the right default for power users who want one MCP entry to rule them all.
For situations where you want the **opposite** â one vault for the whole session, with the router refusing every cross-vault drift â use **lock mode**.
#### When lock mode is useful
- **Safety**: working on a sensitive vault (legal docs, client data) and you want a structural barrier against accidental writes elsewhere.
- **User routing on a shared install**: a single Claude Code installation shared between several people. Each user locks to their personal vault at session start; nobody's notes leak into anyone else's.
- **Focus**: long ingestion or autoresearch session on one wiki â lock prevents the assistant from "helpfully" filing anything in a sibling vault.
#### How to lock / unlock
Three ways to lock:
1. **MCP tool directly** (Claude calls it for you):
```
lock_vault({ vault: "tradingview" }) # volatile (this session)
lock_vault({ vault: "tradingview", persist: true }) # writes .env so it survives restart
```
2. **Slash command** (or natural language â auto-trigger):
- `/obsidian-router:lock tradingview` â volatile
- `/obsidian-router:lock tradingview --persist` â persistent
- Natural language: *"I only want to work on tradingview"*, *"lock to tradingview permanently"*
3. **Environment variable at startup**:
```
OBSIDIAN_ROUTER_LOCKED=tradingview
```
**from the host** â your MCP server declaration or your shell. The router reads it on boot. The same line in a project's `.env` no longer locks anything: locking a session to one vault is the strongest possible way of choosing where its writes land, so a file that travels with a clone may propose it and not impose it. What makes a lock survive a restart is `locked: true` on the workspace's binding, which `lock_vault({ persist: true })` writes for you.
To unlock:
- `unlock_vaults()` â in-memory only
- `unlock_vaults({ persist: true })` â lifts the lock on the binding (where a restart reads it from) and removes the `OBSIDIAN_ROUTER_LOCKED` hint from `<cwd>/.env`
- `/obsidian-router:unlock` or *"give me back access to all vaults"*
> **Caveat â persist refused at home directory.** `lock_vault({ persist: true })` refuses when the current working directory IS your home directory (`%USERPROFILE%` on Windows, `$HOME` elsewhere). That's almost always a mistake â Claude Code was launched from `~` rather than a project folder, and creating `~/.env` would surprise you. The in-memory lock still applies for the session. To make the lock survive a restart in this case: either re-run `lock_vault` from a real project directory, or set `OBSIDIAN_ROUTER_LOCKED=<vault>` in your shell profile (`~/.bashrc`, `~/.zshrc`, or PowerShell `$PROFILE`).
#### What happens while locked
| Operation | Behavior |
|---|---|
| Tool call with `vault: <locked-vault>` | â
proceeds normally |
| Tool call without explicit `vault` | â
resolves to the locked vault (overrides the default cascade) |
| Tool call with `vault: <other-vault>` | â throws `Router is locked to vault "<X>". Cannot operate on "<other>". Use unlock_vaults first or specify "<X>".` |
| Tool call with `vault: "*"` (cross-vault fan-out) | â throws `Cannot fan-out: router is locked to vault "<X>". Use unlock_vaults first or specify "<X>" instead of "*".` |
| `list_vaults` | â
always works. Response includes new field `lockedTo: "<X>"` so callers can render the lock state. |
#### Three concrete cases
**Case 1 â quick volatile lock during a session.**
You're about to ingest 30 articles into your `recherche` wiki and don't want any drift to other vaults:
> *"lock to recherche"*
Router locks. All wiki-ingest calls go to `recherche`. After the session ends or Claude Code restarts, the lock is gone (since you didn't persist).
**Case 2 â permanent lock for a shared install.**
You and other users share the same Claude Code install. Donald wants every Claude session he opens to default to (and stay locked on) the `donald` vault, no matter what `config.defaultVault` says.
In `~/.bashrc` / PowerShell profile, OR in the `.env` of his usual project:
```
OBSIDIAN_ROUTER_LOCKED=donald
```
Or, equivalently, run once:
> *"lock to donald and persist this"*
The slash command writes `OBSIDIAN_ROUTER_LOCKED=donald` to `<cwd>/.env`. From now on, opening Claude in this workspace, the router boots already locked. Other users (Mitch, Bernie...) on different workspaces have their own `.env` with their own lock value.
**Case 3 â switching the lock target.**
You're locked to `recherche`. You want to switch the lock to `tradingview`:
> *"lock to tradingview"*
`lock_vault` overrides the previous lock atomically. No need to unlock first.
#### Verifying the lock state
```
"list my vaults"
```
The response now contains `lockedTo`:
```jsonc
{
"defaultVault": "tradingview",
"lockedTo": "tradingview", // â non-null = locked
"vaults": [...],
"disabled": [...]
}
```
When `lockedTo` is `null`, the router is in normal multi-vault mode.
## `VAULT_*` env-var config (dashboard-editable)
Besides the `config.json` file below, a vault can be defined entirely in an **environment variable** â one per vault â so it's editable straight from the MCPHub server's *Environment Variables* UI (no SSH + file edit). This is a **3rd config source**, merged after `portRegistry` + `remoteVaults`; a `VAULT_*` entry **overrides** any same-name vault. It's **opt-in**: with no `VAULT_*` set, the router behaves exactly as before.
```
VAULT_<NAME> = <vault config as JSON>
```
Required: `name`, `baseUrl`, `apiKey` (the **bare token** â the router adds `Authorization: Bearer ` itself). Optional: `description`, `tlsInsecure`, `timeoutMs` (default `10000`). (There is no per-vault `wireguard` flag â WireGuard is enforced deployment-wide; see below. A leftover `wireguard` key is ignored.)
The three connection modes (all selected purely by `baseUrl`):
```bash
# 1. WireGuard tunnel (sensitive/medical â encrypted). Selected purely by the
# 10.8.0.x baseUrl; WG can be enforced deployment-wide (OBSIDIAN_ROUTER_ENFORCE_WG_OR_LOOPBACK).
VAULT_DEDIBOX={"name":"dedibox","baseUrl":"http://10.8.0.10:27161","apiKey":"<token>","timeoutMs":15000}
# 2. LAN / co-located (non-sensitive) â plain HTTP on the local network.
VAULT_NOTES={"name":"notes","baseUrl":"http://192.168.0.10:27124","apiKey":"<token>"}
# 3. Remote behind TLS (e.g. nginx + Let's Encrypt).
VAULT_REMOTE={"name":"remote","baseUrl":"https://vault.example.com","apiKey":"<token>","tlsInsecure":false}
```
Defensive parsing: a malformed entry is **skipped** with a clear stderr warning naming the faulty key (one bad var never crashes the others). On a JSON-parse failure neither the raw value nor the parser message is logged (both can echo the `apiKey`). The reserved `VAULT_PATH` env var is ignored by the scan.
**Ephemeral view links (optional view-agent provider)** â set `OBSIDIAN_ROUTER_VIEW_AGENT_URL` (plus an optional shared secret `OBSIDIAN_ROUTER_VIEW_AGENT_TOKEN`, sent as `X-View-Token`) to plug a *view-link provider* into the router. Every note write then carries a ready-to-click `viewLink` to the vault's **live Obsidian GUI navigated to that note** (deterministic server-side injection), the `get_view_link` tool appears (it is hidden from ListTools while the URL is unset, so unconfigured routers carry zero dead surface), and `open_in_obsidian` returns the link for remote-container vaults. The router depends only on a small HTTP contract â `GET /view?vault=<name>¬e=<path>` â `{"url": "<browser-ready link>"}` â not on any particular infrastructure: see the **reference provider implementation + the normative contract** at [obsidian-mcp-router-view-agent](https://github.com/tboome33/obsidian-mcp-router-view-agent) (config-driven, stdlib-only Python, ephemeral cloudflared quick tunnels). Each request also carries two optional *vault hints* so a provider can serve a vault nobody declared to it: `rest` (the vault's `baseUrl` reduced to `scheme://host:port` â no credentials, path or query; the API key is never read) and `obsidian_name` (the vault's label inside Obsidian: the folder name for a local vault, the optional `obsidianName` field for a remote one â see [docs/remote-vaults.md](docs/remote-vaults.md)).
**Smart links (optional resolver)** â set `OBSIDIAN_ROUTER_SMART_LINK_URL` (resolver base URL) **and** `OBSIDIAN_ROUTER_SMART_LINK_SECRET` (HMAC secret) to emit **stable signed smart links** instead of agent-fetched view links: note writes and `open_in_obsidian` on remote vaults then carry `viewLink = <resolver>/o/<signed-token>` with `viewLinkKind: "smart"` â a pure HMAC computation, **zero network call** (a write can never be slowed by a down agent), and the link stays valid in chat history (30-day token TTL). The link resolves **on the device that clicks it** (local Obsidian mirror probe â `obsidian://` deep link â streamed-GUI fallback). Provider priority when both are configured: smart link â view-agent â none; `get_view_link` keeps talking to the view-agent directly. Configuring smart links signals a **remote** deployment â do not set `OBSIDIAN_ROUTER_SMART_LINK_*` on a purely local router, or `open_in_obsidian` will hand back a link (`opened:false`, `delivered:"link"`) instead of navigating your local Obsidian. The resolver reference implementation + contracts live in the private saas repo (`obsidian-mcp-router-saas`).
**Deployment-wide transport guard** â set `OBSIDIAN_ROUTER_ENFORCE_WG_OR_LOOPBACK=true` (typically on a multi-tenant MCPHub instance) to make the router **refuse to start** if any served vault's `baseUrl` host is neither loopback (`127.0.0.1`/`::1`/`localhost`) nor inside the `10.8.0.0/24` WireGuard mesh. This is a **boot-time config check on the configured baseUrls** â it does *not* require the WireGuard tunnel to be up, and **loopback passes** (so it is not "WireGuard-only"). Fail-closed â a vault can never be silently served over an exposed link; the check runs after the `OBSIDIAN_ROUTER_ALLOWED_VAULTS` whitelist. Opt-in; unset = no enforcement (local mode unchanged). *(`OBSIDIAN_ROUTER_REQUIRE_WIREGUARD` is still accepted as a deprecated alias; prefer the current name â the old one wrongly implies "WG must be up".)*
### Generating a host deployment (`gen-obsidian-deploy`)
To run a vault as a `linuxserver/obsidian` (Selkies) container on a host (e.g. a server) â serving LiveSync, the Local REST API, and a browser-tab GUI from one plain-markdown `/config` â use the generator instead of hand-writing the JSON above:
```bash
node scripts/gen-obsidian-deploy.mjs --name tribu --rest-port 27145 --mode wg --wg-host 10.8.0.1
```
It prints a docker-compose service, an nginx reverse-proxy block (with a self-healing resolver-variable `proxy_pass`), and the `VAULT_*` line â the latter is **round-trip-tested** against this router's `parseEnvVaults`, so it can't drift. Modes: `wg` (WireGuard-only, for sensitive/medical), `lan`, `public` (HTTPS+bearer; refused for `--sensitive` vaults). Pass `--tls-insecure` to emit `tlsInsecure: true` (an `https` baseUrl behind a self-signed / internal-CA cert). Secrets default to placeholders â never invented. See [`deploy/dedibox-obsidian/`](./deploy/dedibox-obsidian/) for the full runbook (incl. LiveSync Setup-URI onboarding).
## Config
The router reads the existing config maintained by [`scripts/setup-vault.mjs`](./scripts/setup-vault.mjs), and adds three optional fields on top:
```jsonc
{
// --- written by setup-vault.mjs (don't edit by hand) ---
"referenceVault": "C:\\VAULTS\\.template",
"portStart": 27124,
"portRegistry": {
// Two ports per vault â see "Port bookkeeping" below.
// The legacy shape (a bare number) is still read.
"C:\\VAULTS\\.template": { "https": 27124, "http": 27134 },
"C:\\VAULTS\\TradingView": { "https": 27125, "http": 27135 }
},
// --- router-specific (optional, edit freely) ---
"vaultNames": {
"C:\\VAULTS\\.template": "template",
"C:\\VAULTS\\TradingView": "tradingview"
},
"remoteVaults": [
{
"name": "qnap",
"baseUrl": "https://192.168.0.11:27125",
"apiKey": "...",
"tlsInsecure": true
}
],
"defaultVault": "tradingview"
}
```
See [`examples/config.example.json`](./examples/config.example.json) for a complete example with comments, [`docs/vault-identity-and-ports.md`](./docs/vault-identity-and-ports.md) for how a vault's durable identity, its owner, and the band new ports are drawn from all fit together (and what the router deliberately does *not* promise about them), [`docs/remote-vaults.md`](./docs/remote-vaults.md) for the full guide on adding remote vaults, and [`docs/cloudflare-tunnel.md`](./docs/cloudflare-tunnel.md) for the recipe to expose a vault over a Cloudflare Tunnel with optional Cloudflare Access auth (service tokens supported via the `extraHeaders` field).
### Running the router without the vaults' disks
A router that only speaks REST â on a dev box, in a container, behind a hub â cannot read the vaults' files. Measured on 2026-08-31 across the 50 tools that existed then, in isolated processes: **the only universal disk dependency is credential resolution.** For a *local* vault (a `portRegistry` entry) the router reads the API key out of the vault's own `data.json` before any tool runs. Move that key into the config and the dependency disappears â no tool in the tested set needs vault disk any more.
`scripts/gen-remote-config.mjs` performs that move:
```bash
node scripts/gen-remote-config.mjs --vault roland --vault tribu
```
| Flag | What it does |
|---|---|
| `--vault <slug>` | Vault to export. **Repeatable, and required** â there is no implicit "whole fleet". |
| `--all` | The whole fleet, after announcing how many keys that is. |
| `--host <host>` | Default `127.0.0.1` â the remote end of the SSH tunnel. A non-loopback, non-WireGuard host is flagged, because the global `OBSIDIAN_ROUTER_ENFORCE_WG_OR_LOOPBACK` guard would refuse to start. |
| `--format json\|env` | A config file, or `VAULT_<NAME>=<json>` lines. |
| `--out <file>` | Write cleartext; the file is **created** at mode `0600`. |
| `--print-secrets` | Allow cleartext on stdout â for piping into a secret store. |
**The defaults are cautious on purpose, because a config carrying N keys grants read *and write* access to N vaults to every process that can read it.** On a machine that also runs code agents, that is a real privilege escalation. So: output is **redacted by default** (same shape, `<apiKey>` placeholders â reviewable, pasteable, committable); the selection is explicit; `--out` **refuses** to write inside the repository, inside any vault, or over a file with looser permissions; and no key is ever logged, truncated or quoted in an error message.
Keys are read **from disk**, never through the plugin's API â that same `data.json` also holds the vault's TLS private key, and only the one field ever leaves the file.
### Port bookkeeping â two ports per vault
Every vault runs **two** servers: the TLS REST API on `https`, and a plaintext HTTP server on `http` (its `insecurePort`) â the one the bridge's `/open/<path>` route answers on, and therefore the one every click-to-open link in your notes is pinned to.
A registry that records only the HTTPS port lets the allocator hand a brand-new vault a port **already bound by another vault's plaintext server**. That is not theoretical: nine such collisions were measured across a 27-vault fleet, one of them leaving a vault permanently unreachable (a TLS call landing on a plaintext listener returns `ERR_SSL_WRONG_VERSION_NUMBER`). The usual symptom is quieter and worse to diagnose â the second vault to start fails to bind and just looks *offline*, with no error anywhere.
So both ports are recorded, and both spaces are checked before either is handed out.
| Command | What it does |
|---|---|
| `node scripts/setup-vault.mjs --check-ports [--json]` | Read-only report: duplicate ports across both spaces, plus registry-vs-`data.json` drift. Exits `1` on a real collision, so a scheduled task can alert on it. |
| `node scripts/setup-vault.mjs --sync-port-registry [--dry-run]` | Records each vault's plaintext port in the registry, read from its own `data.json`. Takes a timestamped backup of `config.json` first. |
| `node scripts/setup-vault.mjs --status` | Prints **both** ports per vault, and flags collisions at the bottom. |
Three rules the implementation keeps, and that you should keep too if you edit `config.json` by hand:
- **An existing `insecurePort` is never renumbered.** Those numbers live in click-to-open links already written in your notes. When a conflict has to be resolved, the **HTTPS** port is the one that moves.
- **`http` is never guessed as `https + 10`.** That offset is the convention applied to *newly provisioned* vaults, not a property of the fleet â 15 of the 27 vaults measured on 2026-08-30 escape it. When a vault's `data.json` can't be read, its `http` is recorded as `null`, meaning *unknown*, and `--sync-port-registry` fills it in later.
- **Migration is non-destructive.** The legacy shape is still read, converting is idempotent, no key is dropped, no HTTPS port moves, and the pre-migration file is kept as `config.json.portRegistry-<timestamp>.bak`.
### Vault identity and port ownership
Since v0.94.0 every vault carries a durable UUID in `.obsidian/obsidian-mcp-router/identity.json` â it survives a rename, a move, and a change of ports. The file holds no API key, no absolute path and no port (those stay in `data.json`, the plugin's own source of truth; a copy here would be a second one, replicated by sync, free to drift). An installation carries a UUID too, drawn once from a cryptographic source and never recomputed once it exists. **Only a vault's owner may rewrite its ports** â UUIDs are compared, hostnames never are, because two machines can carry the same label and one machine can change its own.
New pairs for **future** vaults are drawn from a band (20000â32000, minus 27000â27999 where the historic fleet and the plugin's factory port live), both members bind-tested against the machine before being handed out. That is not a reserved range â two installations drawing independently can still collide â just fewer collisions between independent creations, on top of the reservations the registry already tracks.
| Command | What it does |
|---|---|
| `node scripts/setup-vault.mjs --migrate-vault-identities --dry-run` | Preview keying the registry by vault UUID instead of by path. Stamps every vault `owner: null` â claiming a vault is a separate, explicit act, never implicit over the whole fleet. Changes no port, no key, no `data.json` (the plan states the count as a literal: `0`). |
| `node scripts/setup-vault.mjs --migrate-vault-identities --approved-plan-sha256 <seal>` | Apply the previewed plan. Journalled and safe to interrupt â a resume reuses identities already created rather than minting a second UUID for a folder that has one. A duplicate UUID **blocks** rather than being guessed at: a replica, a stale move, and an independent copy look identical from the registry and call for opposite actions. |
| `node scripts/setup-vault.mjs --vault-owner "<path>" --show` | Show who owns a vault's ports, without changing anything. |
| `node scripts/setup-vault.mjs --vault-owner "<path>" --claim` | Claim an **unowned** vault for this installation. Writes one field of one file â no port changes, no key is minted. |
| `node scripts/setup-vault.mjs --vault-owner "<path>" --release --acknowledge-transfer` | Release a vault, or take it from another installation with the acknowledgement flag. Both the current and the new owner are shown before anything is written. |
| `/obsidian-router:force-new-port-start` (or `--force-new-port-start` on the CLI) | Draw a new allocation base for **future** vaults only. Sealed two-phase plan â `--dry-run` prints it and its seal, the apply passes both back â so the base written is the base that was shown, never redrawn, and a registry that moved in between makes the apply refuse. |
**What this deliberately does not do.** No existing port is ever renumbered by any of the above (that stays out of scope â the plaintext ports already in use are written into click-to-open links in mail and transcripts that nothing can rewrite). "Unknown owner" is a valid, refusing state, not an error â the whole historic fleet migrates into it, and claiming stays a per-vault decision for a person to make. A shared API key across two vaults is reported, never repaired automatically â a synchronised replica legitimately shares its source's key, and rotating it would lock the other machine out (`--check-ports` reports this fleet-wide now, with only a truncated fingerprint ever printed).
Full design â the seven decisions behind it, the migration's journal-and-resume mechanics, and the six rounds of adversarial review plus a 22-probe penetration test that shaped the final guards: [`docs/vault-identity-and-ports.md`](./docs/vault-identity-and-ports.md).
## Tools exposed
| Tool | Description |
|---|---|
| `list_vaults` | Catalogue of all configured vaults with online status + latency. Always call this first. |
| `list_files` | List files in a directory of a specific vault. |
| `get_file` | Read full file content (markdown + frontmatter). |
| `search` | Plain-text (substring) search. Pass `vault: "*"` to fan-out across all vaults. |
| `search_smart` | Semantic (meaning-based) search via Smart Connections embeddings. Returns ranked chunks with cosine scores and breadcrumbs. Requires `obsidian-mcp-router-bridge` + `smart-connections` plugins enabled in the target vault. Supports `vault: "*"` for cross-vault semantic search. On the semantic tier it also returns a **`freshness`** block naming the hits whose page has been modified since it was indexed (see below). |
| `write_file` | Create a new file or replace the entire content of an existing one. Pass `ifNew: true` to refuse to overwrite. |
| `append_to_file` | Append content at the end of a file. Auto-creates the file unless `requireExisting: true`. |
| `patch_file` | Surgical edit by `heading` / `block` / `frontmatter` target â insert under a heading without rewriting the whole file, replace a block by id, update a single frontmatter key. |
| `delete_file` | Permanently delete a file. Requires explicit `confirm: true` to guard against hallucinated deletes. |
| `execute_template` | Execute a Templater template, optionally writing the rendered result to a new file. Arguments are exposed in the template via `tp.mcpTools.prompt("key")`. |
| `move_file` | Move or rename a file. Implemented as GET source â PUT destination â DELETE source. Pass `overwrite: true` to replace an existing destination. |
| `get_frontmatter` | Read frontmatter (whole object or one key). Returns parsed values â numbers, booleans, arrays preserved. |
| `set_frontmatter` | Set/replace one frontmatter property. Type preserved (string/number/bool/null/array/object). |
| `merge_frontmatter` | Apply multiple frontmatter updates in sequence (non-atomic â see ROADMAP for atomic alternative). |
| `lock_vault` / `unlock_vaults` | Restrict the router to a single vault for the session (single-vault isolation). See the **Lock mode** section. |
| `set_auto_enrich_mode` | Switch the wiki auto-enrichment mode between `ClaudeAsk` / `Hybrid` / `FullAuto` / `off`. |
| `confirm_workspace_binding` | Bind this directory to a vault, in your own config: `{ vault }`, `{ vault, also: [...] }` for secondaries, `{ locked: true }` to restrict the session, `{ clear: true }` to go back to every vault, `{ refuse }` / `{ retract }` to answer a proposal a project file made. Unavailable on gated deployments. |
| `set_secondary_vault_mode` | Record the write tier of one SECONDARY of this workspace â `locked`, `soft` or `writable`. Per workspace, so the same vault can be strict in one project and read-write in another. Unavailable on gated deployments. |
| `register_remote_vault` | Add a vault served over the network (`{ name, baseUrl, apiKey }`) to your own config, without editing JSON by hand. Local-only (absent on gated deployments). |
| `get_view_link` | Build a signed, expiring link that opens a vault page in the read-only view agent. |
| `plan_vault` | **Read-only.** Plan the creation of a NEW local vault: returns computed defaults + a structured questionnaire (the 5 wiki modes, themes installed in the source, registered vaults to copy config from, plugin profiles) + warnings â without writing anything. Feeds the guided wizard; chain with `provision_vault`. Local-only (absent on gated deployments). |
| `provision_vault` | Create a NEW local vault in one call from the wizard answers (typically `plan_vault` defaults + adjustments). Returns a step-by-step report + port, insecurePort, openUri and probe result. Refuses paths outside the known vault roots (judged on the real path, links resolved) unless `allowOutsideRoots: true`; the new vault's directory is pinned for the whole run, must be the one the gate approved, and is refused when its existing tree holds a link, junction or hard link (or a `.git` when `gitInit` is asked) (Windows: NTFS only); `--from-vault` copies config only (credentials excluded, port + API key regenerated). Local-only. |
| `pdf_to_markdown` · `docx_to_markdown` · `xlsx_to_markdown` · `pptx_to_markdown` · `image_to_markdown` · `audio_to_markdown` | Convert a local file to markdown via the bundled `markitdown` Python CLI. Image OCR and audio transcription require the `[all]` extras (opt-in: `npm run install-markitdown`). Returns markdown text only â chain with `write_file` to persist. |
| `pdf_to_markdown_docling` | Convert a local PDF to markdown via **Docling**'s standard pipeline (layout detection + TableFormer table-structure recognition). Higher fidelity than `pdf_to_markdown` on complex tables / multi-column layouts, at ~10Ă the CPU cost. **Opt-in** â requires the Docling extra (see *Conversion tools â runtime dependencies*). PDF only; for office formats keep `pdf_to_markdown`. |
| `pdf_to_images` | **Render** a local PDF's pages to PNG images, returned as MCP image blocks so the model can visually **see** a page (not just read its text). Renders with **pypdfium2** (BSD) + Pillow from the same `.venv-docling` as Docling â returns an actionable install hint if absent. Params: `filepath`, `first_page`, `max_pages` (default 8, cap 30), `scale` (â144 DPI). Hard page/byte caps bound token cost. Does not write to any vault. |
| `pptx_extract_assets` | Extract a PPTX's **embedded images** to files on disk, each mapped to the slide(s) using it. The **complement** of `pptx_to_markdown`, which returns text, tables and notes but only an alt-text reference for pictures â so a deck ingested with that alone leaves dangling image links. Needs **no Python**: a PPTX is a ZIP, read with the router's own reader. Writes to a temp directory unless `outdir` is given; `wiki-ingest` aims `outdir` at the vault's `wiki/.assets/<slug>/` folder, so it is a **write tool** â hidden by `OBSIDIAN_ROUTER_READONLY`, and an `outdir` inside a vault meets that vault's reachability, write tier and shared-vault precondition (`createOnly: true`) exactly as `download_page_assets` does; an `outdir` outside every registered vault and outside the system temp directory is refused. The output directory is pinned before the first write (Windows, Linux) and no file is placed by opening its name, so a link swapped or planted in the output tree cannot redirect a write. Returns a manifest (`name`, `path`, `slides`, `bytes`, `ext`, `sha256`) to chain into `write_file`. An image reused across slides is written **once** and lists them all â unless `skipped` names a part the tool did not read, which may have been one more slide of it. Output names are constructed and the extension comes from magic bytes, never from the archive. Images living only in a slide layout or master are not extracted. |
| `youtube_to_markdown` · `bing_search_to_markdown` · `webpage_to_markdown` | Convert a remote URL to markdown via `markitdown`. URL must be http(s); private/loopback hosts are refused (SSRF guard). For JS-heavy SPAs prefer the `defuddle` skill (headless browser). `webpage_to_markdown` additionally accepts an opt-in `relevanceQuery` to BM25-filter the result to on-topic blocks (see `filter_relevant_blocks`) â output stays a string with a one-line stats comment appended. |
| `git_repo_to_markdown` | Bundle a git repository (file tree + source code) into a single markdown document via `repomix`. Accepts a full URL or the `owner/repo` shorthand. Pass `compress: true` for ~70% size reduction via Tree-sitter. |
| `extract_page_metadata` | Deterministic page-metadata extractor (JSON-LD + OpenGraph + meta tags + title) â feeds non-fabricated frontmatter for ingestion. |
| `propose_linked_sources` | Heuristic-scored `<a href>` follower that proposes recursive-ingestion candidates (top-N, same-domain / related-section boosts). |
| `download_page_assets` | Download a page's images into the vault (image preservation during web ingestion). `outputDir` must be inside a registered vault (whose gates apply) or the system temp directory; it is pinned before the first write, like `pptx_extract_assets`'s. |
| `build_open_link` | Build a ready-to-paste click-to-open markdown link (`http://127.0.0.1:<insecurePort>/open/<path>`) for one or many vault files. Read-only. |
| `open_in_obsidian` | Open a note in the running Obsidian (and raise its window) by calling the bridge `/open` route **server-side** â no browser. The browser-free counterpart to a click-to-open link, for clients (e.g. Claude Desktop) that otherwise proxy clicked links through a browser. Optional `anchor` scrolls to a heading. Navigation-only. |
| `get_wiki_context_pack` | Return a structured JSON context envelope for a query (primaryPages / semanticChunks / graphNeighbors / citations) so non-Claude agents can consume the vault programmatically. |
| `build_wiki_graph` | Assemble the vault into a typed knowledge-graph JSON (Understand-Anything schema: 21 node / 35 edge types). Writes `wiki-meta/graph/knowledge-graph.json` + a derived `.understand-anything/` copy. |
| `build_wiki_tour` | Generate a deterministic, ordered pedagogical reading tour from the knowledge-graph link topology. Read-only. |
| `get_page_neighbors` | Return the neighbours of ONE page from the knowledge graph â the pages it links to (`forward`), the pages that link to it (`backward`), or both â out to `depth` hops. Defaults to pageâpage links; widen `nodeTypes` to surface the concepts/sources a page also touches. An ambiguous page name is refused with the list of candidates. Two optional structural enrichments (`includeSameFolder`, `includeSharedTags`) surface non-linked siblings â same directory, or a shared real tag â at zero extra cost. Read-only. |
| `wiki_path` | Find the shortest chain of links between TWO pages ("how are A and B connected?"). Undirected traversal; returns the ordered list of pages hop by hop, or an explicit null path when they are not connected (not an error). Widen `nodeTypes` (e.g. `["article","entity","topic"]`) for "connected via a shared concept" paths. Read-only. |
| `find_boundary_pages` | Rank the wiki's "frontier" pages â the crossroads many pages link to that stay thin inside â from the persisted graph. Score = inbound links damped by length (`inbound / (1 + words/100)`: full weight on an empty page, halved at 100 words, a tenth at 900), Ă1 to Ă2 for staleness; same graph â same ranking (recency is measured against the graph's own build stamp, not a clock). Pages typed `redirect`/`source`/`answer` are held out by default and the count held out is reported. The score PROPOSES ATTENTION, it does not establish importance â index and hub pages legitimately surface near the top. Refuses on a graph built before the feature rather than scoring every page as empty. Read-only. |
| `find_twin_pages` | Find QUASI-TWIN pages â pairs so close in meaning the vault has probably written one subject twice, splitting its links and updates between two half-complete pages. Compares the per-page vectors Smart Connections already stores on disk (`.smart-env/multi/`) by cosine, every page against every other. THE THRESHOLD IS DERIVED FROM THE VAULT'S OWN DISTRIBUTION and reported with the answer â a fixed cosine cut does not transfer (measured: 0.95 selects 93 pairs on one vault, 398 on another). Stale index entries, generated `index.md`/`log.md` projections and `redirect`/`source`/`answer` pages are held out, each with its count. A pair PROPOSES A READING, never a merge; every row carries the evidence (same folder, same basename, shared links, already linked) needed to dismiss it. Without embeddings the answer is `available: false` with a reason AND NO `pairs` key â deliberately NOT the same answer as `found: 0`. Works on remote vaults too (their bridge must be â„ 0.9.0, which serves the vector store over `GET /smart-env/sources`; an older bridge answers `bridge-route-absent`). Read-only. |
| `filter_relevant_blocks` | BM25 relevance second-pass over markdown you ALREADY have (no fetch, no LLM, deterministic). Drops blocks unrelated to a `query` topic â an ingestion knows *why* it fetched a page, so it can strip intros/bios/digressions before synthesis. Frontmatter and headings always kept; a code block follows the relevance of the prose that introduces it. Safety nets: empty query â strict no-op; <4 scorable blocks â untouched; would drop >70% â returns the original intact. Reuses the router's own tokeniser + IDF. Read-only. Borrowed from [Crawl4AI](https://github.com/unclecode/crawl4ai) (W-A). |
See [ROADMAP.md](./ROADMAP.md) for what's next.
### Conversion tools â runtime dependencies
The `*_to_markdown` family is a JS/ESM port of [zcaceres/markdownify-mcp](https://github.com/zcaceres/markdownify-mcp) (MIT) â see `NOTICE` for the full credit. The actual file â markdown conversion is performed by Microsoft's `markitdown` Python CLI:
- **Python 3.10+** is required, and the install is **explicit** â run `npm run install-markitdown`. The script auto-detects Python on `PATH`, creates a local `.venv` at the repo root, and installs `markitdown[all]>=0.1.5`. If Python is missing it prints a warning and exits cleanly; the rest of the router works either way.
- There is deliberately no npm `postinstall`: the plugin carries the server, so a `postinstall` would mean every third party who installs the plugin silently building a ~100 MB Python virtualenv they never asked for â rebuilt on every plugin update, since each plugin version lives in its own directory. The conversion tools are opt-in; everything else works without Python.
- **You can find out before a tool call fails.** Every `list_vaults` response carries a `conversionToolbox` block â `available`, `via` (`bundled-venv` / `env-override` / `path`), `path`, `verified`, `optedOut`, `toolsAffected`, `toolsDegraded`, and a `hint` naming the command for *this* install â except where the install path contains characters a shell would reinterpret, in which case the hint deliberately falls back to generic wording rather than emit something unsafe to paste. `verified: false` means the answer was taken on your word rather than measured (a bare command name that `execFile` resolves through `PATH` at call time, or a UNC path that is unsafe to stat on this hot path) â read it as "configured", not "ready". `meta-status` renders it as one line. It runs **no subprocess** â but "no subprocess" is not the same as free: it stats a bounded slice of `PATH` synchronously, so a `PATH` entry on a **disconnected mapped drive or dead network mount** can make that call wait for an OS timeout. UNC entries are skipped; a dead `Z:\` looks like a local path and cannot be. The scan is also capped (64 KB of `PATH`, 128 entries), so on a pathological `PATH` it under-reports rather than over-promises. It is a *stat*, not a trial run, so a green tick is not a guarantee either: a POSIX file whose execute bit belongs to another user, or a Windows `.exe` that is not a valid image, still fails at spawn. The authoritative answer is always the conversion call itself.
- `OBSIDIAN_ROUTER_SKIP_MARKITDOWN=1` makes the install script a no-op **and** silences that hint, for scripted environments and for anyone who has already answered the question.
- To use a system-wide install instead of the bundled venv: `pipx install "markitdown[all]"` and set `MARKITDOWN_PATH=/abs/path/to/markitdown`.
- On a locked-down Linux server (no `ensurepip`, no sudo), `install-markitdown` installs with `uv tool install "markitdown[all]"` when uv is present and prints the `MARKITDOWN_PATH` line if needed; without uv it prints the no-admin uv installer command instead of downloading anything.
- `git_repo_to_markdown` uses `repomix` (Node, bundled as a normal npm dependency â no extra setup), so it is **unaffected** by all of the above. `youtube_to_markdown` gets its transcript from yt-dlp â which keeps it working only where yt-dlp itself is installed, another thing the router does not install for you (recommended: `uv tool install "yt-dlp[default,curl-cffi]"`). `conversionToolbox.youtube` says whether yt-dlp was found, and `toolsDegraded` lists `youtube_to_markdown` only when it was not. Details, including the HTTP 429 / bot-check diagnosis: [feature sheet 5](docs/features/05-conversion-de-documents.md) (French).
**High-fidelity PDF via Docling (opt-in).** `pdf_to_markdown_docling` uses [Docling](https://github.com/docling-project/docling) (IBM / LF AI & Data Foundation, MIT) instead of MarkItDown â its layout + TableFormer models reconstruct table structure and reading order that MarkItDown's `pdfminer.six` backend loses, at ~10Ă the CPU cost. Docling pulls torch/onnxruntime + model weights, so it is **not** installed by default. Disk footprint depends on the OS's default torch wheel: **~1.3 GB on Windows/macOS** (CPU-only torch) vs **~5.5 GB on Linux** (its default wheel bundles CUDA libraries, unused on a CPU-only box). The models (layout + TableFormer + OCR, a few hundred MB) download on first conversion into the Hugging Face cache (`HF_HOME`).
- Enable it with `OBSIDIAN_ROUTER_ENABLE_DOCLING=1 npm run install-docling` â that creates a separate `.venv-docling` and runs `pip install docling` (standard pipeline; no VLM/ASR extras). Needs Python 3.10+.
- To use a system-wide install instead: `pipx install docling` and set `DOCLING_PATH=/abs/path/to/docling`.
- `pdf_to_markdown_docling` stays listed even when Docling isn't installed; calling it then returns an actionable install hint. `pdf_to_markdown` (MarkItDown) is unaffected and remains the default fast path. Docling is PDF-only here â DOCX/PPTX/XLSX keep using MarkItDown.
- **Figures are not embedded.** The tool runs Docling with `--image-export-mode placeholder`, so each picture becomes a `<!-- image -->` marker instead of an inline base64 data-URI. The output stays text-only and small â an illustrated PDF that comes back as ~3 MB of base64 in Docling's default `embedded` mode is ~15 KB here â at the cost of dropping the figure images (table structure and reading order are still reconstructed).
Optional sandbox env vars:
| Variable | Purpose |
|---|---|
| `MARKITDOWN_PATH` | Absolute path to the `markitdown` executable. Override when not using the bundled venv. |
| `REPOMIX_PATH` | Absolute path to the `repomix` executable. Override when not using the bundled `node_modules/.bin/repomix`. |
| `YTDLP_PATH` | Absolute path to the `yt-dlp` executable, used by `youtube_to_markdown` for the transcript (when the page conversion fails or comes back without one). When unset, `yt-dlp` is looked up on `PATH`; the fallback degrades with a clear install hint if it's absent. |
| `YTDLP_COOKIES` | Absolute path to a Netscape-format `cookies.txt`, for a machine whose IP YouTube blocks (HTTP 429, "Sign in to confirm you're not a bot"). yt-dlp gets a private copy, so your file is never rewritten. Read from the router's own environment only â a workspace `.env` cannot set it. |
| `YTDLP_PROXY` | `http://`, `https://`, `socks4://`, `socks5://` or `socks5h://` proxy for yt-dlp, same case. Router environment only: a cloned repository's `.env` must not be able to route your traffic through its proxy. |
| `OBSIDIAN_ROUTER_VIDEO_SUBLANGS` | yt-dlp `--sub-langs` value for the caption fallback (default `en.*,en`). Widen to fetch other subtitle languages. |
| `MD_ALLOWED_PATHS` | `:`-separated (POSIX) or `;`-separated (Windows) list of directories the conversion tools are allowed to read. When unset (default), any absolute path is fair game. When set, the file-input conversion tools reject paths outside the listed directories, and the two asset writers (`download_page_assets`, `pptx_extract_assets`) reject an output directory outside them â a narrowing of their standing rule (a registered vault or the system temp directory, nothing else). A path that does not exist yet is judged through its nearest existing ancestor with links resolved, so a directory about to be created under a link that leaves the list is refused. |
| `MD_SHARE_DIR` | Legacy single-directory alias for `MD_ALLOWED_PATHS`, kept for backward compatibility with markdownify-mcp setups. Prefer `MD_ALLOWED_PATHS`. |
| `OBSIDIAN_ROUTER_SKIP_MARKITDOWN` | Set to exactly `1` to make `npm run install-markitdown` a no-op **and** silence the "not installed" hint in `list_vaults` / `meta-status`. |
| `OBSIDIAN_ROUTER_ENABLE_DOCLING` | Set to `1` **before install** to opt into the Docling backend for `pdf_to_markdown_docling` (creates `.venv-docling`, `pip install docling`). Any other value â the tool is listed but errors with an install hint at call time. |
| `DOCLING_PATH` | Absolute path to the `docling` executable. Override when not using the bundled `.venv-docling`. |
**What a spawned tool can see (v0.87.0+).** Every external program the MCP server and its hooks run â markitdown, Docling, the `pdf_to_images` render script, repomix, yt-dlp, git, npm, the `python --version` probe, the provisioning engine â receives an environment built for that tool from a list of **named** variables, never the router's own `process.env` and never a prefix rule. The base is what the OS needs to start a process (`PATH`, `HOME`, the temp and profile roots, `SystemRoot`/`ComSpec`/`PATHEXT` on Windows, the XDG roots on POSIX), plus, per tool, what it actually reads: proxies and CA bundles (`HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY`, `SSL_CERT_FILE`, `REQUESTS_CA_BUNDLE`, âŠ) for the networked ones, `HF_HOME` / `TORCH_HOME` / `DOCLING_ARTIFACTS_PATH` for Docling, the commit identity, `GIT_CONFIG_GLOBAL` and the SSH/GPG agent sockets for git and repomix, `npm_config_cache` and the chatter knobs for npm. Filtering is **by name, not by where a value came from**: a variable on a tool's list reaches it whether the shell or the MCP host set it â and, since v0.87.0, a **workspace `.env` can only set the keys the router's own writers put there** â `OBSIDIAN_ROUTER_DEFAULT_VAULT`, `OBSIDIAN_ROUTER_LOCKED`, `OBSIDIAN_ROUTER_AUTO_ENRICH`, `VAULT_PATH`, `MD_ALLOWED_PATHS`, `MD_SHARE_DIR` and the `OBSIDIAN_ROUTER_NO_*` opt-outs, each listed by name in `src/helpers/workspace-dotenv.mjs`. The two sandbox keys are one setting a workspace file may only **narrow**: its value is taken only when the host set neither and the instance is not gated (`READONLY`, `ALLOWED_VAULTS`, `USER_ID`), withheld and named otherwise. And since v0.89.0 one accepted key has a value it may not carry: `OBSIDIAN_ROUTER_AUTO_ENRICH` is fine, but a value that **canonicalises** to `FullAuto` â `FullAuto`, `fullauto`, `FULLAUTO`, `full`, `full-auto`, `auto` â is refused when it comes from a workspace file, because that is the one mode that turns a file travelling with a cloned repository into standing permission to write into a vault without asking again. The key stays accepted and `ClaudeAsk`, `Hybrid` and `off` still work from a file; the refused value is named on the router's stderr with what to do instead, and surfaced to Claude as `autoEnrichModeRefused` on `list_vaults` â a separate field, never an origin, because a value that was refused is not the source of the default that replaced it. `FullAuto` still comes from the MCP host's server declaration, or from a `set_auto_enrich_mode` call during the session; for the same reason, `set_auto_enrich_mode` with `persist: true` refuses to write that one mode into the file while still applying it to the session. This is the accepted option 4 of the decision recorded as `liaison-workspace-vault-hors-depot`. Every hook loads the file before reading its opt-out, so a `NO_*` there is honoured by the hook it names. Every other key in that file is ignored (the router names them once on its stderr, which is the MCP log; the hooks stay silent), so a cloned repository's `.env` cannot point git, Node, a proxy, a tool override â or the router's own config, view agent or smart-link endpoint â anywhere. Host-level settings such as `OBSIDIAN_ROUTER_CONFIG`, `OBSIDIAN_ROUTER_VIEW_AGENT_URL` or `OBSIDIAN_ROUTER_SMART_LINK_SECRET` belong in the MCP host's server declaration or in the launcher of a served instance, never in a workspace file. `PYTHONIOENCODING=utf-8` is fixed for the five Python children â before v0.87.0 a piped Python stdout on Windows used the ANSI code page, and accented characters came back as `ïżœ`. Refused everywhere, whatever a list says: anything that runs a command or injects code (`NODE_OPTIONS`, `GIT_SSH_COMMAND`, `GIT_CONFIG_VALUE_n`, `LD_PRELOAD`, `PYTHONPATH`, `PYTHONWARNINGS`, `PSModulePath`, âŠ), redirects a repository or a registry (`GIT_DIR`, `npm_config_registry`), or looks like a credential (`*TOKEN*`, `*SECRET*`, `*_API_KEY`, `npm_config__authToken`, âŠ). Fifteen spawns keep the full environment on purpose and are pinned by file, count and command in the test: the release tooling run from the developer's own shell (`build-mcpb`, `bump-version`, `create-release`, `export-gate`), the two interactive installers, and the three desktop-app launchers (`cmd /c start`, `open`, `xdg-open`). The conversion tools also run in a private, empty temp directory rather than in the workspace, so a `yt-dlp.conf` or `repomix.config.json` sitting in a repository can no longer reconfigure them; a relative `MARKITDOWN_PATH`-style override is resolved against the router's cwd before the spawn. The full tables live in `src/helpers/subprocess-env.mjs`; a test spawns a real executable through the production entry points to prove nothing else gets through.
**Which of your settings a project's file chose (v0.88.0+).** A workspace `.env` can still legitimately name one of your **registered** vaults â that is what `setup-vault --link-workspace`, `lock_vault --persist` and `auto-mode --persist` write there. But a workspace is very often a cloned repository, and its `.env` travels with it: nothing distinguished *you* setting the binding from *the repository* carrying one. `list_vaults` now answers that question directly, with `defaultVaultSource`, `lockSource` and `autoEnrichModeSource`, each `{ origin, variable }`. `origin` is `"binding"` when the confirmed workspace binding in your own config chose it â the tier that outranks the environment, because it is the only one that cannot have arrived with a `git clone` â `"workspace-dotenv"` when this project's own file chose it, `"host"` when the value was already in the environment (the MCP host's server declaration, a launcher, a shell), `"runtime"` when a tool call in this session set it, `"config"` when it comes from the router's `config.json`, `"first-healthy"` / `"first-active"` when nobody chose and the resolution cascade fell back to a vault, `"default"` when nothing set it, `"unset"` when there is no value, and `"unknown"` when the router cannot say â a guess is never dressed up as a fact. A variable that was set but **rejected** â a typo, a vault that no longer exists â is never reported as the source of what replaced it. The boot line says the same thing in one sentence when a workspace file chose any of the three. Nothing here changes what is allowed, and what is allowed is the short list above: a vault chosen from the ones you had already registered, the auto-enrichment mode from three of its four valid values (never `FullAuto`, since v0.89.0 â see the next paragraph), `VAULT_PATH`, a **narrowing** of the conversion sandbox, and the enumerated `OBSIDIAN_ROUTER_NO_*` opt-outs â never an endpoint, never a credential, never the router's own config. It changes what can be *said* â an assistant can now tell you "this repository's file chose the vault this session reads" instead of applying it silently. Moving that binding out of the repository altogether is the accepted decision this implements the first half of (`liaison-workspace-vault-hors-depot` in the project vault).
**And one thing a project's file can no longer choose at all (v0.89.0+).** The mode a file could put you in *silently* was also the one worth refusing outright, so the same decision's accepted option 4 does exactly that: `FullAuto` from a workspace `.env` is not applied, in any of its spellings. `autoEnrichModeSource` keeps reporting what actually took effect â `"default"`, or `"host"` if you set the mode yourself â and a **fourth** field, `autoEnrichModeRefused`, says what the file asked for and did not get: `{ value, canonical, origin, variable, reason }`, or `null` in the normal case. The two are separate on purpose: a refused value chose nothing, so naming it as a source would credit a file for the default that replaced it. A `FullAuto` you set yourself, in the MCP host's server declaration or your shell, is untouched â it reads as `"host"` and works â and a file that merely repeats it is not reported as refused, because nothing was refused. `set_auto_enrich_mode` is symmetrical: `persist: true` writes `ClaudeAsk`, `Hybrid` and `off` to `<cwd>/.env` as before, and for `FullAuto` it applies the mode to the session and returns `persistRefused` instead of writing a line the next start-up would ignore.
## Usage examples
Once the router is registered in Claude, you'd typically prompt Claude in natural language and let it pick the right tool. The shapes below show the JSON arguments each tool accepts â handy when authoring custom workflows or when reviewing what Claude actually called.
### Discovery â start every session here
```jsonc
// list_vaults â no args. Returns every vault with online/latency/missingApiKey.
{}
```
```jsonc
// list_files â explore a directory.
{ "vault": "tradingview", "directory": "Sessions" }
// Or list root if you omit directory:
{ "vault": "tradingview" }
```
### Read
```jsonc
// get_file â full markdown content + frontmatter as text.
{ "vault": "tradingview", "path": "Sessions/2026-04-29.md" }
```
```jsonc
// search â substring match, with surrounding context.
{ "vault": "tradingview", "query": "AL2SI", "contextLength": 80 }
// Cross-vault fan-out:
{ "vault": "*", "query": "money management" }
```
```jsonc
// search_smart â semantic similarity (Smart Connections embeddings).
// Returns chunks with cosine scores and breadcrumbs.
{
"vault": "tradingview",
"query": "rules for breakeven and trailing stop",
"folders": ["Formations", "Indicators"],
"excludeFolders": [".trash"],
"limit": 10
}
// Cross-vault semantic fan-out:
{ "vault": "*", "query": "what did I learn this week?" }
```
#### Freshness â when a semantic hit is older than the page it names
Smart Connections embeds a note on its own schedule. A note edited afterwards
still answers with its **previous** vector, and until v0.83.0 nothing said so:
a stale hit and a current one arrived looking identical.
On the semantic tier `search_smart` now returns a `freshness` block, and
`get_wiki_context_pack` annotates each chunk plus raises
`semantic-results-possibly-stale`. Each page gets one verdict:
| Verdict | Means |
|---|---|
| `fresh` | No evidence it differs from what was indexed. |
| `changed` | It does differ â a different byte size (proof), or a moved mtime. `sizeEvidence` says which. |
| `touched` | The mtime moved but the size is **proven identical** â a same-length edit, or a sync client touching the clock. Reported apart because it is weaker evidence. |
| `page-missing` | The page this hit names is not on disk any more. |
| `not-indexed` | No store record for it at all. |
| `unknown` | We could not tell â always with a `reason`. |
The comparison is the note's mtime and size against the ones Smart Connections
recorded **at import** (`last_import`), so it is like-for-like rather than a
heuristic. It reads the local `.smart-env` store directly and therefore works
only on a vault whose disk this machine has: a remote vault answers
`checkable: false` with a `reason` and **no warning** â never a false positive.
The block always says whether it looked, because "no warning" and "nothing to
check" are different facts.
#### Session logs are excluded by default
Omit `excludeFolders` and semantic search leaves out `wiki-meta/Sessions` â the
chronological session journals the `log-discipline` convention parks there.
That folder is **41.6% of the indexed pages across this fleet** (1212 of 2915;
498 of 803 on the router's own vault), it is raw log by construction, and no
navigational path (hot â catalog â page) ever visits it.
The default was measured, not guessed: `.trash` and `Templates` exist on none of
the 23 vaults, and `wiki-meta/graph`, `wiki-meta/digests` and
`wiki-meta/presence` hold nothing the index carries â so none of them ships. A
default that excludes nothing is worse than no default: it reads as protection.
Because the cut is large it is never silent. Every response carries
`folderExclusion` with the folders, `chosenBy` (`caller` or `default`) and
`excludedHits`; if the page still comes back short, `shortPage` says so rather
than letting it look full. Pass `excludeFolders` explicitly to replace the
default, `excludeFolders: []` to exclude nothing, or set
`OBSIDIAN_ROUTER_DEFAULT_EXCLUDE_FOLDERS` (comma-separated; empty disables it)
for a vault whose conventions differ. The BM25 tier applies the same exclusion,
so a fallback never surfaces what the tier it replaced was hiding.
#### `webpage_to_markdown` â inline links as footnotes
Pass `citations: true` and a captured page's inline links move out of the prose
into numbered footnotes with a `## References` list at the end â one footnote per
**destination**, numbered by first appearance, starting above any footnote the
page already uses. Left alone: links inside code or HTML comments, images,
wikilinks, and non-http targets (an in-document `#anchor` is navigation, not a
citation). Without the flag the output is **byte-identical** to before.
Combined with `relevanceQuery`, the filter runs **first**: markers and
definitions then match one-to-one, with no orphan reference to a block the
reader can no longer see.
#### `get_wiki_context_pack` â provenance on every item
Each entry of the pack now carries `source`: `index` (ranked out of
`wiki-meta/catalog.md`), `graph` (a wikilink from a page that was read), or
`semantic` (a Smart Connections chunk). The envelope declares the closed
vocabulary in `provenance`, naming which half is authoritative â navigation
is primary, the semantic tier is augmentation. When the navigational half comes
back **empty** while semantic chunks did not, the pack raises
`answer-relies-on-semantic-only`: that answer has no navigational anchor and
must not be the sole support for a factual claim.
### Write
```jsonc
// write_file â create or replace.
{
"vault": "tradingview",
"path": "Trades/2026-05-02 - GLE Long.md",
"content": "---\nstatus: open\nticker: GLE\n---\n\n# GLE Long\n\nEntry: ..."
}
// Refuse to overwrite if file exists:
{ "vault": "tradingview", "path": "...", "content": "...", "ifNew": true }
```
```jsonc
// append_to_file â useful for journals/logs.
{
"vault": "tradingview",
"path": "Sessions/2026-05-02.md",
"content": "\n## 14:32 â TSLA breakout invalidĂ©\n\nStop touchĂ© Ă 178.40\n"
}
```
```jsonc
// patch_file â surgical edit, no full rewrite.
// Insert under a heading (use full heading path with :: delimiter):
{
"vault": "tradingview",
"path": "Sessions/2026-05-02.md",
"operation": "append",
"targetType": "heading",
"target": "Session 2026-05-02::Trades du jour",
"content": "- TSLA: stopped out -1.2%\n"
}
// Update a single frontmatter key:
{
"vault": "tradingview",
"path": "Trades/2026-05-02 - GLE Long.md",
"operation": "replace",
"targetType": "frontmatter",
"target": "status",
"content": "closed"
}
// Replace a block by id:
{
"vault": "tradingview",
"path": "Indicators/ATP/notes.md",
"operation": "replace",
"targetType": "block",
"target": "atp-config",
"content": "Updated config for v2.3"
}
```
```jsonc
// delete_file â guarded. confirm: true is mandatory.
{ "vault": "tradingview", "path": "_scratch/old.md", "confirm": true }
```
### Templater
```jsonc
// execute_template â render and optionally save.
// Template file must exist in the vault. Args are accessible inside the
// template via tp.mcpTools.prompt("key") â note: directly under tp,
// NOT under tp.user.
{
"vault": "tradingview",
"name": "Templates/Trade.md",
"arguments": {
"ticker": "AAPL",
"direction": "long",
"entry": "175.20",
"stop": "172.50"
},
"createFile": true,
"targetPath": "Trades/2026-05-02 - AAPL Long.md"
}
// Render only (preview), don't save:
{
"vault": "tradingview",
"name": "Templates/Trade.md",
"arguments": { "ticker": "AAPL" }
}
```
## TLS
The Local REST API plugin generates a self-signed certificate by default. For localhost vaults, set `tlsInsecure: true` (the default for vaults loaded from `portRegistry`). For remote vaults behind a real TLS cert (e.g., a reverse proxy with Let's Encrypt), set `tlsInsecure: false`.
## License
Apache 2.0 â see [LICENSE](./LICENSE) and [NOTICE](./NOTICE). No usage restrictions.
---
## đ«đ· Version française
<p align="center">
<img src="./docs/assets/logo.png" alt="obsidian-mcp-router â serveur MCP multi-vaults" width="540">
</p>
<p align="center">
<a href="https://github.com/tboome33/obsidian-mcp-router/actions/workflows/test.yml"><img src="https://github.com/tboome33/obsidian-mcp-router/actions/workflows/test.yml/badge.svg" alt="tests"></a>
<a href="./LICENSE"><img src="https://img.shields.io/badge/license-Apache%202.0-blue.svg" alt="license"></a>
<a href="https://nodejs.org"><img src="https://img.shields.io/badge/node-%E2%89%A520.19.0-brightgreen.svg" alt="node"></a>
<a href="./CHANGELOG.md"><img src="https://img.shields.io/badge/version-0.97.0-blueviolet.svg" alt="version"></a>
</p>
> Serveur MCP qui aiguille les appels d'outils Claude vers **plusieurs** vaults Obsidian â locaux ou distants â via le plugin [Local REST API](https://github.com/coddingtonbear/obsidian-local-rest-api).
Au lieu d'enregistrer un MCP par vault (un process, un port, une clé API), ce router expose un **seul** MCP qui connaßt tous les vaults que tu as configurés. Chaque outil prend un paramÚtre `vault` (ou utilise ton vault par défaut), et le router fait suivre l'appel HTTPS vers la bonne instance Obsidian.
### Pourquoi
Si tu maintiens plusieurs vaults Obsidian â locaux ou distants, dans n'importe quelle combinaison â tu ne veux pas enregistrer un serveur MCP par vault et changer de contexte Ă chaque fois. Ce router est **un seul** process qui les connaĂźt tous et route chaque appel d'outil vers le bon en fonction d'un paramĂštre `vault`.
Ce que tu obtiens :
- **Une seule installation** â le plugin Claude Code embarque et lance le serveur (une entrĂ©e `~/.claude.json` sur les setups de dev) â tous les vaults sont visibles depuis n'importe quelle session Claude Desktop ou Code.
- **Vaults locaux et distants traitĂ©s Ă l'identique**. Pose l'URL + la clĂ© API dans le config ; le router se moque d'oĂč le vault tourne rĂ©ellement.
- **Recherche cross-vault** : passe `vault: "*"` Ă l'outil `search` pour lancer la recherche sur tous les vaults en parallĂšle.
### Capacités
| Surface d'outils | Couverture |
|---|---|
| Découverte | `list_vaults`, `list_files` |
| Lectures | `get_file`, `search`, `search_smart`, `get_frontmatter` |
| Ăcritures | `write_file`, `append_to_file`, `patch_file`, `delete_file`, `set_frontmatter`, `merge_frontmatter` |
| Gestion de fichiers | `move_file` |
| Templater | `execute_template` |
| Ătat du router | `lock_vault`, `unlock_vaults`, `set_auto_enrich_mode` |
| Liaison de workspace | `confirm_workspace_binding` (rattacher ce workspace à un vault principal, ajouter des secondaires, refuser une proposition), `set_secondary_vault_mode` (palier d'écriture d'un secondaire : `locked` / `soft` / `writable`) |
| Provisionnement de vault | `plan_vault`, `provision_vault` â moteur du wizard de crĂ©ation de vault (dĂ©fauts d'abord) ; `register_remote_vault` enregistre depuis une conversation un vault distant dĂ©jĂ en service (URL + clĂ©) |
| Conversion | `pdf_to_markdown`, `docx_to_markdown`, `xlsx_to_markdown`, `pptx_to_markdown`, `image_to_markdown`, `audio_to_markdown`, `youtube_to_markdown`, `bing_search_to_markdown`, `webpage_to_markdown`, `git_repo_to_markdown`, plus `pdf_to_markdown_docling` (opt-in high-fidelity PDF via [Docling](https://github.com/docling-project/docling), MIT) â port de [zcaceres/markdownify-mcp](https://github.com/zcaceres/markdownify-mcp) (MIT). Aussi `pptx_extract_assets` (les images embarquĂ©es d'un deck sur disque, par diapositive â sans Python), `pdf_to_images` (rend les pages d'un PDF en PNG que le modĂšle peut *voir*) et `filter_relevant_blocks` (filtre de pertinence BM25 sur du markdown dĂ©jĂ acquis). |
| Métadonnées web/page | `extract_page_metadata`, `propose_linked_sources`, `download_page_assets` |
| Maintenance du wiki & sources | `write_bundle` (bundle multi-fichiers journalisĂ© â application tout-ou-rien avec rollback), `refresh_okf_projections` (rĂ©gĂ©nĂšre la navigation OKF gĂ©nĂ©rĂ©e), `build_search_index` (index BM25 local, marche sur tous les vaults), `record_source` / `audit_sources` (registre de provenance du contenu ingĂ©rĂ©), `audit_vault_conventions` (les conventions sous lesquelles un vault est rĂ©ellement â fichiers en double, recommandĂ©es absentes, copies du modĂšle ; lecture seule, rĂ©parations proposĂ©es jamais appliquĂ©es) |
| Contexte & graphe | `get_wiki_context_pack`, `build_wiki_graph`, `build_wiki_tour`, `get_page_neighbors`, `wiki_path`, `find_boundary_pages`, `find_twin_pages`, `build_open_link`, `open_in_obsidian`, `get_view_link` |
| Cross-vault | tous les outils acceptent `vault: "*"` pour fan-out |
La recherche sĂ©mantique (`search_smart`), l'exĂ©cution Templater (`execute_template`) et les liens click-to-open (`build_open_link`, `open_in_obsidian`, le `clickToOpenUrl` auto-Ă©mis sur les rĂ©sultats d'Ă©criture) nĂ©cessitent que le plugin [`obsidian-mcp-router-bridge`](https://github.com/tboome33/obsidian-mcp-router-bridge) soit installĂ© dans chaque vault cible â il enregistre les routes correspondantes `/search/smart`, `/templates/execute` et `/open/*` sur Local REST API. Le bridge **â„ 0.7.0** enregistre aussi `PUT /vault-cas/*`, qui rend les Ă©critures `ifMatch` **atomiques** (lecture-comparaison-Ă©criture dans le process Obsidian) ; sans lui, `ifMatch` marche partout via un repli vĂ©rifiĂ© mais non atomique. Le bridge **â„ 0.9.0** sert en plus `GET /smart-env/sources` â le magasin de vecteurs Smart Connections, un dot-rĂ©pertoire que Local REST API refuse lui-mĂȘme de servir â ce qui permet Ă `find_twin_pages` de tourner sur un vault **distant** ; et son `GET /ping?v=<vault>` (loopback seul) ne rĂ©pond 200 que pour le vault qui Ă©coute rĂ©ellement sur ce port â l'auto-test en un clic derriĂšre les vĂ©rifications de ports du click-to-open. Les outils de conversion nĂ©cessitent Python 3.10+ sur le `PATH` plus un `npm run install-markitdown` explicite (opt-in) â voir la section anglaise « Conversion tools â runtime dependencies ». Tout le reste fonctionne contre les endpoints standards de Local REST API seuls.
### Limites â ce que le routeur ne fait **pas**
Ă lire avant de construire quoi que ce soit par-dessus. Aucune de ces limites n'est un bug : ce sont les frontiĂšres du design.
**Il ne lit pas ton vault sur le disque.** Chaque appel part en HTTPS vers la Local REST API d'un Obsidian **en cours d'exĂ©cution**. Ferme Obsidian et le vault est simplement injoignable (`ECONNREFUSED`) â le routeur ne bascule pas sur le systĂšme de fichiers, et c'est dĂ©libĂ©rĂ© : le systĂšme de fichiers ne peut honorer ni les verrous du plugin, ni les Ă©critures atomiques du bridge, ni les index propres au vault. Un vault dont Obsidian est fermĂ© n'est pas « dĂ©gradĂ© », il est hors service.
**Il ne lance pas, n'installe pas, ne synchronise pas et ne dĂ©ploie pas Obsidian.** Il configure des vaults et leur parle. Provisionner un vault (`provision_vault`) Ă©crit un squelette de vault ; ça n'installe pas Obsidian, ne l'ouvre pas et ne maintient pas deux machines en phase. Il n'y a aucune intĂ©gration git et aucune *rĂ©solution* de conflit â quand deux Ă©crivains se tĂ©lescopent, le routeur **refuse**, il ne fusionne jamais.
**Le fichier dotenv d'un dĂ©pĂŽt n'a aucune autoritĂ©.** Il peut *proposer* â sept clĂ©s, pas une de plus ; tout le reste y est ignorĂ© et nommĂ© sur la sortie d'erreur du routeur. C'est ta propre config (`~/.claude/obsidian-mcp-router/config.json`) qui dĂ©cide. Toute la grille tient lĂ : **le fichier dotenv PROPOSE, la config DĂCIDE.** Un dĂ©pĂŽt clonĂ© ne peut donc jamais rediriger tes Ă©critures ; au pire, il pose une question, une fois.
**Les vaults secondaires sont en lecture seule par dĂ©faut.** Un vault dĂ©clarĂ© par un workspace sous `also`, sans figurer dans aucune liste de palier, s'ouvre au palier `soft` : les lectures passent, et une Ă©criture est refusĂ©e tant qu'elle n'a pas Ă©tĂ© confirmĂ©e une fois (`confirmSecondaryWrite: true`) ou que le vault n'a pas Ă©tĂ© promu dĂ©finitivement (`set_secondary_vault_mode({ mode: "writable" })`, ou la liste `alsoWritable` de la config). `alsoLocked` est plus fort encore â un refus **cĂŽtĂ© serveur** qu'aucun paramĂštre ne peut outrepasser.
**Un vault que deux workspaces dĂ©clarent exige une prĂ©condition Ă chaque Ă©criture.** L'exigence est *calculĂ©e* depuis le registre de liaisons, jamais dĂ©clarĂ©e : elle peut donc s'activer sans que tu aies rien Ă©ditĂ©. `write_file` et ses frĂšres refusent alors un appel aveugle et veulent `ifMatch` (ou `ifNew`, une prĂ©condition par Ă©tape, ou un sceau de plan approuvĂ©). Ces vĂ©rifications protĂšgent les Ă©crivains qui passent par le routeur **les uns des autres** â elles ne voient pas une Ă©dition faite directement dans l'interface d'Obsidian.
**`ifMatch` n'est atomique qu'avec le bridge.** Avec `obsidian-mcp-router-bridge` â„ 0.7.0, la comparaison-Ă©criture a lieu dans le process Obsidian. Sans lui, la vĂ©rification tourne quand mĂȘme partout, mais en GET-comparer-puis-Ă©crire : une fenĂȘtre Ă©troite subsiste.
**L'atteignabilitĂ© est optionnelle, et coupe net quand elle est active.** Avec `vaultReach: "declared"`, un workspace n'atteint que les vaults qu'il dĂ©clare, plus `openVaults`. Une session non liĂ©e â le chat Claude Desktop, par exemple â ne voit que `openVaults`. C'est l'intention ; c'est aussi le moyen le plus rapide de faire disparaĂźtre tous les vaults d'un coup si tu la poses sans liste d'exception.
**Sur un déploiement gated, rien ne persiste.** Quand `OBSIDIAN_ROUTER_READONLY`, `OBSIDIAN_ROUTER_ALLOWED_VAULTS` ou `OBSIDIAN_ROUTER_USER_ID` est posé, un seul répertoire sert plusieurs appelants : une réponse persistée parlerait pour tous. Les outils de liaison, `register_remote_vault`, et la forme `persist` de `lock_vault` / `unlock_vaults` / `set_auto_enrich_mode` y sont refusés ou cachés. Les formes valables pour la session seule continuent de marcher.
**Les briques optionnelles le sont vraiment.** La recherche sémantique (`search_smart`), Templater (`execute_template`) et le click-to-open ont besoin du bridge dans chaque vault cible. Les outils de conversion ont besoin de Python 3.10+ et d'un `npm run install-markitdown` explicite. Docling est encore un opt-in distinct. Sans eux, ces outils annoncent la dépendance qui manque au lieu de deviner.
### Modes de déploiement
Le router tourne en deux modes, pilotĂ©s uniquement par variables d'environnement â **aucun changement de code**, **aucun binaire sĂ©parĂ©** :
- **Mode local (défaut)** : aucune variable posée. Un seul process stdio ; le router voit tous les vaults de `~/.claude/obsidian-mcp-router/config.json`.
- **Mode multi-tenant (opt-in)** : des variables indĂ©pendantes qui composent librement â
- `OBSIDIAN_ROUTER_ALLOWED_VAULTS=a,b,c` â whitelist des vaults que cette instance voit ;
- `VAULT_<NOM>=<JSON>` â un vault dĂ©fini entiĂšrement en variable d'env (voir la section [Config `VAULT_*`](#config-vault_-en-variable-denvironnement-Ă©ditable-depuis-le-dashboard)) ;
- `OBSIDIAN_ROUTER_READONLY=true` â masque de `ListTools` **et** refuse au `CallTool` les **18 outils d'Ă©criture** (`write_file`, `append_to_file`, `patch_file`, `set_frontmatter`, `merge_frontmatter`, `move_file`, `delete_file`, `execute_template`, `download_page_assets`, `pptx_extract_assets`, `build_wiki_graph`, `provision_vault`, `register_remote_vault`, `refresh_okf_projections`, `write_bundle`, `record_source`, `install_conventions`, `build_search_index`) ;
- `OBSIDIAN_ROUTER_USER_ID=<slug>` â journal d'audit de chaque Ă©criture rĂ©ussie dans le `wiki-meta/journal.md` du vault touchĂ© (masque aussi les outils local-only `plan_vault` / `provision_vault`).
Tableau détaillé, exemple d'entrée MCPHub et recette de déploiement complÚte : voir la section anglaise « [Deployment modes](#deployment-modes) ».
#### Mode servi â atteindre le router local depuis une session distante
Une session Claude Code distante ne peut pas simplement lancer le router : 11 de ses modules touchent légitimement le **disque** des vaults, donc le porter reviendrait à le livrer à moitié cassé. Le router reste donc à la maison et se fait **servir** sur un point d'entrée streamable-HTTP authentifié, atteint par le tunnel SSH existant :
```bash
node scripts/serve-http.mjs [--port 27300] [--session-timeout-min 240]
```
Il n'Ă©coute que sur `127.0.0.1` (dĂ©libĂ©rĂ©ment non configurable), exige un jeton bearer sur **chaque** verbe (`OBSIDIAN_ROUTER_HTTP_TOKEN`, ou `~/.claude/obsidian-mcp-router/serve-http.token`), et refuse de dĂ©marrer sans jeton. Chaque session MCP obtient **son propre processus router enfant** : un verrou de vault pris par une session est invisible pour une autre â exactement l'isolation qu'ont dĂ©jĂ les sessions stdio.
| Drapeau | Effet | Défaut |
|---|---|---|
| `--port <n>` | Port d'écoute sur la loopback. | `27300` |
| `--session-timeout-min <n>` | Seuil d'inactivitĂ© avant rĂ©colte de la session et arrĂȘt de son enfant. Minimum 1. | **240** (4 h) |
**Pourquoi le seuil vaut quatre heures et non trente minutes.** Un tunnel qui tombe n'est pas un `DELETE` : sans rĂ©colte, les clients disparus laissent des enfants zombies (six mesurĂ©s lors du spike du 2026-08-28) â le moissonneur est donc obligatoire. Mais son *Ă©chelle* compte plus que son existence : un seuil plus court qu'une pause humaine ordinaire rĂ©colte des sessions **vivantes**. Avec un seuil de 30 minutes, une sĂ©ance de plusieurs heures perd le router en plein vol pendant que l'utilisateur exĂ©cute simplement un script sur son poste â et Claude Code ne rĂ©tablit pas un serveur MCP tombĂ© en cours de session : les outils manquent pour le reste de la sĂ©ance. Les deux modes d'Ă©chec ne se valent pas : trop court coĂ»te Ă l'utilisateur ses outils pendant des heures, sans rĂ©cupĂ©ration possible depuis la session ; trop long coĂ»te un processus enfant dormant jusqu'au seuil. Ă abaisser si vous servez beaucoup de clients depuis un seul hĂŽte et que les enfants dormants deviennent le coĂ»t dominant.
**Une session périmée répond `404`, et c'est voulu.** Le serveur ne fait jamais renaßtre un enfant en silence sur un identifiant de session inconnu. Ce serait transparent, et ce serait un mensonge : l'état par session (verrou de vault, mode d'auto-enrichissement, conformité « une fois par session ») aurait été réinitialisé sous un identifiant que le client croit stable. Se remettre du `404` en se ré-initialisant, c'est le travail du client.
### Slash commands & skills (plugin Claude Code)
Le repo est aussi un **marketplace de plugin Claude Code** qui expose **54 slash commands** sous le namespace `/obsidian-router:*`. Tape `/obsidian-router:` dans Claude Code â l'autocomplete montre tout. Chaque slash command s'auto-dĂ©clenche aussi sur du langage naturel (EN + FR), donc tu n'as quasiment jamais Ă retenir le nom exact â dĂ©cris simplement ce que tu veux.
> đ **PDF de rĂ©fĂ©rence rapide** (vue d'ensemble du router + setup + config + chaque slash command avec phrases dĂ©clencheuses en langage naturel) â [Français](./docs/quick-reference-fr.pdf) · [English](./docs/quick-reference-en.pdf). Imprimable, fontes lisibles â pour papier ou consultation Ă©cran.
> đ **Guide des features (en prose, par catĂ©gorie)** â les tables de ce README sont un aide-mĂ©moire ; pour une explication lisible de chaque feature (le besoin auquel elle rĂ©pond, ce qu'elle fait, comment l'utiliser), voir [`docs/features/`](./docs/features/README.md) (13 fiches classĂ©es par catĂ©gorie, en français).
#### đ§ 17 wrappers MCP â un par outil de base du vault
##### `discover/` (2)
| Commande | Effet | Phrases déclencheuses |
|---|---|---|
| `/obsidian-router:discover-list-vaults` | Liste tous les vaults configurés (local + remote) avec online/offline/latence, vault par défaut, état du lock | *"liste mes vaults"*, *"mes vaults sont-ils en ligne"* / *"list my vaults"*, *"are my vaults online"* |
| `/obsidian-router:discover-list-files` | Liste fichiers et sous-dossiers d'un chemin de vault | *"liste les fichiers de Sessions"*, *"qu'est-ce qu'il y a dans <dossier>"* / *"list files in Sessions"*, *"what's in <folder>"* |
##### `read/` (4)
| Commande | Effet | Phrases déclencheuses |
|---|---|---|
| `/obsidian-router:read-get` | Lit un fichier en intégralité (markdown + frontmatter + meta) | *"montre-moi X"*, *"ouvre le fichier X"* / *"show me X"*, *"open the file X"* |
| `/obsidian-router:read-search` | Recherche keyword full-text (substring) avec contexte | *"trouve <texte> dans mon vault"*, *"grep <X>"* / *"find <text> in my vault"*, *"grep for X"* |
| `/obsidian-router:read-search-smart` | Recherche sémantique via Smart Connections (cosine + breadcrumbs) | *"trouve mes notes sur X"*, *"recherche sémantique sur X"* / *"find notes about X"*, *"semantic search for X"* |
| `/obsidian-router:read-frontmatter` | Lit le frontmatter (objet entier ou une clé, types préservés) | *"quel est le statut de X"*, *"montre les méta de X"* / *"what's the status of X"*, *"show me the metadata of X"* |
##### `write/` (6)
| Commande | Effet | Phrases déclencheuses |
|---|---|---|
| `/obsidian-router:write-create-or-replace` | PUT â crĂ©e ou remplace un fichier | *"crĂ©e une note X"*, *"enregistre ça comme X.md"* / *"create a note X"*, *"save this as X.md"* |
| `/obsidian-router:write-append` | POST â append Ă un fichier (auto-crĂ©ation si absent) | *"ajoute Ă X"*, *"rajoute Ă la fin de X"* / *"append to my journal"*, *"add a line to X"* |
| `/obsidian-router:write-patch` | PATCH chirurgical sur heading / block / frontmatter | *"édite la section X dans Y"*, *"remplace le contenu sous X"* / *"edit the X section in Y"*, *"replace the content under X"* |
| `/obsidian-router:write-frontmatter-set` | Set/remplace une seule clé du frontmatter | *"passe le statut de X à closed"*, *"tag ça avec X"* / *"set status to closed on X"*, *"tag this with X"* |
| `/obsidian-router:write-frontmatter-merge` | Applique plusieurs updates de frontmatter en séquence | *"sur X mets status=closed outcome=tp1"* / *"on X set status=closed outcome=tp1"* |
| `/obsidian-router:write-bundle` | Bundle multi-fichiers journalisĂ© â application tout-ou-rien avec rollback | *"Ă©cris ces 4 pages d'un bloc"* / *"write these pages atomically"* |
##### `manage/` (2)
| Commande | Effet | Phrases déclencheuses |
|---|---|---|
| `/obsidian-router:manage-move` | DĂ©place ou renomme un fichier (GET â PUT â DELETE) | *"renomme X en Y"*, *"dĂ©place X dans <dossier>"* / *"rename X to Y"*, *"move X into <folder>"* |
| `/obsidian-router:manage-delete` | Supprime un fichier (avec garde confirm en deux étapes) | *"supprime X"* (preview), *"oui confirm=true"* (proceed) / *"delete X"* puis *"yes confirm=true"* |
##### `template/` (1)
| Commande | Effet | Phrases déclencheuses |
|---|---|---|
| `/obsidian-router:template-execute` | Exécute un template Templater (preview ou save) | *"rends Templates/X.md avec arg1=v1"*, *"exécute le template daily"* / *"render Templates/X.md with arg1=v1"*, *"run the daily template"* |
##### `convert/` (2)
| Commande | Effet | Phrases déclencheuses |
|---|---|---|
| `/obsidian-router:pdf-to-markdown` | Convertit un PDF local en markdown via le CLI MarkItDown embarqué (rapide, extraction texte brut) | *"convertis ce PDF en markdown"*, *"markdown de X.pdf"* / *"convert this PDF to markdown"*, *"markdown of X.pdf"* |
| `/obsidian-router:pdf-to-markdown-docling` | PDF â markdown haute fidĂ©litĂ© via Docling (mise en page + structure de tableaux, ~10Ă plus lent â nĂ©cessite l'install Docling opt-in) | *"convertis ce PDF avec docling"*, *"conversion haute fidĂ©litĂ© de X.pdf"* / *"convert this PDF with docling"*, *"high-fidelity conversion of X.pdf"* |
#### đ 4 commandes d'Ă©tat du router (lock + auto-enrichissement + base de ports)
| Commande | Effet | Phrases déclencheuses |
|---|---|---|
| `/obsidian-router:lock` | Restreint le router à un seul vault pour la session (volatile ou `--persist` pour écrire dans `.env`) | *"verrouille sur tradingview"*, *"je ne veux travailler que sur tradingview"*, *"verrouille sur tradingview de maniÚre permanente"* / *"lock to tradingview"*, *"I only want to work on tradingview"*, *"isolate to tradingview permanently"* |
| `/obsidian-router:unlock` | LÚve le lock et restaure le routing multi-vault (`--persist` pour aussi nettoyer `.env`) | *"déverrouille les vaults"*, *"je veux pouvoir avoir accÚs à tous les vaults"* / *"unlock vaults"*, *"give me back access to all vaults"* |
| `/obsidian-router:auto-mode` | Set le mode d'auto-enrichissement wiki (`ClaudeAsk` / `Hybrid` / `FullAuto` / `off`) ; `--persist` Ă©crit dans `.env`, sauf `FullAuto` â voir plus bas | *"passe en mode Hybrid"*, *"sauve tout automatiquement"* (â FullAuto), *"arrĂȘte de sauver auto"* (â off) / *"switch to Hybrid mode"*, *"save everything automatically"*, *"stop auto-saving"* |
| `/obsidian-router:force-new-port-start` | Tire une nouvelle base d'allocation pour les **futurs** vaults uniquement â plan scellĂ© en deux phases, zĂ©ro port existant modifiĂ©, `installId` inchangĂ© | *"tire une nouvelle base de ports"*, *"les nouveaux vaults tombent sur des ports dĂ©jĂ pris"* / *"draw a new port base"*, *"my new vaults keep colliding with something"* |
Voir [Mode lock (isolation mono-vault)](#mode-lock-isolation-mono-vault), le callout auto-enrichissement plus bas, et [Identité de vault et propriété des ports](#identité-de-vault-et-propriété-des-ports) pour les designs complets et cas d'usage concrets.
#### đ 2 commandes de liaison de workspace (dans quel vault ce projet Ă©crit)
| Commande | Effet | Phrases déclencheuses |
|---|---|---|
| `/obsidian-router:bind-workspace` | Assistant dĂ©terministe : rattache ce workspace Ă un vault **principal** (dĂ©tecte les vaults ouverts, demande, confirme, lie), puis Ă©ventuellement Ă des vaults **secondaires** avec un palier d'Ă©criture chacun. Nomme aussi une proposition faite par le fichier dotenv du projet Ă laquelle personne n'a rĂ©pondu â et un « non » est enregistrĂ© comme un refus | *"Ă quel vault ce projet est-il rattachĂ©"*, *"rattache ce workspace Ă un vault"* / *"which vault is this project attached to"*, *"bind this workspace to a vault"* |
| `/obsidian-router:configure-secondary-vaults` | Le mĂȘme assistant, entrĂ© directement Ă son Ă©tape **vaults secondaires** : dĂ©clare les secondaires et choisit le palier d'Ă©criture de chacun â lecture seule stricte, lecture seule avec Ă©criture sur demande, ou lecture-Ă©criture | *"ajoute un vault secondaire"*, *"mets X en lecture seule pour ce projet"* / *"add a secondary vault"*, *"make X read-only for this project"* |
Les deux Ă©crivent dans **ta propre config du router**, pour ce workspace uniquement â jamais dans le dĂ©pĂŽt. Voir [Quels vaults un workspace peut atteindre, et lesquels il peut Ă©crire](#quels-vaults-un-workspace-peut-atteindre-et-lesquels-il-peut-Ă©crire).
#### đ©ș 7 helpers conversationnels
| Commande | Effet | Phrases déclencheuses |
|---|---|---|
| `/obsidian-router:meta-setup` | Guide l'installation MANUELLE (clone, npm link, registration MCP) â parcours dev ; une installation normale reçoit le serveur via le plugin | *"installe le router"*, *"setup obsidian-mcp-router sur cette machine"* / *"install the router"*, *"bootstrap obsidian-mcp-router on this machine"* |
| `/obsidian-router:meta-attach-vault` | Wizard interactif pour attacher un vault à un workspace (cas courant), bootstrapper un vault standalone, ou enregistrer un vault distant. Provisionne plugins + scaffolde wiki + lie `.env` + édite `.gitignore` + picker de conventions. | *"configure Obsidian pour ce projet"*, *"attache un vault à ce workspace"*, *"connecte mon vault distant"* / *"set up Obsidian for this project"*, *"attach a vault to this workspace"*, *"connect my remote vault"* |
| `/obsidian-router:meta-status` | Health-check de chaque vault avec hints de fix par catégorie d'erreur | *"diagnostique le router"*, *"mes vaults sont-ils accessibles"* / *"diagnose the router"*, *"are my vaults reachable"* |
| `/obsidian-router:meta-sync-template` | Propage les plugins/snippets/docs du vault de référence vers un ou plusieurs vaults (picker interactif) | *"synchronise le template vers tous les vaults"*, *"pousse les plugins de référence vers X"* / *"sync the template to all vaults"*, *"push reference plugins to X"* |
| `/obsidian-router:sync-from-github` | Met Ă jour un vault ou toute la flotte directement depuis le squelette GitHub (plugins, thĂšmes, snippets, docs) â sans repo de dĂ©veloppement local. MĂȘmes gardes que `--sync-plugins` plus extraction d'archive durcie | *"synchronise mes vaults depuis github"*, *"mets Ă jour la flotte depuis github"* / *"sync my vaults from github"*, *"update the fleet from github"* |
| `/obsidian-router:meta-audit-bridge-readiness` | Audite la disponibilitĂ© du click-to-open sur les vaults (bridge â„0.2.0, REST API â„4.0.0, HTTP insecure, probe live `/open`) | *"audite la disponibilitĂ© du bridge"*, *"le click-to-open est-il prĂȘt"* / *"audit bridge readiness"*, *"is click-to-open ready"* |
| `/obsidian-router:conventions` | Installe / retire / statut / propage les conventions CLAUDE.md (source-type, languages, heading-hierarchy, ...) sur les vaults | *"installe la convention source-type sur X"*, *"liste les conventions"* / *"install source-type convention on X"*, *"list conventions"* |
#### đ 24 commandes de gestion de connaissances (LLM-wiki façon Karpathy)
Un petit workflow par-dessus le router pour une base de connaissances en markdown structurĂ©, maintenue par le LLM, oĂč les pages se rĂ©fĂ©rencent entre elles et croissent avec l'usage.
| Commande | Effet | Phrases déclencheuses |
|---|---|---|
| `/obsidian-router:wiki` | Scaffold `wiki/` dans un vault (index, log, hot, overview + update CLAUDE.md) | *"scaffold un wiki"*, *"crée une base de connaissances"* / *"set up a wiki"*, *"scaffold a knowledge base"* |
| `/obsidian-router:wiki-ingest` | Ingestion d'une source (URL/fichier/texte) â pages entitĂ© & concept + cross-refs | *"ingĂšre cette URL"*, *"absorbe cet article"* / *"ingest this URL"*, *"absorb this article"* |
| `/obsidian-router:wiki-query` | RAG en 3 tiers (hot.md â catalog.md â drill), wiki-only (sans web) | *"d'aprĂšs mes notes, ..."*, *"que dit mon wiki sur X"* / *"based on my notes, ..."*, *"what does my wiki say about X"* |
| `/obsidian-router:wiki-lint` | Health check (orphelins, wikilinks morts, dérive d'index, frontmatter manquant) | *"lint le wiki"*, *"audit mon wiki"* / *"lint the wiki"*, *"audit my wiki"* |
| `/obsidian-router:wiki-fold` | Rollup idempotent des entrées du log dans `wiki/folds/` | *"compacte le journal"*, *"résume l'activité wiki de cette semaine"* / *"fold the log"*, *"roll up recent activity"* |
| `/obsidian-router:hot-compact` | Recompacte un `wiki-meta/hot.md` hors limite vers son contrat de cache (backup complet vĂ©rifiĂ© â réécriture mince state-first â trace au log) | *"compacte le hot"*, *"hot.md dĂ©passe la limite"* / *"compact the hot cache"*, *"hot.md is over limit"* |
| `/obsidian-router:save` | File la conversation courante comme note typée (session/answer/decision/ADR/...) | *"sauvegarde ça"*, *"archive cette conversation"* / *"save this"*, *"file this conversation"* |
| `/obsidian-router:decision-consolidate` | Compresse une page de décision tranchée à l'essentiel et déplace l'historique complet de la délibération vers une note d'archive vérifiée | *"consolide cette décision"*, *"archive la délibération de X"* / *"consolidate this decision"*, *"archive the deliberation of X"* |
| `/obsidian-router:autoresearch` | Boucle webâsynthĂšseâfile autonome bornĂ©e par un programme de recherche | *"fais une recherche web sur X"*, *"investigue X en ligne"* / *"research X on the web"*, *"go investigate X online"* |
| `/obsidian-router:canvas` | Crée/édite des fichiers `.canvas` Obsidian (couche visuelle pour wiki, images, PDFs) | *"crée un canvas pour X"*, *"ajoute à mon canvas"* / *"create a canvas for X"*, *"add to my canvas"* |
| `/obsidian-router:defuddle` | Strip le bruit des pages web (pubs, nav, footers) avant ingestion | *"nettoie cette page"*, *"extrais la version lisible de <url>"* / *"defuddle <url>"*, *"clean this page"* |
| `/obsidian-router:obsidian-bases` | Crée/édite des fichiers `.base` Obsidian (vues database sur frontmatter) | *"crée une base pour X"*, *"base task tracker"* / *"create a base for X"*, *"task tracker base"* |
| `/obsidian-router:wiki-graph` | Construit un knowledge-graph JSON typé depuis le vault (schéma Understand-Anything ; alimente le viewer graphe natif) | *"construis le graphe du wiki"*, *"génÚre le knowledge graph"* / *"build the wiki graph"*, *"generate the knowledge graph"* |
| `/obsidian-router:wiki-tour` | GĂ©nĂšre un parcours de lecture pĂ©dagogique ordonnĂ© depuis la topologie de liens du vault | *"fais-moi un tour du vault"*, *"par oĂč je commence"* / *"give me a tour of this vault"*, *"where do I start"* |
| `/obsidian-router:wiki-neighbors` | Montre les voisines d'une page depuis le knowledge-graph â ce qu'elle cite, ce qui la cite (backlinks), ou les deux | *"quelles pages sont liĂ©es Ă X"*, *"voisins de X"* / *"what links to X"*, *"show me the backlinks of X"* |
| `/obsidian-router:wiki-path` | Trouve la chaßne de liens la plus courte entre deux pages (« quel rapport entre A et B ? ») | *"quel rapport entre X et Y"*, *"chemin entre X et Y"* / *"how is X connected to Y"*, *"path between X and Y"* |
| `/obsidian-router:wiki-export` | Exporte le vault en fichier unique portable (`llms.txt` / `llms-full.txt`) ou en **bundle OKF** (Open Knowledge Format v0.1 de Google, partageable avec tout agent compatible OKF) | *"exporte le wiki en llms.txt"*, *"exporte en bundle OKF"* / *"export the wiki as llms.txt"*, *"export as an OKF bundle"* |
| `/obsidian-router:okf-export` | Exporte un sous-ensemble du wiki en **bundle OKF v0.1** partageable â noms slugifiĂ©s, liens relatifs, index par dossier, conformitĂ© auto-vĂ©rifiĂ©e, README agent optionnel | *"exporte ce dossier en bundle OKF"*, *"publie mon wiki en bundle"* / *"export this folder as an OKF bundle"*, *"publish my wiki as a knowledge bundle"* |
| `/obsidian-router:okf-projections` | RĂ©gĂ©nĂšre la **navigation OKF gĂ©nĂ©rĂ©e** dans `wiki/` â `index.md` racine (`okf_version` seul), un `index.md` par rĂ©pertoire, `log.md` newest-first ; auto-rafraĂźchie ~15 s aprĂšs chaque Ă©criture une fois initialisĂ©e ; `--check` = rapport de dĂ©rive | *"rafraĂźchis les projections OKF"*, *"regĂ©nĂšre les index du wiki"* / *"refresh the OKF projections"* |
| `/obsidian-router:okf-check` | Valide un bundle OKF (le nĂŽtre ou un tiers) contre les rĂšgles de conformitĂ© Open Knowledge Format v0.1 â l'un des premiers validateurs de l'Ă©cosystĂšme | *"valide ce bundle OKF"*, *"ce bundle est-il conforme ?"* / *"validate this OKF bundle"*, *"is this bundle conformant?"* |
| `/obsidian-router:build-search-index` | Construit/rafraĂźchit l'index BM25 local â recherche sans plugin, sur tous les vaults, idempotent | *"construis l'index de recherche"* / *"build the search index"* |
| `/obsidian-router:wiki-boundary` | Classe les pages « frontiĂšre » â trĂšs liĂ©es mais presque vides, celles qui valent d'ĂȘtre Ă©crites | *"qu'est-ce que je devrais Ă©crire ensuite"* / *"frontier pages"* |
| `/obsidian-router:wiki-refresh-digests` | RégénÚre les digests sidecar par page (concepts/claims/keywords) utilisés par `wiki-lint --deep` et le graphe | *"rafraßchis les digests"*, *"régénÚre les digests de page"* / *"refresh the digests"*, *"rebuild page digests"* |
| `/obsidian-router:who-is-speaking` | Identifie le membre de la famille courant dans un vault partagé et lock le routing par membre | *"qui parle"*, *"c'est Karine"* / *"who is speaking"*, *"it's Karine"* |
Plus un skill de rĂ©fĂ©rence Obsidian (sans slash command â surfacĂ© quand d'autres skills tournent) : `obsidian-markdown` (rĂ©fĂ©rence du Obsidian Flavored Markdown : wikilinks, embeds, callouts, properties, etc.). Note : `obsidian-bases` est Ă LA FOIS un skill de rĂ©fĂ©rence ET a sa propre slash command (la ligne au-dessus) â d'autres skills le consultent quand ils ont besoin de gĂ©nĂ©rer des fichiers `.base`, et tu peux aussi l'invoquer directement.
**Deux sub-agents parallĂšles** pour les batches :
- agent `wiki-ingest` â fan-out un agent par source, en parallĂšle
- agent `wiki-lint` â diagnostic read-only dans un contexte isolĂ©
**Hooks** â **11 hooks Node cross-platform**. La rĂ©partition : installer le plugin active exactement trois d'entre eux (`hot-cache-load` + `decisions-recall` + `workspace-briefing`, dĂ©clarĂ©s dans `hooks/hooks.json`) ; les huit autres ne se dĂ©clenchent que s'ils sont cĂąblĂ©s via `setup-vault.mjs` â le bootstrap de vault les auto-cĂąble dans `~/.claude/settings.json` (opt-out via `--no-hooks`), ou lance `node scripts/setup-vault.mjs --install-hooks` seul. Voir [Les hooks que le plugin active tout seul](#les-hooks-que-le-plugin-active-tout-seul) :
- `session-auto-journal` â journalise automatiquement chaque session Claude sous `wiki-meta/Sessions/` + un rĂ©cap 2 lignes dans `wiki-meta/journal.md` (rĂ©conciliation auto-rĂ©paratrice)
- `hot-cache-load` â charge `wiki-meta/hot.md` dans le contexte au SessionStart / PostCompact
- `hot-cache-update-prompt` â garde dĂ©terministe : **bloque le tour** (exit 2) tant que `wiki-meta/hot.md` n'est pas rafraĂźchi quand la session a Ă©crit une note `wiki/` (par vault, scopĂ© au transcript ; opt-out `OBSIDIAN_ROUTER_NO_HOT_CACHE_GUARD`)
- `wiki-autocommit` â auto-commit `wiki/`, `wiki-meta/`, `.raw/`, `.vault-meta/` sur git aprĂšs les Ă©critures
- `wiki-query-first-nudge` â rappelle Ă Claude de consulter le vault avant de rĂ©pondre (+ injecte les PATH RESOLUTION RULES)
- `decisions-recall` â remonte les **dĂ©cisions dĂ©jĂ tranchĂ©es** que le prompt touche, pour qu'une option Ă©cartĂ©e il y a six mois ne soit pas re-proposĂ©e. DĂ©terministe et sans modĂšle : statut tranchĂ© (`accepted`, plus les synonymes legacy que le linter tolĂšre encore) puis recouvrement de tokens, les matchs pĂ©riphĂ©riques sur du vocabulaire omniprĂ©sent Ă©tant dĂ©motĂ©s pour qu'un mot comme « router » ne remonte pas tout. Silencieux quand rien ne matche ; bornĂ© par un budget wall-clock pour qu'un vault sur lecteur virtuel ne fige pas un prompt. Une `review_after:` Ă©chue â ou illisible â est prĂ©sentĂ©e comme *Ă réévaluer*, jamais comme une contrainte. InjectĂ© comme donnĂ©e citĂ©e, jamais comme instruction (opt-out `OBSIDIAN_ROUTER_NO_DECISIONS_RECALL`)
- `vault-link-linter` â attrape les liens vault cassĂ©s/fantĂŽmes avant qu'ils ne t'atteignent
- `doc-propagation-checker` â signale les docs qui dĂ©rivent du code shippĂ©
- `vault-doc-startup-check` â surface la santĂ© vault & docs au dĂ©marrage de session
- `check-router-update` â check de version GitHub toutes les 24h
- `workspace-briefing` â ouvre chaque session par quelques lignes : Ă quel(s) vault(s) ce workspace est rattachĂ© (un, plusieurs, ou tous), ce que son `.env` a proposĂ© et s'est vu refuser, le mode d'enrichissement et sa plage, et les deux appels qui changent tout ça. Il signale aussi, en lisant le disque des vaults liĂ©s (donc Obsidian fermĂ©), un Smart Connections installĂ©-mais-dĂ©sactivĂ© ou activĂ©-avec-un-index-vide â les deux Ă©tats oĂč `search_smart` ne peut plus rĂ©pondre que par son repli BM25 et oĂč `find_twin_pages` ne peut plus rĂ©pondre du tout (opt-out `OBSIDIAN_ROUTER_NO_SEMANTIC_READINESS`, depuis l'hĂŽte uniquement). Lecture seule, ne pingue rien (opt-out `OBSIDIAN_ROUTER_NO_BINDING_BRIEFING`, **depuis l'hĂŽte uniquement** â un fichier de projet ne coupe pas le message qui parle de lui)
Les hooks vivent dans [`hooks/`](./hooks/) ; `setup-vault.mjs` les cĂąble automatiquement au bootstrap.
**Auto-enrichissement** â Claude propose proactivement de saver dans le wiki Ă trois moments naturels : **validation** (tu dis "OK" / "valide" â pin inline), **rĂ©sultat obtenu** (commit pushĂ©, tests verts â digest de candidats), et **changement de sujet** (checkpoint obligatoire avant que Claude rĂ©ponde au nouveau sujet). Agnostique du domaine : marche pour le dev, la vie perso, la recherche, la planification familiale, n'importe quoi.
**Quatre modes** (`/obsidian-router:auto-mode <Mode>` pour switcher, `--persist` pour Ă©crire dans `.env` â avec une exception : depuis la v0.89.0, `FullAuto` n'est ni Ă©crit dans le `.env` d'un workspace ni relu depuis un, parce que ce mode est une autorisation permanente d'Ă©crire dans un vault sans redemander et que le `.env` qu'un dĂ©pĂŽt clonĂ© transporte n'a pas Ă l'accorder ; il vient toujours de la dĂ©claration du serveur dans l'hĂŽte MCP ou d'un appel pendant la session, et `--persist` l'applique Ă la session en le disant. Pour ĂȘtre honnĂȘte sur la frontiĂšre : ceci ne ferme que la porte `.env`. Le `CLAUDE.md` d'un dĂ©pĂŽt peut toujours *demander Ă Claude* d'appeler `set_auto_enrich_mode`, et le router ne distingue pas cet appel du tien â c'est pourquoi le skill `auto-mode` dit Ă Claude de ne poser `FullAuto` que sur ta demande dans la conversation, jamais sur l'instruction d'un fichier du workspace) :
| Mode | Comportement | Pour quel usage |
|---|---|---|
| `ClaudeAsk` (dĂ©faut) | Propose, confirme toujours | DĂ©couverte de la feature · sessions longues Ă importance mixte · vaults oĂč les faux positifs coĂ»tent cher Ă nettoyer · pĂ©riode de calibration (1-2 semaines) avant de faire confiance Ă l'auto-save |
| `Hybrid` | Auto-save les items type-safe (facts, URLs, prĂ©fĂ©rences) ; ask sur les high-stakes (dĂ©cisions, ADRs, rĂšgles, techniques) | Sweet spot power-user aprĂšs calibration · dev actif avec ingestion d'URLs frĂ©quente · recherche oĂč les citations s'empilent mais les conclusions doivent ĂȘtre vettĂ©es |
| `FullAuto` | Auto-save tout ; audit log dans `wiki-meta/journal.md` + filtre de sensibilitĂ© (jamais d'auto-save sur credentials/mĂ©dical/financier) + hard cap (dĂ©grade en `ClaudeAsk` aprĂšs 5 saves/session) | Sessions Ă haute confiance en Claude · journal perso / chronique familiale · flows longs non supervisĂ©s (autoresearch, ingestion en batch) · brain-dumps solo oĂč le wiki EST le log de conversation |
| `off` | Pas de suggestions auto ; seul `/save` manuel | Sessions de debug que tu ne veux pas polluer dans le wiki · conversations sensibles · défaut pour les vaults légal/médical/financier · préférence control-freak |
**Placement** â la consigne est shipped dans le `CLAUDE.md` template du vault, mais aussi configurable en **instructions de Project Claude Desktop** (pattern Ă©lĂ©gant : un Project "Journal Trading" sauve toujours dans `tradingview`, un Project "Personnel" dans `personal`). Voir [`docs/auto-enrichment.md`](./docs/auto-enrichment.md) pour les cinq canaux de placement (CLAUDE.md du vault, instructions de Project, Memory, CLAUDE.md global, CLAUDE.md du dĂ©pĂŽt liĂ©), la liaison qui les gate tous, les rĂšgles d'activation, et des boilerplates copy-paste par canal.
Ătapes d'install dans la section [Installation](#installation) ci-dessous.
### Les trois briques et leurs dépendances
Trois composants, deux repos, une seule chaĂźne de dĂ©pendances. Ă lire de bas en haut â chaque couche parle Ă celle du dessus :
```
Obsidian â Local REST API (plugin communautaire) â BRIDGE (mcp-router-bridge)
â HTTP par vault (port + apiKey du plugin Local REST API)
SERVEUR MCP (obsidian-mcp-router) â process Node sur le PC
â MCP sur stdio, lancĂ© par Claude Code
PLUGIN CLAUDE CODE (obsidian-router) â commandes + skills + agents + hooks,
et il EMBARQUE ET LANCE le serveur lui-mĂȘme
```
- **Le bridge** tourne *dans Obsidian*. Il requiert Obsidian plus le plugin Local REST API : il enregistre des routes supplĂ©mentaires sur le serveur HTTP de Local REST API (`/search/smart`, `/templates/execute`, `/open/*`, heartbeat de prĂ©sence). Le `search_smart`, l'`execute_template` et les liens click-to-open du serveur en dĂ©pendent. **Sans lui**, le CRUD de fichiers du serveur fonctionne toujours (routes Local REST API standard) â la recherche sĂ©mantique, l'exĂ©cution Templater et les liens cliquables, non. Il se met Ă jour via BRAT depuis les releases GitHub.
- **Le serveur MCP** tourne sur le PC, lancĂ© par Claude Code â via le plugin (le cas normal), ou via une entrĂ©e manuelle `~/.claude.json` sur les setups de dev. Il requiert Node â„ 20.19.0, le plugin Local REST API dans chaque vault (obligatoire), le bridge dans chaque vault (optionnel â nĂ©cessaire pour recherche sĂ©mantique / Templater / click-to-open), et son registre `~/.claude/obsidian-mcp-router/config.json` (maintenu par `setup-vault.mjs`). Les commandes et skills du plugin Claude Code orchestrent ses outils MCP.
- **Le plugin Claude Code** tourne dans Claude Code et **embarque le serveur** (une installation = tout ; une mise Ă jour = tout). Ses skills et commandes pilotent les outils du serveur ; deux hooks (`hot-cache-load`, `decisions-recall`) lisent les fichiers du vault directement sur disque, sans passer par le serveur. Le prĂ©fixe des noms d'outils dĂ©pend du mode d'enregistrement du serveur â voir [Les noms d'outils dĂ©pendent du mode d'enregistrement](#les-noms-doutils-dĂ©pendent-du-mode-denregistrement).
### Prérequis
| Plugin (par vault) | Requis pour | OĂč l'obtenir |
|---|---|---|
| **Local REST API** | Tous les outils | Community plugins â "Local REST API" par Adam Coddington |
| **MCP Router Bridge** | `search_smart`, `execute_template`, liens click-to-open (`build_open_link`, `open_in_obsidian`, le `clickToOpenUrl` auto-Ă©mis) | Ă installer depuis [`tboome33/obsidian-mcp-router-bridge`](https://github.com/tboome33/obsidian-mcp-router-bridge) â enregistre les routes REST `/search/smart`, `/templates/execute` et `/open/*` que ce router appelle (`meta-audit-bridge-readiness` sonde ces derniĂšres). |
| **Smart Connections** | `search_smart` | Community plugins â "Smart Connections" â moteur d'embeddings |
| **Smart Lookup** | *rien â pour l'humain seulement* | Community plugins â "Smart Lookup". Depuis Smart Connections 4.7, la moitiĂ© « recherche » est un plugin Ă part : elle rĂ©pond Ă une question tapĂ©e, lĂ oĂč Smart Connections rĂ©pond « qu'est-ce qui ressemble Ă la note ouverte ». Les deux lisent le mĂȘme index `.smart-env`, donc `search_smart` n'a besoin que de **Smart Connections** â cette ligne existe parce que le vault de rĂ©fĂ©rence distribue aussi Smart Lookup, et que son absence n'est pas une panne du router. |
| **Templater** | `execute_template` | Community plugins â "Templater" par SilentVoid13 |
Il te faut aussi :
- **Node.js â„ 20.19.0** (`undici@7` exige 20.18.1 ; le patch supplĂ©mentaire, c'est le drapeau `--permission` de Node â renommĂ© depuis `--experimental-permission` en 20.19.0 â dont la suite de tests se sert pour prouver qu'aucun outil n'a besoin du disque du vault)
- **Sous Windows**, les deux outils qui Ă©crivent des images (`pptx_extract_assets`, `download_page_assets`) utilisent **koffi** â une interface vers les fonctions natives, installĂ©e avec les autres dĂ©pendances npm â pour demander Ă Windows oĂč se trouve vraiment le dossier de sortie qu'ils tiennent. Sans lui, ils refusent d'Ă©crire plutĂŽt que d'Ă©crire sans Ă©pinglage ; ils refusent aussi un disque qui ne se dĂ©clare pas NTFS (une lettre FAT32 ou de lecteur cloud, par exemple).
- Au moins un vault provisionnĂ© dans `~/.claude/obsidian-mcp-router/config.json`. Si tu n'as jamais fait ce setup, lance `npm run setup-vault -- "<vault-path>"` depuis un clone de ce repo, ou invoque [`scripts/setup-vault.mjs`](./scripts/setup-vault.mjs) directement â il bootstrappe la config interactivement. RĂ©fĂ©rence du schĂ©ma : [`examples/config.example.json`](./examples/config.example.json).
- Un **vault de référence** enregistré auprÚs du router. Il contient le set canonique de plugins + config que `setup-vault.mjs` clone dans chaque nouveau vault. Voie rapide : `node scripts/setup-vault.mjs --bootstrap-reference <path>` scaffolde depuis le skeleton livré ([`templates/reference-vault-skeleton/`](./templates/reference-vault-skeleton/)) et télécharge automatiquement le bridge plugin. Procédure complÚte (manuelle + troubleshooting) : [`docs/reference-vault-setup.md`](./docs/reference-vault-setup.md) (en anglais).
> đ **Le vault existe dĂ©jĂ ? Ne lance pas le wizard â attache-le.** Une seule commande idempotente, depuis le dossier du workspace :
>
> ```bash
> obsidian-mcp-router --attach <slug-du-vault> [--also <autre-slug>]...
> ```
>
> Elle ne provisionne rien (chaque slug doit dĂ©jĂ ĂȘtre enregistrĂ©) et fait les quatre Ă©critures cĂŽtĂ© workspace : le binding `.env`, `.claude/settings.json` pour **activer le plugin router â sans quoi le `.env` est inerte et aucun hook ne tourne**, un bloc `CLAUDE.md` qui nomme les vaults, et `.gitignore`. Flags : `--workspace <path>` (dĂ©faut : le cwd), `--no-plugin` / `--no-claude-md` / `--no-gitignore`. Elle vit sur le binaire et non dans le plugin, dĂ©libĂ©rĂ©ment : c'est la commande dont tu as besoin *avant* que le router existe dans ton workspace, et le plugin est justement activĂ© par l'une de ses Ă©critures. **Multi-vault** : le router lie UN vault par workspace â les vaults `--also` sont documentĂ©s dans le bloc gĂ©nĂ©rĂ© et s'adressent explicitement par `vault: "<slug>"`, jamais chargĂ©s automatiquement. Ensuite, redĂ©marre Claude Code dans ce workspace.
> đ§ **Wizard guidĂ© de crĂ©ation de vault.** CrĂ©er un nouveau vault est defaults-first : le moteur calcule un plan par dĂ©faut complet, le montre en une ligne, et tu l'acceptes tel quel (happy path = 1 interaction) ou tu ajustes n'importe quel point (nom · emplacement · source du template · plugins · thĂšme · mode wiki). Il fonctionne depuis **n'importe quel harnais LLM** via les outils MCP `plan_vault` (read-only) + `provision_vault` â pas seulement le CLI. Dans Claude Code : le skill [`meta-attach-vault`](./skills/meta-attach-vault/SKILL.md). Depuis tout autre agent (Codex, Hermes, un client MCP brut) : le playbook [`docs/vault-wizard.md`](./docs/vault-wizard.md). En direct : `node scripts/setup-vault.mjs "<vault-path>" --dry-run --json` pour prĂ©visualiser, puis sans `--dry-run` pour appliquer (`--help` liste tous les flags du wizard). Les deux outils sont LOCAL-ONLY (masquĂ©s sur les dĂ©ploiements gated) ; `provision_vault` refuse les chemins hors des racines de vaults connues ; `--from-vault` copie la config seule (secrets toujours rĂ©gĂ©nĂ©rĂ©s).
> **Les snippets CSS sont clonĂ©s automatiquement.** Chaque invocation de `setup-vault.mjs` copie aussi `<referenceVault>/.obsidian/snippets/*.css` dans le vault target et merge les basenames dans `<target>/.obsidian/appearance.json` `enabledCssSnippets`. Le skeleton ship `no-task-strikethrough.css` (dĂ©sactive le `text-decoration: line-through` par dĂ©faut d'Obsidian sur les items `- [x]`, alignĂ© sur la convention [`roadmap-discipline`](./skills/conventions/snippets/roadmap-discipline.md) §2bis). Opt-out par vault dans Settings â Appearance â CSS snippets. Pour pousser une mise Ă jour de snippet (ou plugin) Ă TOUS les vaults configurĂ©s d'un coup : `node scripts/setup-vault.mjs --sync-all` (idempotent ; ajoute `--force` pour re-cloner les fichiers existants).
### Installation
> đ **Vault de rĂ©fĂ©rence requis pour `setup-vault.mjs`** â pour bootstrapper de nouveaux vaults via le script (ce que la plupart des utilisateurs voudront), il faut d'abord un vault de rĂ©fĂ©rence configurĂ© une seule fois qui contient le set canonique de plugins. Voie la plus rapide : `node scripts/setup-vault.mjs --bootstrap-reference <path>` (scaffolde le skeleton + tĂ©lĂ©charge le bridge plugin en une commande, puis te guide pour installer les plugins marketplace via Obsidian). Doc complĂšte avec troubleshooting : [`docs/reference-vault-setup.md`](./docs/reference-vault-setup.md) (en anglais).
**Le plugin embarque le serveur MCP.** Installer le plugin apporte le serveur, les slash commands, les skills et les hooks ensemble ; le mettre Ă jour les met tous Ă jour d'un coup. Va directement Ă l'Ă©tape 2 et saute l'Ă©tape 1 â elle ne sert qu'Ă faire tourner le serveur depuis un clone.
#### Ătape 1 â Installer le MCP server *(optionnel â le plugin l'embarque dĂ©jĂ )*
Utile seulement si tu développes sur le router, ou si tu veux délibérément enregistrer le serveur indépendamment du plugin.
```bash
git clone https://github.com/tboome33/obsidian-mcp-router.git
cd obsidian-mcp-router
npm install
npm link # rend le binaire `obsidian-mcp-router` accessible globalement
```
Enregistre-le dans `~/.claude.json` (user scope) sous le nom `obsidian-router` :
```json
{
"mcpServers": {
"obsidian-router": {
"type": "stdio",
"command": "obsidian-mcp-router"
}
}
}
```
Le router lit `~/.claude/obsidian-mcp-router/config.json` au dĂ©marrage (le mĂȘme fichier maintenu par `setup-vault.mjs`) et expose tous les vaults automatiquement.
> â ïž **Ne fais pas les deux sans le vouloir.** Un serveur enregistrĂ© Ă la main et celui fourni par le plugin sont deux commandes diffĂ©rentes : Claude Code ne les considĂšre donc pas comme des doublons, et tu te retrouves avec **deux processus serveur et deux exemplaires de chaque outil**. Choisis-en un. Pour basculer une installation enregistrĂ©e Ă la main vers le plugin, supprime l'entrĂ©e `obsidian-router` de `~/.claude.json`.
#### Ătape 2 â Installer le plugin
**Enregistre le marketplace globalement** dans `~/.claude/settings.json` :
```json
{
"extraKnownMarketplaces": {
"obsidian-mcp-router-marketplace": {
"source": {
"source": "github",
"repo": "tboome33/obsidian-mcp-router"
}
}
}
}
```
**Puis active le plugin par workspace**, PAS globalement. Le plugin charge 54 slash commands et 49 skills (~10k tokens de contexte par session) â tu ne veux ça que sur les workspaces qui font effectivement de l'Obsidian. Pour chaque dossier de vault et chaque workspace d'app qui consomme le router, ajoute un `.claude/settings.json` Ă la racine du workspace :
```json
{
"enabledPlugins": {
"obsidian-router@obsidian-mcp-router-marketplace": true
}
}
```
Pour les vaults bootstrappĂ©s via `setup-vault.mjs`, ce fichier est **clonĂ© automatiquement** depuis `.template/.claude/settings.json` â pas Ă Ă©crire Ă la main. Pour les workspaces hors-vault (repos de code qui travaillent avec le contenu d'un vault), copie le snippet ci-dessus dans `<workspace>/.claude/settings.json`.
RedĂ©marre Claude Code. Depuis un workspace oĂč le plugin est activĂ©, tape `/obsidian-router:` â les 54 slash commands doivent apparaĂźtre. Depuis un workspace sans, le namespace reste vide.
> **Pourquoi pas en global ?** Si tu mets `enabledPlugins` dans `~/.claude/settings.json` au lieu de per-workspace, le plugin se charge dans CHAQUE session Claude Code â scripts random, sessions de debug, repos sans rapport â payant ~10k tokens pour des commandes que ces sessions n'utiliseront jamais. Le project-scope garde le budget serrĂ©.
> **Augmenter le budget de la skill-listing (recommandĂ©).** Le router ajoute 49 skills Ă la liste exposĂ©e Ă Claude Code. Sur une instance par dĂ©faut (`skillListingBudgetFraction: 0.01`, soit 1% de la fenĂȘtre de contexte), ça pousse souvent la liste au-delĂ du budget â les descriptions sont tronquĂ©es et le triggering en langage naturel pour `/save`, `/wiki`, `/autoresearch` etc. casse silencieusement. **RecommandĂ©** : passer Ă `0.05` dans `~/.claude/settings.json` (~6k tokens supplĂ©mentaires par session). Le message *"Skill listing will be truncated â N descriptions dropped"* au dĂ©marrage de session est le symptĂŽme que ce rĂ©glage corrige.
>
> ```json
> { "skillListingBudgetFraction": 0.05 }
> ```
>
> Le skill `meta-setup` détecte un budget sous-dimensionné et propose d'appliquer ce changement interactivement.
Une installation normale se rĂ©sume Ă l'Ătape 2. Si tu prends le parcours dev (Ătape 1 â clone + `npm link` + entrĂ©e `~/.claude.json`), le skill `meta-setup` du plugin peut te guider interactivement : demande Ă Claude *"setup le obsidian-mcp-router sur cette machine"*.
### Les noms d'outils dépendent du mode d'enregistrement
Le serveur ne dĂ©clare que des noms nus (`get_file`, `write_file`, âŠ). Le prĂ©fixe vient de l'enregistrement, donc un mĂȘme outil porte des noms diffĂ©rents :
| Mode d'enregistrement | Nom complet de l'outil |
| --- | --- |
| Fourni par le plugin (le cas par défaut) | `mcp__plugin_obsidian-router_router__get_file` |
| Enregistré à la main dans `~/.claude.json` | `mcp__obsidian-router__get_file` |
| DerriĂšre MCPHub | `mcp__<id>__obsidian-router-<vault>-get_file` |
La documentation et les skills utilisent la forme courte `mcp__obsidian-router__*` par lisibilitĂ© â Claude appelle le nom qui figure rĂ©ellement dans sa liste d'outils. Les hooks, eux, reconnaissent ces outils **par suffixe** (`hooks/_helpers/tool-names.mjs`) prĂ©cisĂ©ment pour continuer Ă se dĂ©clencher sous les trois formes.
**Quand deux prĂ©fixes sont prĂ©sents en mĂȘme temps, prĂ©fĂ©rer celui du plugin.** Une session Claude Code dans l'app desktop voit Ă la fois son propre serveur de plugin et ceux que l'app dĂ©clare elle-mĂȘme, et les deux ne rĂ©pondent pas pareil sur *ce* workspace : un serveur MCP dĂ©marre dans le rĂ©pertoire de travail de son lanceur, et seul celui du plugin dĂ©marre dans le workspace. Un serveur dĂ©clarĂ© dans la config de l'app desktop dĂ©marre dans le rĂ©pertoire de l'app, n'appartient donc Ă aucun workspace â `workspaceBinding` vaut `null` et le vault par dĂ©faut est celui de la config globale. Cette rĂ©ponse est juste pour un serveur sans workspace, et fausse pour le projet dans lequel tu te trouves. MesurĂ© le 2026-09-04.
### à quel vault ce projet est-il rattaché ?
Chaque session s'ouvre en te le disant, en quelques lignes â c'est le hook
`workspace-briefing`. Il y a **trois** états, pas deux :
| Ătat | Ce que ça veut dire |
| --- | --- |
| **un vault** | Ce dossier y est rattaché. C'est le défaut de la session, et `list_vaults` montre `workspaceBinding` avec un `also` vide. |
| **plusieurs** | Un primaire plus des secondaires (`also`), tous rattachés et adressables par leur nom. Seul le primaire est le défaut. |
| **tous** | Aucune liaison (`workspaceBinding: null`). Sans `vaultReach`, les vaults enregistrés sont disponibles ; avec `vaultReach: "declared"`, seuls les `openVaults` restent accessibles, éventuellement aucun. La cascade choisit parmi les vaults accessibles. |
**OĂč vit la liaison, et pourquoi lĂ .** Dans *ton* `config.json`, sous
`workspaceBindings`, indexée par le chemin canonique du dossier. Ce fichier
n'est jamais synchronisĂ© entre machines â il porte tes chemins de vaults et tes
clĂ©s d'API â donc la dĂ©cision d'une machine n'engage jamais l'autre, et rien
dans un dépÎt ne peut y écrire une entrée.
**Ă quoi sert le `.env` du projet maintenant.** C'est un *indice portable*. Un
workspace est trÚs souvent un dépÎt cloné, et jusqu'à cette version la ligne
`OBSIDIAN_ROUTER_DEFAULT_VAULT` qu'il transportait décidait lequel de tes vaults
la session lisait, verrouillait et remplissait â un fichier que tu n'as
peut-ĂȘtre jamais Ă©crit, choisissant oĂč va une annĂ©e de notes. Il est dĂ©sormais
rapporté et non appliqué : `list_vaults` le porte dans `bindingHint`, et le
briefing le nomme. Son utilitĂ© reste entiĂšre pour ce qu'il faisait bien â
arriver sur ta *seconde* machine et y proposer la bonne réponse, une fois.
**Le changer**, depuis une conversation ou un terminal :
```bash
node scripts/setup-vault.mjs --attach <vault> --also <autre>
```
ou demande Ă Claude, qui appelle `confirm_workspace_binding` : `{ vault }` pour
rattacher, `{ vault, also: [...] }` pour plusieurs, `{ locked: true }` pour
restreindre la session, `{ clear: true }` pour supprimer la liaison. LâaccĂšs dĂ©pend toujours de `vaultReach` : avec `"declared"`, seuls les `openVaults` restent accessibles (Ă©ventuellement aucun) ; sans ce rĂ©glage, les vaults enregistrĂ©s sont disponibles. Un
vault rattachĂ© dont Obsidian est fermĂ© est ouvert pour toi â un vault fermĂ© ne
répond pas, donc une liaison vers lui serait une promesse qui ne marche pas.
**Mise à jour depuis une version antérieure.** Au premier démarrage du router
dans un workspace qui avait dĂ©jĂ un indice, il l'importe en liaison â une fois,
et il le dit en tĂȘte de chaque session jusqu'Ă ce que tu l'adoptes
(`confirm_workspace_binding({ vault })`) ou l'annules (`{ clear: true }`, qui
tient). Une ligne `OBSIDIAN_ROUTER_LOCKED` écrite par un ancien
`lock_vault --persist` est reprise elle aussi, en `locked: true` sur la liaison
importée : un cloisonnement que tu avais posé ne disparaßt pas en silence.
L'import est bornĂ© par la date de modification du `.env` lui-mĂȘme face au
moment de ta mise Ă jour : un dĂ©pĂŽt **clonĂ©** aprĂšs n'est donc jamais importĂ© â
`git clone` écrit ses fichiers maintenant, et c'est ce qui sépare un workspace
rattaché l'an dernier d'un dossier arrivé ce matin. Deux limites à connaßtre,
parce que l'horodatage est le seul signal que porte le disque : **désarchiver**
(`tar x`, un unzip qui restaure les dates, le zip source de GitHub, `rsync -a`)
conserve la date enregistrĂ©e, donc un projet obtenu ainsi *peut* ĂȘtre importĂ© ;
et sur un router dont le tout premier démarrage se fait sur cette version, il
n'y a pas de « moment de la mise Ă jour » Ă comparer, donc tout ce qui est dĂ©jĂ
sur le disque compte comme antérieur. Les deux cas sont annoncés par le
briefing de session comme n'importe quel import, et c'est ce qui les rend
réparables en une phrase.
Une chose encore sur ce chemin. Si tu fais tourner le router depuis un
checkout plutÎt que le plugin et que tes hooks ont été cùblés avant cette
version, relance une fois `node scripts/setup-vault.mjs --install-hooks` :
l'import tourne dans le router pour tout le monde, mais le briefing qui
l'annonce est un hook que ton ancien `settings.json` ne porte pas.
### Dire non Ă une proposition
Un indice dont tu ne voulais pas n'avait aucune sortie â tu l'adoptais ou tu le
subissais, réannoncé à chaque session, parce que rien ne pouvait enregistrer que
la question avait reçu sa réponse. Il se **refuse** désormais, et le refus
s'Ă©crit des deux cĂŽtĂ©s, qui ne font pas le mĂȘme travail :
| OĂč | Ce que c'est | Effet |
| --- | --- | --- |
| ton `config.json`, sous `workspaceRefusals` | l'**autorité** | silence : l'indice se lit `refused`, le briefing ne dit plus rien, l'import unique ne liera jamais ce vault |
| le `.env` du projet, en `OBSIDIAN_ROUTER_REFUSED_VAULT` | un **indice portable** | ne fait taire personne ; il survit à une désinstallation du router, et aprÚs une réinstallation la question est posée *une* fois de plus, avec ce contexte |
```
confirm_workspace_binding({ refuse: "<vault>" }) # enregistrer le non
confirm_workspace_binding({ retract: "<vault>" }) # le reprendre
```
Un refus nomme **un vault**, donc un `.env` qui en propose un autre plus tard
est quand mĂȘme signalĂ©. Lier un vault fait tomber son refus â lier, c'est
adopter. Le vault auquel un workspace est liĂ© ne peut pas ĂȘtre refusĂ© : la
liaison est déjà la réponse inverse. Et la moitié `.env` n'est écrite **que dans
le fichier qui a proposé ce vault**, par sa ligne `OBSIDIAN_ROUTER_DEFAULT_VAULT`
ou sa ligne `OBSIDIAN_ROUTER_LOCKED` â une proposition venue de ton shell ou de
ton hÎte MCP laisse le fichier du projet intact. Un dépÎt committé avec un refus
dedans peut donc, au pire, faire poser une question Ă un collĂšgue, une fois.
Sur un déploiement multi-tenant (`OBSIDIAN_ROUTER_READONLY`,
`OBSIDIAN_ROUTER_ALLOWED_VAULTS` ou `OBSIDIAN_ROUTER_USER_ID` posé), aucun des
outils de liaison n'est disponible : le workspace y est le répertoire du serveur
lui-mĂȘme, partagĂ© par tous les appelants, et une rĂ©ponse vaudrait pour tous.
### Quels vaults un workspace peut atteindre, et lesquels il peut écrire
Deux mĂ©canismes, et ils n'ont **pas** le mĂȘme dĂ©faut. L'atteignabilitĂ© est
optionnelle et ne change rien tant que tu ne la poses pas. Le palier
d'écriture, lui, est **actif**, et si tu avais déjà des secondaires il change
ce qu'ils acceptent â voir la note de mise Ă jour en fin de section.
**Atteignabilité.** Avec `"vaultReach": "declared"` dans `config.json`, un vault
enregistré ne répond à une session que si la liaison de ce workspace le nomme
(en `vault` ou dans `also`), ou s'il figure dans **`openVaults`** â la liste
d'exception qui garde un vault personnel atteignable de partout, y compris
depuis le chat Desktop, qui n'a pas de workspace. `list_vaults` continue de
*montrer* un vault inatteignable, dans `disabled[]`, avec sa raison.
**Paliers d'écriture.** Les secondaires d'un workspace (`also`) sont en lecture
seule par dĂ©faut. Trois paliers, par workspace, pour qu'un mĂȘme vault soit
strict dans un projet et ouvert dans un autre :
| Palier | Ce que fait une écriture |
| --- | --- |
| `locked` | refusĂ©e tant que ce palier est en vigueur ; les paramĂštres dâĂ©criture ne le contournent pas. Modifier un palier local avec `set_secondary_vault_mode`, ou supprimer la liaison avant de la recrĂ©er. Une entrĂ©e globale `alsoLocked` nĂ©cessite une modification de la config pour lever la restriction secondaire. |
| `soft` *(défaut)* | refusée sauf si l'appel porte `confirmSecondaryWrite: true`, que Claude ne pose qu'aprÚs ton oui explicite |
| `writable` | passe, sans friction |
Le principal est toujours en lecture-écriture. On enregistre un palier avec
`set_secondary_vault_mode({ vault, mode })`, ou on laisse l'assistant
**`/bind-workspace`** dĂ©rouler l'ensemble â oĂč on en est, le principal, les
secondaires, une question de palier par secondaire, puis un tableau de ce qui a
été enregistré.
> **Mise à jour, si tu avais déjà des secondaires.** C'est l'un des deux
> défauts que cette version inverse (l'autre est un vault que deux workspaces
> dĂ©clarent, qui cesse d'accepter les Ă©critures Ă l'aveugle â voir *Les vaults
> que deux workspaces partagent* plus bas) : avant cette version, un vault dans
> `also` acceptait les écritures comme n'importe quel autre ; désormais il est
> `soft`, et une écriture qui ne
> porte pas `confirmSecondaryWrite: true` est refusĂ©e â sans aucun
> `vaultReach` posé et sans aucune liste de palier dans ta config. Deux façons
> de revenir en arriĂšre, par vault :
> `set_secondary_vault_mode({ vault, mode: "writable" })` pour ce workspace, ou
> la liste `alsoWritable` de `config.json` pour tous les workspaces Ă la fois.
> Les lectures ne changent pas, et le refus nomme les deux remĂšdes : rien n'est
> perdu si tu le rencontres avant d'avoir lu ceci.
### Les vaults que deux workspaces partagent
Quand plusieurs workspaces dĂ©clarent le mĂȘme vault, une Ă©criture Ă l'aveugle y
est refusée : l'appel doit porter une **précondition**. `write_file` prend
`ifMatch` (le `contentSha256` renvoyé par une lecture) ou `ifNew: true` ;
`patch_file`, `append_to_file`, `set_frontmatter`, `merge_frontmatter`,
`move_file` et `delete_file` prennent `ifMatch` (`delete_file` accepte aussi le
sceau qu'a renvoyé son appel `preview: true`) ; un `write_bundle` en veut une
**par étape** (`ifMatch`, ou `ifNew: true` sur une étape d'écriture), ou le
`approvedPlanSha256` qu'a renvoyĂ© un appel `preview: true` â `expect` est la
précondition d'une *reprise*, pas d'un bundle ordinaire ;
`download_page_assets` et `pptx_extract_assets` prennent `createOnly` ; `execute_template` est
création-seule cÎté bridge. `list_vaults` le rapporte par vault en
`writesRequireIfMatch` et `sharingReason`, pour que tu le voies au lieu de le
découvrir sur un refus.
> **Mise à jour, si un de tes vaults est déjà partagé.** C'est le second défaut
> que cette version inverse, et il n'a besoin d'aucun changement de config pour
> t'atteindre : l'exigence est *calculée* depuis ton registre de liaisons, donc
> si deux workspaces dĂ©clarent dĂ©jĂ un mĂȘme vault, un `write_file` qui marchait
> hier est refusé aujourd'hui. Un vault dans `openVaults` compte comme partagé
> par hypothÚse, son lectorat n'étant pas connaissable. Rien n'est perdu quand
> tu le rencontres â le refus nomme ce qui le satisfait â mais un appelant qui
> ne passait jamais `ifMatch` doit désormais le faire. Si un vault n'est
> partagé que par accident, la sortie est de cesser de le déclarer depuis le
> second workspace (`confirm_workspace_binding`) ; il n'y a délibérément aucun
> interrupteur pour désactiver l'exigence, parce qu'un interrupteur se lirait
> « ce vault est sûr à écraser à l'aveugle », ce qui est exactement la croyance
> qui fait perdre une note.
### Enregistrer un vault distant depuis une conversation
`register_remote_vault({ name, baseUrl, apiKey })` ajoute Ă ta propre config un
vault servi par le rĂ©seau, sans Ă©diter de JSON Ă la main â la moitiĂ©
conversationnelle du bloc `remoteVaults` documenté plus bas. L'outil est caché
sur les dĂ©ploiements multi-tenant, oĂč la config est partagĂ©e. Un `localPath`
absolu optionnel indique que les fichiers du vault sont aussi sur cette machine
(Obsidian en conteneur, par exemple) ; l'outil l'enregistre tel que déclaré, et
`obsidian-mcp-router --attach <nom> --local-path <dossier>` le vérifie contre le
vault avant de l'enregistrer. Les notes passent toujours par REST uniquement â
voir [`docs/remote-vaults.md`](docs/remote-vaults.md) pour ce que ce champ ouvre.
### Les hooks que le plugin active tout seul
Installer le plugin active exactement trois hooks, sans étape d'activation, parce que Claude Code exécute ce qu'un plugin déclare dans `hooks/hooks.json` :
| Hook | RÎle | Désactivation |
| --- | --- | --- |
| `hot-cache-load` | Au démarrage de session, injecte le `wiki-meta/hot.md` du vault dans le contexte. Lecture seule. | `OBSIDIAN_ROUTER_NO_HOT_CACHE_LOAD=1` |
| `decisions-recall` | Sur un prompt qui recoupe une décision actée, la cite. Lecture seule. | `OBSIDIAN_ROUTER_NO_DECISIONS_RECALL=1` |
| `workspace-briefing` | Au dĂ©marrage de session, dit Ă quel(s) vault(s) ce workspace est rattachĂ© et comment en changer, et signale un vault liĂ© dont Smart Connections est installĂ©-mais-dĂ©sactivĂ© ou dont l'index est vide. Lecture seule, sans rĂ©seau. | `OBSIDIAN_ROUTER_NO_BINDING_BRIEFING=1` â **depuis l'hĂŽte uniquement** ; `OBSIDIAN_ROUTER_NO_SEMANTIC_READINESS=1` pour le seul contrĂŽle sĂ©mantique |
Les trois sont des no-op silencieux sans vault configurĂ©. `workspace-briefing` est ici plutĂŽt qu'en opt-in par construction : c'est lui qui rend visible le registre de liaisons, et une liaison que le router a importĂ©e depuis le `.env` d'un projet n'est sĂ»re Ă importer que parce qu'elle s'annonce au dĂ©but de chaque session. Son opt-out est le seul que le `.env` du workspace ne peut pas poser â un fichier capable de couper le message qui parle de lui serait exactement le trou que cette fonctionnalitĂ© ferme. **Les huit autres hooks restent opt-in** via `node scripts/setup-vault.mjs --install-hooks` : ils commitent dans git, Ă©crivent les transcriptions de session dans un vault, bloquent la fin d'un tour ou appellent le rĂ©seau â rien de tout cela n'est un dĂ©faut dĂ©fendable pour quelqu'un qui vient d'installer un plugin. `--hooks-status` montre lesquels sont cĂąblĂ©s, lesquels viennent du plugin, et alerte si l'un fait les deux (il se dĂ©clencherait deux fois par Ă©vĂ©nement).
### Maintenance automatique des vaults (et ses trois boutons)
AprĂšs une Ă©criture, et de nouveau au premier contact avec un vault dans une session, le routeur exĂ©cute une **passe de maintenance** : il rĂ©gĂ©nĂšre les projections de navigation OKF du vault, puis reconstruit l'index de recherche BM25 local â les deux dans une seule prise du verrou de ce vault, pour que deux sessions ne puissent jamais s'entrelacer Ă mi-parcours. La passe est dĂ©bouncĂ©e (une rafale d'Ă©critures coĂ»te une passe, pas une par fichier) et ne tourne jamais contre un vault en lecture seule, injoignable, ou sous le palier d'Ă©criture que le workspace a dĂ©clarĂ©.
| Variable | Effet | Défaut |
| --- | --- | --- |
| `OBSIDIAN_ROUTER_NO_AUTO_CONFORMANCE` | Coupe toute la passe de maintenance. Les lectures et Ă©critures continuent ; les projections et l'index cessent simplement d'ĂȘtre rafraĂźchis pour toi â appelle `refresh_okf_projections` et `build_search_index` toi-mĂȘme quand tu les veux | inactif (passe activĂ©e) |
| `OBSIDIAN_ROUTER_NO_OKF_PROJECTIONS` | Garde la passe mais saute le rafraßchissement des projections OKF, en ne laissant que la reconstruction de l'index BM25 | inactif (projections activées) |
| `OBSIDIAN_ROUTER_PROJECTIONS_DEBOUNCE_MS` | Combien de temps le routeur attend aprÚs la derniÚre écriture avant de vider la file. Un entier positif ; toute autre valeur retombe sur le défaut | `15000` (15 s) |
Les deux premiĂšres acceptent `true` / `1` / `yes` / `on`. Ce sont des opt-outs `OBSIDIAN_ROUTER_NO_*`, donc un fichier dotenv de workspace peut les poser â ils coupent un confort, jamais une garde.
### Rester Ă jour
Le router ship un hook SessionStart (`hooks/check-router-update.mjs`) qui check GitHub une fois par 24h et Ă©met une notice si une nouvelle version est disponible. La notice demande Ă Claude de la relayer sur sa premiĂšre rĂ©ponse de la session â tu es au courant sans avoir besoin de penser Ă check.
**Il est opt-in, pas activĂ© par le plugin** â cĂąble-le avec `node scripts/setup-vault.mjs --install-hooks`. Il reste dĂ©libĂ©rĂ©ment hors de `hooks/hooks.json` : il fait un appel rĂ©seau, et un plugin ne doit pas tĂ©lĂ©phoner Ă la maison dĂšs l'installation sans qu'on le lui demande. Si tu t'en passes, `/plugin update` reste la voie normale de mise Ă jour.
Si `/plugin update obsidian-router@obsidian-mcp-router-marketplace` est disponible dans ton environnement Claude Code, c'est le path one-liner. Sinon (certains environnements n'exposent pas le slash command `/plugin`), voir [`docs/how-to-update.md`](./docs/how-to-update.md) pour l'équivalent filesystem manuel en 5 étapes (recettes bash + PowerShell).
Opt-out â dĂ©finis une de ces env vars et le check est skippĂ© :
- `OBSIDIAN_ROUTER_NO_UPDATE_CHECK=true` (any truthy value)
- `OBSIDIAN_ROUTER_USER_ID=<slug>` (dĂ©ploiements multi-tenant â assume que le sysadmin gĂšre les updates centralement)
Le check est un seul GET sur `raw.githubusercontent.com`. Pas de payload, pas de tĂ©lĂ©mĂ©trie â source dans [`hooks/check-router-update.mjs`](./hooks/check-router-update.mjs).
### Flags CLI
```bash
obsidian-mcp-router --version
obsidian-mcp-router --help
obsidian-mcp-router --config /chemin/perso/config.json
obsidian-mcp-router --no-watch # désactive le hot-reload du fichier de config
obsidian-mcp-router --plugin-health <vault> [--json] # code des plugins sur le disque vs chargé par Obsidian
obsidian-mcp-router --install-plugins <vault> --dry-run # plan scellé ; appliquer avec --approved-plan-sha256 <sceau>
```
Les deux commandes de plugins travaillent sur le dossier du vault (un vault local, ou un distant qui déclare `localPath`) ; détail dans la [fiche 13](docs/features/13-installation-et-administration.md).
Par dĂ©faut, le router surveille le fichier de config et le recharge automatiquement Ă chaque modification â utile quand `setup-vault.mjs` ajoute de nouveaux vaults, ou quand le futur plugin `Obsidian Cloudflare Tunnel` Ă©crit automatiquement des URLs de tunnel dans `remoteVaults`.
### Construire tes propres macros par-dessus (avancé)
Les 54 commandes du plugin sont agnostiques du domaine. Si tu veux des **macros** qui enchaĂźnent plusieurs outils ou intĂšgrent les conventions de ton vault (daily notes, capture inbox, rollups hebdoâŠ), construis-les sĂ©parĂ©ment comme slash commands dans `~/.claude/commands/<name>.md` â pas en PR sur ce repo. Le routeur reste neutre, les macros restent Ă toi.
Voir [`docs/building-commands.md`](./docs/building-commands.md) pour le pattern et trois exemples illustratifs.
### Désactiver un vault temporairement
Pour cacher un vault de `list_vaults` sans le retirer de la config, deux options :
```jsonc
{
// Blacklist globale (fonctionne pour les vaults locaux ET distants, par nom) :
"disabledVaults": ["template", "vps-experimental"],
// Ou flag par-remote-vault (uniquement dans remoteVaults) :
"remoteVaults": [
{ "name": "qnap", "baseUrl": "...", "apiKey": "...", "enabled": false }
]
}
```
Les vaults désactivés apparaissent dans le log de démarrage `(N disabled: ...)` pour visibilité, mais n'apparaissent pas dans `list_vaults` et ne sont pas pingés.
### Résolution du vault par défaut
Quand un appel d'outil omet l'argument `vault` (ex: `read-search "gestion du risque"`), le router doit en choisir un. **Le mĂȘme appel peut rĂ©soudre vers des vaults diffĂ©rents selon le dossier depuis lequel tu lances Claude.**
Cascade de résolution, par ordre de priorité décroissant :
0. **La liaison confirmĂ©e du workspace** â ce Ă quoi *tu* as rattachĂ© ce dossier, enregistrĂ© dans ton propre `config.json` sous `workspaceBindings` et indexĂ© par le chemin canonique du dossier. Le seul niveau qui ne peut pas ĂȘtre arrivĂ© avec un `git clone`, et c'est pour ça qu'il passe devant l'environnement. Voir [Ă quel vault ce projet est-il rattachĂ© ?](#Ă -quel-vault-ce-projet-est-il-rattachĂ©-).
1. **Variable d'env `OBSIDIAN_ROUTER_DEFAULT_VAULT`** â override explicite par process, **depuis l'hĂŽte uniquement** : ta dĂ©claration de serveur MCP, un lanceur, ton shell. La mĂȘme variable dans le `.env` d'un projet est une *proposition* : elle est rapportĂ©e et jamais appliquĂ©e, parce qu'un workspace est trĂšs souvent un dĂ©pĂŽt clonĂ© et que son `.env` est venu avec. Confirme-la une fois et elle devient la liaison ci-dessus.
2. **Variable d'env `VAULT_PATH`** â auto-dĂ©tection. Si `VAULT_PATH` correspond Ă un chemin enregistrĂ© dans `portRegistry`, ce vault devient le default. Depuis le `.env` d'un projet, honorĂ© **seulement s'il nomme ce mĂȘme dossier** â le cas « ce dossier EST un vault », prĂ©cisĂ©ment ce que `setup-vault.mjs` Ă©crit dans le `.env` de chaque vault qu'il bootstrap : lancer Claude Code dans le dossier d'un vault marche donc toujours tout seul. Un fichier de projet pointant `VAULT_PATH` vers un *autre* de tes vaults est une proposition comme une autre.
3. **`config.defaultVault`** â default global explicite dans `~/.claude/obsidian-mcp-router/config.json`.
4. **Premier vault local en bonne santĂ©** â fallback historique.
5. **Premier vault actif quel que soit le type** â dernier recours.
Le router charge automatiquement le `.env` du cwd au dĂ©marrage, donc les Ă©tapes 1 et 2 fonctionnent sans outillage supplĂ©mentaire â sous rĂ©serve de la rĂšgle d'origine ci-dessus. Les variables d'env dĂ©jĂ prĂ©sentes dans le process parent gagnent sur le `.env`, et le router **enregistre laquelle des deux** a portĂ© la valeur : c'est ce constat qui distingue une proposition d'une dĂ©cision, et `list_vaults` le rapporte dans `bindingHint.origin`.
#### Trois cas concrets
**Cas 1 â ton projet EST un vault (le cas le plus commun).**
```
cd C:\VAULTS\TradingView\
claude
```
Le `.env` (écrit par `setup-vault.mjs` lors du bootstrap) contient :
```
VAULT_PATH=C:\VAULTS\TradingView
OBSIDIAN_API_KEY=...
OBSIDIAN_BASE_URL=https://127.0.0.1:27125
```
L'auto-dĂ©tection (Ă©tape 2) matche `VAULT_PATH` contre ton `portRegistry` â default = `tradingview`. **Aucune config nĂ©cessaire.** Les outils qui omettent `vault` opĂšrent sur `tradingview`.
**Cas 2 â ton projet n'est PAS un vault, mais il bosse avec un.**
```
cd C:\Code\mon-app\
claude
```
Ce dossier n'est pas un vault, donc `VAULT_PATH` n'est pas dĂ©fini. Sans intervention, le router retombe sur `config.defaultVault` (probablement `tradingview`). Si tu veux que ce projet utilise un autre vault par dĂ©faut â disons `recherche` pour la prise de notes â ajoute dans `C:\Code\mon-app\.env` :
```
OBSIDIAN_ROUTER_DEFAULT_VAULT=recherche
```
L'Ă©tape 1 gagne â default = `recherche` pour ce projet uniquement.
**Cas 3 â ton projet EST un vault, mais tu veux un autre default.**
Tu as ouvert Claude Code dans `C:\VAULTS\.template\` parce que tu documentes ce vault, mais tu veux que les appels d'outils sans `vault=` explicite tapent sur `tradingview` plutĂŽt que `template`. Ajoute dans `C:\VAULTS\.template\.env` :
```
OBSIDIAN_ROUTER_DEFAULT_VAULT=tradingview
```
L'étape 1 override l'auto-détection de l'étape 2.
#### Vérifier quel default le router a choisi
Appelle `list_vaults` â le rĂ©sultat a un champ `defaultVault` qui indique le nom rĂ©solu.
```bash
# depuis n'importe quel projet dans Claude Code :
"liste mes vaults"
```
Si le `defaultVault` n'est pas celui que tu attendais, vérifie dans l'ordre : le `.env` de ton projet, les variables d'env du process parent, et le champ `defaultVault` dans `~/.claude/obsidian-mcp-router/config.json`.
#### Override qui n'a pas pris ?
Si tu mets `OBSIDIAN_ROUTER_DEFAULT_VAULT="quelque-chose"` et que le router ne trouve pas ce nom dans l'ensemble actif (faute de frappe, vault désactivé, vault supprimé), la cascade retombe sur les étapes 2/3/4/5 ET écrit un avertissement d'une ligne sur stderr :
```
[registry] OBSIDIAN_ROUTER_DEFAULT_VAULT="recherchee" does not match any active vault â falling through to other resolution tiers. Active vaults: template, tradingview.
```
### Mode lock (isolation mono-vault)
Par défaut le router est en **mode multi-vault** : chaque appel d'outil peut cibler n'importe quel vault enregistré via le paramÚtre `vault`, et `vault: "*"` fait du fan-out sur tous. C'est le bon défaut quand tu veux qu'une seule entrée MCP serve tout le monde.
Pour les situations oĂč tu veux **l'inverse** â un seul vault pour toute la session, le router refusant tout dĂ©bordement â utilise le **mode lock**.
#### Quand le mode lock est utile
- **Sécurité** : tu travailles sur un vault sensible (documents juridiques, données client) et tu veux une barriÚre structurelle contre les écritures accidentelles ailleurs.
- **Routing par utilisateur sur une install partagée** : un seul Claude Code partagé entre plusieurs personnes. Chacun verrouille sur son vault perso au début de session ; les notes des uns ne fuitent pas chez les autres.
- **Concentration** : longue session d'ingestion ou d'autoresearch sur un wiki â le lock empĂȘche l'assistant de classer "utilement" des trucs dans un vault frĂšre.
#### Comment lock / unlock
Trois façons de verrouiller :
1. **Outil MCP direct** (Claude l'appelle pour toi) :
```
lock_vault({ vault: "tradingview" }) # volatile (cette session)
lock_vault({ vault: "tradingview", persist: true }) # écrit dans .env, survit aux restarts
```
2. **Slash command** (ou langage naturel â auto-dĂ©clenchement) :
- `/obsidian-router:lock tradingview` â volatile
- `/obsidian-router:lock tradingview --persist` â persistant
- Langage naturel : *"je ne veux travailler que sur tradingview"*, *"verrouille sur tradingview de maniĂšre permanente"*
3. **Variable d'env au démarrage** :
```
OBSIDIAN_ROUTER_LOCKED=tradingview
```
**depuis l'hĂŽte** â ta dĂ©claration de serveur MCP ou ton shell. Le router la lit au boot. La mĂȘme ligne dans le `.env` d'un projet ne verrouille plus rien : verrouiller une session sur un vault est la façon la plus forte de choisir oĂč atterrissent ses Ă©critures, donc un fichier qui voyage avec un clone peut le proposer, pas l'imposer. Ce qui fait survivre un verrou Ă un redĂ©marrage, c'est `locked: true` sur la liaison du workspace â ce que `lock_vault({ persist: true })` Ă©crit pour toi.
Pour déverrouiller :
- `unlock_vaults()` â en mĂ©moire uniquement
- `unlock_vaults({ persist: true })` â lĂšve le verrou sur la liaison (l'endroit que relit un redĂ©marrage) et retire l'indice `OBSIDIAN_ROUTER_LOCKED` du `<cwd>/.env`
- `/obsidian-router:unlock` ou *"redonne-moi accĂšs Ă tous les vaults"*
> **Caveat â persist refusĂ© au home directory.** `lock_vault({ persist: true })` refuse si le rĂ©pertoire courant EST ton home (`%USERPROFILE%` sur Windows, `$HOME` ailleurs). C'est presque toujours une erreur â Claude Code a Ă©tĂ© lancĂ© depuis `~` plutĂŽt que depuis un dossier de projet, et crĂ©er `~/.env` te surprendrait. Le lock en mĂ©moire reste actif pour la session. Pour rendre le lock persistant dans ce cas : soit relance `lock_vault` depuis un vrai dossier de projet, soit pose `OBSIDIAN_ROUTER_LOCKED=<vault>` dans ton profil shell (`~/.bashrc`, `~/.zshrc`, ou PowerShell `$PROFILE`).
#### Ce qui se passe pendant le lock
| Opération | Comportement |
|---|---|
| Appel d'outil avec `vault: <vault-lockĂ©>` | â
procĂšde normalement |
| Appel d'outil sans `vault` explicite | â
résout vers le vault locké (override la cascade default) |
| Appel d'outil avec `vault: <autre-vault>` | â throw `Router is locked to vault "<X>". Cannot operate on "<autre>". Use unlock_vaults first or specify "<X>".` |
| Appel d'outil avec `vault: "*"` (fan-out cross-vault) | â throw `Cannot fan-out: router is locked to vault "<X>". Use unlock_vaults first or specify "<X>" instead of "*".` |
| `list_vaults` | â
marche toujours. Réponse inclut un nouveau champ `lockedTo: "<X>"` pour que les callers puissent rendre l'état du lock. |
#### Trois cas concrets
**Cas 1 â lock volatile rapide pendant une session.**
Tu vas ingérer 30 articles dans ton wiki `recherche` et tu ne veux aucune dérive vers d'autres vaults :
> *"verrouille sur recherche"*
Le router lock. Tous les `wiki-ingest` partent vers `recherche`. à la fin de la session (ou au restart de Claude Code), le lock disparaßt (puisque pas persisté).
**Cas 2 â lock permanent pour une install partagĂ©e.**
Plusieurs utilisateurs partagent la mĂȘme install Claude Code. Donald veut que chaque session Claude qu'il ouvre se positionne (et reste verrouillĂ©e) sur son vault `donald`, peu importe ce que dit `config.defaultVault`.
Dans son `.env` du projet habituel :
```
OBSIDIAN_ROUTER_LOCKED=donald
```
Ou, équivalent, lancer une fois :
> *"verrouille sur donald de maniĂšre permanente"*
La slash command écrit `OBSIDIAN_ROUTER_LOCKED=donald` dans `<cwd>/.env`. Désormais, en ouvrant Claude dans ce workspace, le router boot déjà locké. Les autres utilisateurs (Mitch, Bernie...) sur d'autres workspaces ont leur propre `.env` avec leur propre valeur de lock.
**Cas 3 â changer la cible du lock.**
Tu es locké sur `recherche`. Tu veux basculer le lock sur `tradingview` :
> *"verrouille sur tradingview"*
`lock_vault` override le lock précédent atomiquement. Pas besoin d'unlocker avant.
#### Vérifier l'état du lock
```
"liste mes vaults"
```
La réponse contient maintenant `lockedTo` :
```jsonc
{
"defaultVault": "tradingview",
"lockedTo": "tradingview", // â non-null = locked
"vaults": [...],
"disabled": [...]
}
```
Quand `lockedTo` est `null`, le router est en mode multi-vault normal.
### Config `VAULT_*` en variable d'environnement (éditable depuis le dashboard)
En plus du fichier `config.json` ci-dessous, un vault peut ĂȘtre dĂ©fini entiĂšrement dans une **variable d'environnement** â une par vault â donc Ă©ditable directement depuis l'UI *Environment Variables* du serveur MCPHub (sans SSH ni Ă©dition de fichier). C'est une **3á” source de config**, mergĂ©e aprĂšs `portRegistry` + `remoteVaults` ; une entrĂ©e `VAULT_*` **Ă©crase** tout vault de mĂȘme nom. C'est **opt-in** : sans aucune `VAULT_*`, le router se comporte exactement comme avant.
```
VAULT_<NOM> = <config du vault en JSON>
```
Requis : `name`, `baseUrl`, `apiKey` (le **token seul** â le router ajoute `Authorization: Bearer ` lui-mĂȘme). Optionnel : `description`, `tlsInsecure`, `timeoutMs` (dĂ©faut `10000`). Il n'y a **pas** de flag `wireguard` par-vault â WireGuard est enforced au niveau **dĂ©ploiement** (voir ci-dessous) ; une clĂ© `wireguard` rĂ©siduelle est ignorĂ©e. Les trois modes de connexion sont choisis uniquement par `baseUrl` (tunnel WireGuard `10.8.0.x` / LAN / distant TLS â cf. exemples de la section EN « `VAULT_*` env-var config »).
Parsing défensif : une entrée malformée est **ignorée** avec un warning stderr clair nommant la clé fautive (une mauvaise var ne fait jamais planter les autres). Sur un échec de parse JSON, ni la valeur brute ni le message du parser ne sont loggés (les deux peuvent contenir l'`apiKey`). La variable réservée `VAULT_PATH` est ignorée par le scan.
**Liens de lecture Ă©phĂ©mĂšres (provider view-agent optionnel)** â poser `OBSIDIAN_ROUTER_VIEW_AGENT_URL` (+ secret partagĂ© optionnel `OBSIDIAN_ROUTER_VIEW_AGENT_TOKEN`, envoyĂ© en `X-View-Token`) branche un *provider de view-links* sur le router. Chaque Ă©criture de note porte alors un `viewLink` prĂȘt Ă cliquer vers le **GUI Obsidian live du vault, naviguĂ© sur la note** (injection dĂ©terministe cĂŽtĂ© serveur), le tool `get_view_link` apparaĂźt (masquĂ© de ListTools tant que l'URL n'est pas posĂ©e : zĂ©ro surface morte sans l'infra), et `open_in_obsidian` renvoie le lien pour les vaults distants en conteneur. Le router ne dĂ©pend que d'un petit contrat HTTP â `GET /view?vault=<nom>¬e=<chemin>` â `{"url": "<lien prĂȘt navigateur>"}` â d'aucune infrastructure particuliĂšre : voir l'**implĂ©mentation de rĂ©fĂ©rence + le contrat normatif** sur [obsidian-mcp-router-view-agent](https://github.com/tboome33/obsidian-mcp-router-view-agent) (config-driven, Python stdlib, quick tunnels cloudflared Ă©phĂ©mĂšres). Chaque requĂȘte porte aussi deux *indices de vault* optionnels, pour qu'un provider serve un vault qu'on ne lui a pas dĂ©clarĂ© : `rest` (le `baseUrl` du vault rĂ©duit Ă `scheme://hĂŽte:port` â ni identifiants, ni chemin, ni query ; la clĂ© d'API n'est jamais lue) et `obsidian_name` (le nom du vault dans Obsidian : le nom du dossier pour un vault local, le champ optionnel `obsidianName` pour un distant â voir [docs/remote-vaults.md](docs/remote-vaults.md)).
**Smart links (rĂ©solveur optionnel)** â poser `OBSIDIAN_ROUTER_SMART_LINK_URL` (URL de base du rĂ©solveur) **et** `OBSIDIAN_ROUTER_SMART_LINK_SECRET` (secret HMAC) fait Ă©mettre des **smart links signĂ©s et stables** Ă la place des view-links demandĂ©s Ă l'agent : les Ă©critures de notes et `open_in_obsidian` sur vault distant portent alors `viewLink = <rĂ©solveur>/o/<token-signĂ©>` avec `viewLinkKind: "smart"` â un calcul HMAC pur, **zĂ©ro appel rĂ©seau** (une Ă©criture ne peut jamais ĂȘtre ralentie par un agent down), et le lien reste valable dans l'historique du chat (TTL du token : 30 jours). Le lien se rĂ©sout **sur le device qui clique** (sonde du miroir Obsidian local â deep link `obsidian://` â GUI streamĂ© en dernier recours). PrioritĂ© des providers quand les deux sont configurĂ©s : smart link â view-agent â rien ; `get_view_link` continue de parler directement au view-agent. Configurer les smart links signale un dĂ©ploiement **remote** â ne posez pas `OBSIDIAN_ROUTER_SMART_LINK_*` sur un router purement local, sinon `open_in_obsidian` rendrait un lien (`opened:false`, `delivered:"link"`) au lieu de naviguer votre Obsidian local. L'implĂ©mentation de rĂ©fĂ©rence du rĂ©solveur + les contrats vivent dans le repo saas privĂ© (`obsidian-mcp-router-saas`).
**Garde de transport au niveau dĂ©ploiement** â poser `OBSIDIAN_ROUTER_ENFORCE_WG_OR_LOOPBACK=true` (typiquement sur une instance MCPHub multi-tenant) fait **REFUSER le dĂ©marrage** du router si un vault servi a un `baseUrl` dont l'hĂŽte n'est ni loopback (`127.0.0.1`/`::1`/`localhost`) ni dans le mesh WireGuard `10.8.0.0/24`. C'est un **check de config au boot sur les baseUrls configurĂ©s** â il n'exige *pas* que le tunnel WireGuard soit up, et le **loopback passe** (donc ce n'est pas « WireGuard-only »). Fail-closed â un vault ne peut jamais ĂȘtre servi silencieusement sur un lien exposĂ© ; le check tourne aprĂšs la whitelist `OBSIDIAN_ROUTER_ALLOWED_VAULTS`. Opt-in ; variable absente = aucun enforce (mode local inchangĂ©). *(`OBSIDIAN_ROUTER_REQUIRE_WIREGUARD` reste acceptĂ© comme alias dĂ©prĂ©ciĂ© ; prĂ©fĂ©rez le nom actuel â l'ancien laisse croire Ă tort que « WG doit ĂȘtre up ».)*
### Config
Le router lit la config existante maintenue par [`scripts/setup-vault.mjs`](./scripts/setup-vault.mjs), et ajoute trois champs optionnels par-dessus :
```jsonc
{
// --- écrits par setup-vault.mjs (ne pas éditer à la main) ---
"referenceVault": "C:\\VAULTS\\.template",
"portStart": 27124,
"portRegistry": {
// Two ports per vault â see "Port bookkeeping" below.
// The legacy shape (a bare number) is still read.
"C:\\VAULTS\\.template": { "https": 27124, "http": 27134 },
"C:\\VAULTS\\TradingView": { "https": 27125, "http": 27135 }
},
// --- spécifiques au router (optionnels, modifiables librement) ---
"vaultNames": {
"C:\\VAULTS\\.template": "template",
"C:\\VAULTS\\TradingView": "tradingview"
},
"remoteVaults": [
{
"name": "qnap",
"baseUrl": "https://192.168.0.11:27125",
"apiKey": "...",
"tlsInsecure": true
}
],
"defaultVault": "tradingview"
}
```
Voir [`examples/config.example.json`](./examples/config.example.json) pour un exemple complet commenté, [`docs/remote-vaults.md`](./docs/remote-vaults.md) pour le guide complet d'ajout d'un vault distant, et [`docs/cloudflare-tunnel.md`](./docs/cloudflare-tunnel.md) pour la recette d'exposition d'un vault via Cloudflare Tunnel avec auth optionnelle Cloudflare Access (service tokens supportés via le champ `extraHeaders`).
#### Faire tourner le routeur sans les disques des vaults
Un routeur qui ne parle que REST â machine de dev, conteneur, derriĂšre un hub â ne peut pas lire les fichiers des vaults. MesurĂ© le 2026-08-31 sur les 50 outils d'alors, en processus isolĂ©s : **la seule dĂ©pendance universelle au disque est la rĂ©solution de la clĂ© d'API.** Pour un vault *local* (une entrĂ©e de `portRegistry`), le routeur va chercher la clĂ© dans le `data.json` du vault avant que le moindre outil ne s'exĂ©cute. DĂ©placez cette clĂ© dans la config et la dĂ©pendance disparaĂźt â plus aucun outil de l'Ă©chantillon Ă©prouvĂ© n'a besoin du disque.
`scripts/gen-remote-config.mjs` fait ce déplacement :
```bash
node scripts/gen-remote-config.mjs --vault roland --vault tribu
```
| Drapeau | Effet |
|---|---|
| `--vault <slug>` | Vault Ă exporter. **RĂ©pĂ©table, et obligatoire** â il n'y a pas de « tout le parc » implicite. |
| `--all` | Tout le parc, aprÚs avoir annoncé combien de clés cela représente. |
| `--host <hĂŽte>` | DĂ©faut `127.0.0.1` â le bout du tunnel SSH cĂŽtĂ© distant. Un hĂŽte ni loopback ni WireGuard est signalĂ© : la garde globale `OBSIDIAN_ROUTER_ENFORCE_WG_OR_LOOPBACK` refuserait de dĂ©marrer. |
| `--format json\|env` | Un fichier de config, ou des lignes `VAULT_<NOM>=<json>`. |
| `--out <fichier>` | Ăcrire en clair ; le fichier est **créé** en `0600`. |
| `--print-secrets` | Autoriser le clair sur stdout â pour tuyauter vers un magasin de secrets. |
**Les dĂ©fauts sont prudents Ă dessein : une config portant N clĂ©s donne Ă tout processus capable de la lire un accĂšs complet en lecture *et en Ă©criture* aux N vaults.** Sur une machine oĂč tournent aussi des agents de code, c'est une Ă©lĂ©vation de privilĂšge rĂ©elle. Donc : sortie **rĂ©digĂ©e par dĂ©faut** (mĂȘme forme, marque-place `<apiKey>` â relisible, collable, versionnable) ; sĂ©lection explicite ; `--out` **refuse** d'Ă©crire dans le dĂ©pĂŽt, dans un vault, ou par-dessus un fichier aux permissions plus larges ; et aucune clĂ© n'est jamais journalisĂ©e, tronquĂ©e ni citĂ©e dans un message d'erreur.
Les clĂ©s sont lues **sur le disque**, jamais via l'API du plugin â ce mĂȘme `data.json` contient aussi la clĂ© privĂ©e TLS du vault, et seul le champ nĂ©cessaire quitte le fichier.
#### ComptabilitĂ© des ports â deux ports par vault
Chaque vault fait tourner **deux** serveurs : l'API REST en TLS sur `https`, et un serveur HTTP en clair sur `http` (son `insecurePort`) â celui que sert la route `/open/<chemin>` du bridge, donc celui auquel est Ă©pinglĂ© chaque lien click-to-open Ă©crit dans vos notes.
Un registre qui ne mémorise que le port HTTPS laisse l'allocateur attribuer à un nouveau vault un port **déjà tenu par le serveur en clair d'un autre**. Ce n'est pas théorique : neuf collisions de ce type ont été relevées sur un parc de 27 vaults, dont une rendait un vault définitivement injoignable (un appel TLS qui atterrit sur un serveur en clair rend `ERR_SSL_WRONG_VERSION_NUMBER`). Le symptÎme habituel est plus discret et plus pénible à diagnostiquer : le second vault à démarrer n'arrive pas à se lier au socket et paraßt simplement *hors ligne*, sans erreur nulle part.
Les deux ports sont donc enregistrés, et les deux espaces sont vérifiés avant d'attribuer l'un ou l'autre.
| Commande | Effet |
|---|---|
| `node scripts/setup-vault.mjs --check-ports [--json]` | Rapport en lecture seule : doublons de ports dans les deux espaces, plus la dérive registre-vs-`data.json`. Sort en `1` sur une vraie collision, pour qu'une tùche planifiée puisse alerter. |
| `node scripts/setup-vault.mjs --sync-port-registry [--dry-run]` | Enregistre le port en clair de chaque vault dans le registre, lu depuis son propre `data.json`. Sauvegarde horodatée de `config.json` d'abord. |
| `node scripts/setup-vault.mjs --status` | Affiche **les deux** ports par vault, et signale les collisions en bas. |
Trois rĂšgles que l'implĂ©mentation respecte â et que vous devriez respecter aussi si vous Ă©ditez `config.json` Ă la main :
- **Un `insecurePort` existant n'est jamais renumĂ©rotĂ©.** Ces numĂ©ros vivent dans les liens click-to-open dĂ©jĂ Ă©crits dans vos notes. Quand un conflit doit ĂȘtre rĂ©solu, c'est le port **HTTPS** qui bouge.
- **`http` n'est jamais devinĂ© comme `https + 10`.** Cet offset est la convention appliquĂ©e aux vaults **nouvellement provisionnĂ©s**, pas une propriĂ©tĂ© du parc â 15 des 27 vaults mesurĂ©s le 2026-08-30 y Ă©chappent. Quand le `data.json` d'un vault n'est pas lisible, son `http` est enregistrĂ© Ă `null`, c'est-Ă -dire *inconnu*, et `--sync-port-registry` le complĂ©tera plus tard.
- **La migration est non destructive.** L'ancienne forme est toujours lue, la conversion est idempotente, aucune clé n'est perdue, aucun port HTTPS ne bouge, et le fichier d'avant migration est conservé sous `config.json.portRegistry-<horodatage>.bak`.
#### Identité de vault et propriété des ports
Depuis la v0.94.0, chaque vault porte un UUID durable dans `.obsidian/obsidian-mcp-router/identity.json` â il survit Ă un renommage, un dĂ©placement et un changement de ports. Le fichier ne contient ni clĂ© API, ni chemin absolu, ni port (ceux-ci restent dans `data.json`, seule source de vĂ©ritĂ© du plugin ; une copie ici serait une seconde source, rĂ©pliquĂ©e par la synchronisation, libre de dĂ©river). Une installation porte aussi un UUID, tirĂ© une seule fois depuis une source cryptographique et jamais recalculĂ© une fois qu'il existe. **Seul le propriĂ©taire d'un vault peut réécrire ses ports** â les UUID sont comparĂ©s, jamais les hostnames, car deux machines peuvent partager un nom et une machine peut changer le sien.
Les nouvelles paires pour les **futurs** vaults sont tirĂ©es dans une bande (20000â32000, hors 27000â27999 oĂč vit le parc historique et le port d'usine du plugin), les deux membres testĂ©s par bind rĂ©el sur la machine avant d'ĂȘtre attribuĂ©s. Ce n'est pas une plage rĂ©servĂ©e â deux installations qui tirent indĂ©pendamment peuvent toujours entrer en collision â juste moins de collisions entre crĂ©ations indĂ©pendantes, en plus des rĂ©servations que le registre suit dĂ©jĂ .
| Commande | Effet |
|---|---|
| `node scripts/setup-vault.mjs --migrate-vault-identities --dry-run` | PrĂ©visualise le rĂ©indexage du registre par UUID de vault plutĂŽt que par chemin. Marque chaque vault `owner: null` â revendiquer un vault est un acte sĂ©parĂ© et explicite, jamais implicite sur tout le parc. Ne change aucun port, aucune clĂ©, aucun `data.json` (le plan Ă©nonce ce compte comme un littĂ©ral : `0`). |
| `node scripts/setup-vault.mjs --migrate-vault-identities --approved-plan-sha256 <sceau>` | Applique le plan prĂ©visualisĂ©. JournalisĂ© et interruption sans risque â une reprise rĂ©utilise les identitĂ©s dĂ©jĂ créées plutĂŽt que d'en frapper une seconde pour un dossier qui en a dĂ©jĂ une. Un UUID dupliquĂ© **bloque** au lieu d'ĂȘtre devinĂ© : une rĂ©plique, un dĂ©placement avec entrĂ©e obsolĂšte, et une copie indĂ©pendante se ressemblent depuis le registre et appellent des actions opposĂ©es. |
| `node scripts/setup-vault.mjs --vault-owner "<chemin>" --show` | Montre qui possĂšde les ports d'un vault, sans rien changer. |
| `node scripts/setup-vault.mjs --vault-owner "<chemin>" --claim` | Revendique un vault **sans propriĂ©taire** pour cette installation. Ăcrit un seul champ d'un seul fichier â aucun port ne change, aucune clĂ© n'est créée. |
| `node scripts/setup-vault.mjs --vault-owner "<chemin>" --release --acknowledge-transfer` | LibÚre un vault, ou le reprend à une autre installation avec le drapeau d'acquiescement. Le propriétaire actuel et le nouveau sont tous deux affichés avant toute écriture. |
| `/obsidian-router:force-new-port-start` (ou `--force-new-port-start` en CLI) | Tire une nouvelle base d'allocation pour les **futurs** vaults uniquement. Plan scellĂ© en deux phases â `--dry-run` l'affiche avec son sceau, l'application repasse les deux â donc la base Ă©crite est celle qui a Ă©tĂ© montrĂ©e, jamais retirĂ©e, et un registre qui a bougĂ© entre-temps fait refuser l'application. |
**Ce que ceci ne fait dĂ©libĂ©rĂ©ment pas.** Aucun port existant n'est jamais renumĂ©rotĂ© par ce qui prĂ©cĂšde (hors pĂ©rimĂštre assumĂ© â les ports en clair dĂ©jĂ utilisĂ©s sont Ă©crits dans des liens click-to-open, dans des mails et des transcripts que rien ne peut réécrire). « PropriĂ©taire inconnu » est un Ă©tat valide qui refuse, pas une erreur â tout le parc historique migre dans cet Ă©tat, et revendiquer reste une dĂ©cision par vault, prise par une personne. Une clĂ© API partagĂ©e entre deux vaults est signalĂ©e, jamais rĂ©parĂ©e automatiquement â une rĂ©plique synchronisĂ©e partage lĂ©gitimement la clĂ© de sa source, et la faire tourner verrouillerait l'autre machine dehors (`--check-ports` le signale dĂ©sormais sur tout le parc, avec seulement une empreinte tronquĂ©e jamais imprimĂ©e).
Design complet â les sept dĂ©cisions qui le fondent, la mĂ©canique de journal et de reprise de la migration, et les six tours de revue adversariale plus un pen test de 22 sondes qui ont façonnĂ© les garde-fous finaux : [`docs/vault-identity-and-ports.md`](./docs/vault-identity-and-ports.md).
### Outils exposés
| Outil | Description |
|---|---|
| `list_vaults` | Catalogue de tous les vaults configurés avec leur état online + latence. à appeler en premier. |
| `list_files` | Liste les fichiers d'un répertoire d'un vault donné. |
| `get_file` | Lit le contenu complet d'un fichier (markdown + frontmatter). |
| `search` | Recherche texte simple (substring). Passe `vault: "*"` pour lancer la recherche sur tous les vaults en parallĂšle. |
| `search_smart` | Recherche sémantique (par sens) via les embeddings de Smart Connections. Retourne les chunks classés avec scores cosinus et breadcrumbs (chemin de titres). Nécessite les plugins `obsidian-mcp-router-bridge` + `smart-connections` activés dans le vault cible. Supporte `vault: "*"` pour la recherche sémantique cross-vaults. |
| `write_file` | Crée un fichier ou remplace son contenu intégral. Passe `ifNew: true` pour refuser l'écrasement. |
| `append_to_file` | Ajoute du contenu en fin de fichier. Crée le fichier si absent (sauf si `requireExisting: true`). |
| `patch_file` | Ădition chirurgicale par cible `heading` / `block` / `frontmatter` â insĂ©rer sous un titre sans réécrire tout le fichier, remplacer un bloc par id, modifier une clĂ© de frontmatter. |
| `delete_file` | Suppression définitive. Exige `confirm: true` pour éviter les suppressions accidentelles. |
| `execute_template` | Exécute un template Templater, écrit optionnellement le rendu dans un nouveau fichier. Les arguments sont accessibles dans le template via `tp.mcpTools.prompt("clé")`. |
| `move_file` | DĂ©place ou renomme un fichier. ImplĂ©mentĂ© en GET source â PUT destination â DELETE source. Passe `overwrite: true` pour remplacer une destination existante. |
| `get_frontmatter` | Lit le frontmatter (objet complet ou une clĂ©). Retourne les valeurs typĂ©es â nombres, boolĂ©ens, tableaux prĂ©servĂ©s. |
| `set_frontmatter` | Définit/remplace une propriété de frontmatter. Type préservé (string/number/bool/null/array/object). |
| `merge_frontmatter` | Applique plusieurs mises Ă jour de frontmatter en sĂ©quence (non-atomique â voir ROADMAP pour l'alternative atomique). |
| `lock_vault` / `unlock_vaults` | Restreint le router Ă un seul vault pour la session (isolation mono-vault). Voir la section **Mode lock**. |
| `set_auto_enrich_mode` | Bascule le mode d'auto-enrichissement wiki entre `ClaudeAsk` / `Hybrid` / `FullAuto` / `off`. |
| `confirm_workspace_binding` | Lie ce dossier Ă un vault, dans ta propre config : `{ vault }`, `{ vault, also: [...] }` pour des secondaires, `{ locked: true }` pour restreindre la session, `{ clear: true }` pour supprimer la liaison (lâaccĂšs reste rĂ©gi par `vaultReach` et `openVaults`), `{ refuse }` / `{ retract }` pour rĂ©pondre Ă une proposition faite par un fichier de projet. Indisponible sur un dĂ©ploiement gated. |
| `set_secondary_vault_mode` | Enregistre le palier d'Ă©criture d'un SECONDAIRE de ce workspace â `locked`, `soft` ou `writable`. Par workspace : le mĂȘme vault peut ĂȘtre strict dans un projet et ouvert dans un autre. Indisponible sur un dĂ©ploiement gated. |
| `register_remote_vault` | Ajoute à ta propre config un vault servi par le réseau (`{ name, baseUrl, apiKey }`), sans éditer de JSON à la main. Local uniquement (absent des déploiements gated). |
| `get_view_link` | Construit un lien signé et expirant qui ouvre une page de vault dans l'agent de vue en lecture seule. |
| `plan_vault` | **Read-only.** Planifie la crĂ©ation d'un NOUVEAU vault local : retourne les dĂ©fauts calculĂ©s + un questionnaire structurĂ© (les 5 modes wiki, les thĂšmes installĂ©s dans la source, les vaults enregistrĂ©s dont copier la config, les profils de plugins) + avertissements â sans rien Ă©crire. Alimente le wizard guidĂ© ; enchaĂźner avec `provision_vault`. Local uniquement (absent des dĂ©ploiements gated). |
| `provision_vault` | CrĂ©e un NOUVEAU vault local en un appel depuis les rĂ©ponses du wizard (typiquement les dĂ©fauts de `plan_vault` + ajustements). Retourne un rapport Ă©tape par Ă©tape + port, insecurePort, openUri et rĂ©sultat de probe. Refuse les chemins hors des racines de vaults connues (jugĂ©s sur le chemin rĂ©el, liens rĂ©solus) sauf `allowOutsideRoots: true` ; le dossier du nouveau vault est Ă©pinglĂ© pendant toute la crĂ©ation, doit ĂȘtre celui que la vĂ©rification a approuvĂ©, et est refusĂ© si son arborescence existante contient un lien, une jonction ou un lien dur (ou un `.git` quand `gitInit` est demandĂ©) (Windows : NTFS seulement) ; `--from-vault` copie la config seule (credentials exclus, port + clĂ© API rĂ©gĂ©nĂ©rĂ©s). Local uniquement. |
| `pdf_to_markdown` · `docx_to_markdown` · `xlsx_to_markdown` · `pptx_to_markdown` · `image_to_markdown` · `audio_to_markdown` | Convertit un fichier local en markdown via le CLI Python `markitdown`. OCR image et transcription audio nĂ©cessitent les extras `[all]` (opt-in : `npm run install-markitdown`). Retourne du texte markdown â chaĂźne avec `write_file` pour persister. |
| `pdf_to_markdown_docling` | Convertit un PDF local en markdown via le pipeline standard de **Docling** (dĂ©tection de mise en page + reconnaissance de structure de tableau TableFormer). Plus haute fidĂ©litĂ© que `pdf_to_markdown` sur les tableaux complexes / mises en page multi-colonnes, Ă ~10Ă le coĂ»t CPU. **Opt-in** â nĂ©cessite l'extra Docling (voir la section anglaise « Conversion tools â runtime dependencies »). PDF uniquement ; pour les formats bureautiques, garder `pdf_to_markdown`. |
| `pdf_to_images` | **Rend** les pages d'un PDF local en images PNG, renvoyĂ©es comme blocs image MCP pour que le modĂšle **voie** une page (pas seulement son texte). Rendu via **pypdfium2** (BSD) + Pillow, du mĂȘme `.venv-docling` que Docling â renvoie un hint d'install si absent. ParamĂštres : `filepath`, `first_page`, `max_pages` (dĂ©faut 8, plafond 30), `scale` (â144 DPI). Plafonds durs de pages/octets pour borner le coĂ»t en tokens. N'Ă©crit dans aucun coffre. |
| `pptx_extract_assets` | Extrait les **images embarquĂ©es** d'un PPTX vers des fichiers, chacune rattachĂ©e Ă la ou aux diapositives qui l'utilisent. **ComplĂ©ment** de `pptx_to_markdown`, qui rend le texte, les tableaux et les notes mais seulement une rĂ©fĂ©rence alt pour les images â un deck ingĂ©rĂ© avec lui seul laisse donc des liens d'image morts. **Aucun Python requis** : un PPTX est un ZIP, lu par le lecteur du router. Ăcrit dans un dossier temporaire sauf si `outdir` est fourni ; `wiki-ingest` pointe `outdir` sur le dossier `wiki/.assets/<slug>/` du coffre, c'est donc un **outil d'Ă©criture** â masquĂ© par `OBSIDIAN_ROUTER_READONLY`, et un `outdir` situĂ© dans un coffre subit l'accessibilitĂ©, le palier d'Ă©criture et la prĂ©condition de coffre partagĂ© (`createOnly: true`) de ce coffre, exactement comme `download_page_assets` ; un `outdir` hors de tout coffre enregistrĂ© et hors du dossier temporaire du systĂšme est refusĂ©. Le dossier de sortie est Ă©pinglĂ© avant la premiĂšre Ă©criture (Windows, Linux) et aucun fichier n'est Ă©crit en ouvrant son nom : un lien permutĂ© ou posĂ© dans l'arborescence de sortie ne peut pas dĂ©tourner une Ă©criture. Renvoie un manifeste (`name`, `path`, `slides`, `bytes`, `ext`, `sha256`) Ă chaĂźner avec `write_file`. Une image rĂ©utilisĂ©e sur plusieurs diapositives est Ă©crite **une seule fois** et les liste toutes â sauf si `skipped` nomme une partie que l'outil n'a pas lue, qui pouvait ĂȘtre une diapositive de plus pour elle. Les noms de sortie sont construits et l'extension vient des octets magiques, jamais de l'archive. Les images prĂ©sentes uniquement dans une mise en page ou un masque ne sont pas extraites. |
| `youtube_to_markdown` · `bing_search_to_markdown` · `webpage_to_markdown` | Convertit une URL distante en markdown via `markitdown`. URL http(s) uniquement ; hĂŽtes privĂ©s/loopback refusĂ©s (garde SSRF). Pour les SPA JS-lourdes, prĂ©fĂšre le skill `defuddle` (navigateur headless). `webpage_to_markdown` accepte en plus un `relevanceQuery` opt-in pour filtrer le rĂ©sultat aux blocs pertinents par BM25 (cf. `filter_relevant_blocks`) â la sortie reste une string avec un commentaire de stats d'une ligne en fin. |
| `git_repo_to_markdown` | Bundle un dépÎt git (arbre de fichiers + code source) en un seul document markdown via `repomix`. Accepte une URL complÚte ou le raccourci `owner/repo`. Passe `compress: true` pour ~70% de réduction via Tree-sitter. |
| `extract_page_metadata` | Extracteur dĂ©terministe de mĂ©tadonnĂ©es de page (JSON-LD + OpenGraph + meta tags + titre) â alimente un frontmatter non-fabriquĂ© pour l'ingestion. |
| `propose_linked_sources` | Suit les `<a href>` avec scoring heuristique pour proposer des candidats d'ingestion rĂ©cursive (top-N, boosts mĂȘme-domaine / section Related). |
| `download_page_assets` | TĂ©lĂ©charge les images d'une page dans le vault (prĂ©servation des images lors de l'ingestion web). `outputDir` doit ĂȘtre dans un vault enregistrĂ© (dont les rĂšgles s'appliquent) ou dans le dossier temporaire du systĂšme ; il est Ă©pinglĂ© avant la premiĂšre Ă©criture, comme celui de `pptx_extract_assets`. |
| `build_open_link` | Construit un lien markdown click-to-open prĂȘt Ă coller (`http://127.0.0.1:<insecurePort>/open/<path>`) pour un ou plusieurs fichiers du vault. Read-only. |
| `open_in_obsidian` | Ouvre une note dans l'Obsidian en cours (et ramĂšne sa fenĂȘtre au premier plan) en appelant la route `/open` du bridge **cĂŽtĂ© serveur** â sans navigateur. Le pendant sans-navigateur d'un lien click-to-open, pour les clients (ex. Claude Desktop) qui sinon proxifient les clics de liens via un navigateur. `anchor` optionnel pour scroller Ă un titre. Navigation seule. |
| `get_wiki_context_pack` | Retourne une enveloppe de contexte JSON structurĂ©e pour une requĂȘte (primaryPages / semanticChunks / graphNeighbors / citations) afin que des agents non-Claude consomment le vault programmatiquement. |
| `build_wiki_graph` | Assemble le vault en un knowledge-graph JSON typĂ© (schĂ©ma Understand-Anything : 21 types de nĆuds / 35 d'arĂȘtes). Ăcrit `wiki-meta/graph/knowledge-graph.json` + une copie dĂ©rivĂ©e `.understand-anything/`. |
| `build_wiki_tour` | GénÚre un parcours de lecture pédagogique déterministe et ordonné depuis la topologie de liens du knowledge-graph. Read-only. |
| `get_page_neighbors` | Retourne les voisines d'UNE page depuis le knowledge-graph â celles qu'elle cite (`forward`), celles qui la citent (`backward`), ou les deux â jusqu'Ă `depth` sauts. Par dĂ©faut des liens pageâpage ; Ă©largir `nodeTypes` pour faire apparaĂźtre les concepts/sources que la page touche aussi. Un nom de page ambigu est refusĂ© avec la liste des candidats. Deux enrichissements structurels optionnels (`includeSameFolder`, `includeSharedTags`) font apparaĂźtre des voisines non liĂ©es â mĂȘme dossier, ou un tag rĂ©el partagĂ© â Ă coĂ»t nul. Read-only. |
| `wiki_path` | Trouve la chaĂźne de liens la plus courte entre DEUX pages (« quel rapport entre A et B ? »). Parcours non-orientĂ© ; retourne la liste ordonnĂ©e des pages saut par saut, ou un chemin null explicite si elles ne sont pas connectĂ©es (pas une erreur). Ălargir `nodeTypes` (ex. `["article","entity","topic"]`) pour des chemins « par concept partagĂ© ». Read-only. |
| `find_boundary_pages` | Classe les pages « frontiĂšre » du wiki â les carrefours vers lesquels tout le monde pointe et qui restent maigres â depuis le graphe persistĂ©. Score = liens entrants amortis par la longueur (`inbound / (1 + mots/100)` : poids plein sur une page vide, moitiĂ© Ă 100 mots, un dixiĂšme Ă 900), Ă1 Ă Ă2 selon l'anciennetĂ© ; mĂȘme graphe â mĂȘme classement (la rĂ©cence se mesure contre l'horodatage du graphe, pas contre l'horloge). Les pages typĂ©es `redirect`/`source`/`answer` sont Ă©cartĂ©es par dĂ©faut, et le nombre Ă©cartĂ© est rapportĂ©. Le score PROPOSE L'ATTENTION, il n'Ă©tablit pas l'importance â les pages d'index et de hub remontent lĂ©gitimement en tĂȘte. Refuse sur un graphe antĂ©rieur Ă la fonctionnalitĂ© plutĂŽt que de compter toutes les pages comme vides. Read-only. |
| `find_twin_pages` | RepĂšre les pages QUASI-JUMELLES â les paires si proches que le vault a probablement Ă©crit deux fois le mĂȘme sujet, rĂ©partissant liens et mises Ă jour entre deux pages incomplĂštes. Compare par cosinus les vecteurs par page que Smart Connections stocke dĂ©jĂ sur disque (`.smart-env/multi/`), chaque page contre chaque autre. LE SEUIL EST DĂRIVĂ DE LA DISTRIBUTION PROPRE AU VAULT et affichĂ© avec la rĂ©ponse â un seuil cosinus fixe ne se transfĂšre pas (mesurĂ© : 0,95 sĂ©lectionne 93 paires sur un vault, 398 sur un autre). EntrĂ©es d'index pĂ©rimĂ©es, projections gĂ©nĂ©rĂ©es (`index.md`/`log.md`) et pages `redirect`/`source`/`answer` sont Ă©cartĂ©es, chaque compte Ă©tant rapportĂ©. Une paire PROPOSE UNE LECTURE, jamais une fusion ; chaque ligne porte les indices (mĂȘme dossier, mĂȘme basename, liens communs, dĂ©jĂ liĂ©es) qui permettent de l'Ă©carter. Sans embeddings la rĂ©ponse est `available: false` avec un motif ET SANS clĂ© `pairs` â dĂ©libĂ©rĂ©ment PAS la mĂȘme rĂ©ponse que `found: 0`. Marche aussi sur les vaults distants (leur bridge doit ĂȘtre â„ 0.9.0, qui sert le magasin de vecteurs via `GET /smart-env/sources` ; un bridge plus ancien rĂ©pond `bridge-route-absent`). Read-only. |
| `filter_relevant_blocks` | 2á” passe de pertinence BM25 sur du markdown que tu as DĂJĂ (aucun fetch, aucun LLM, dĂ©terministe). Ăcarte les blocs hors-sujet vis-Ă -vis d'une `query` â une ingestion sait *pourquoi* elle a rĂ©cupĂ©rĂ© une page, donc elle peut retirer intros/bios/digressions avant la synthĂšse. Frontmatter et titres toujours conservĂ©s ; un bloc de code suit la pertinence de la prose qui l'introduit. Garde-fous : requĂȘte vide â no-op strict ; < 4 blocs scorables â intact ; filtrerait > 70 % â renvoie l'original intact. RĂ©utilise le tokeniseur + l'IDF du router. Read-only. EmpruntĂ© Ă [Crawl4AI](https://github.com/unclecode/crawl4ai) (W-A). |
Voir [ROADMAP.md](./ROADMAP.md) pour la suite.
### Exemples d'usage
Une fois le router enregistrĂ© dans Claude, tu prompteras Claude en langage naturel et il choisira le bon outil. Les payloads ci-dessous montrent les arguments JSON que chaque outil accepte â utile pour Ă©crire des workflows custom ou pour vĂ©rifier ce que Claude a rĂ©ellement appelĂ©.
#### DĂ©couverte â Ă appeler au dĂ©but de chaque session
```jsonc
// list_vaults â pas d'argument. Retourne chaque vault avec online/latency/missingApiKey.
{}
```
```jsonc
// list_files â explorer un rĂ©pertoire.
{ "vault": "tradingview", "directory": "Sessions" }
// Ou la racine si tu omets directory :
{ "vault": "tradingview" }
```
#### Lecture
```jsonc
// get_file â contenu markdown complet + frontmatter en texte brut.
{ "vault": "tradingview", "path": "Sessions/2026-04-29.md" }
```
```jsonc
// search â recherche substring avec contexte.
{ "vault": "tradingview", "query": "AL2SI", "contextLength": 80 }
// Fan-out cross-vaults :
{ "vault": "*", "query": "money management" }
```
```jsonc
// search_smart â similaritĂ© sĂ©mantique (embeddings Smart Connections).
// Retourne des chunks avec scores cosinus et breadcrumbs.
{
"vault": "tradingview",
"query": "rĂšgles de breakeven et trailing stop",
"folders": ["Formations", "Indicators"],
"excludeFolders": [".trash"],
"limit": 10
}
// Fan-out sémantique cross-vaults :
{ "vault": "*", "query": "qu'est-ce que j'ai appris cette semaine ?" }
```
##### FraĂźcheur â quand un rĂ©sultat sĂ©mantique est plus vieux que la page qu'il nomme
Smart Connections calcule le vecteur d'une note Ă son propre rythme. Une note
éditée ensuite répond encore avec son vecteur **précédent**, et jusqu'à la
v0.83.0 rien ne le disait : un résultat périmé et un résultat à jour arrivaient
identiques.
Sur le tier sémantique, `search_smart` renvoie désormais un bloc `freshness`, et
`get_wiki_context_pack` annote chaque chunk et lĂšve
`semantic-results-possibly-stale`. Un verdict par page :
| Verdict | Signification |
|---|---|
| `fresh` | Aucune preuve d'écart avec ce qui a été indexé. |
| `changed` | Il y a Ă©cart â taille en octets diffĂ©rente (preuve), ou mtime dĂ©placĂ©. `sizeEvidence` dit lequel. |
| `touched` | Le mtime a bougĂ© mais la taille est **prouvĂ©e identique** â Ă©dition de mĂȘme longueur, ou client de synchro qui touche l'horloge. RapportĂ© Ă part, car la preuve est plus faible. |
| `page-missing` | La page nommée par ce résultat n'est plus sur le disque. |
| `not-indexed` | Aucun enregistrement de store pour elle. |
| `unknown` | On n'a pas pu savoir â toujours avec une `reason`. |
La comparaison porte sur le mtime et la taille de la note face Ă ceux que Smart
Connections a enregistrĂ©s **Ă l'import** (`last_import`) : c'est du comparable Ă
comparable, pas une heuristique. Elle lit le store `.smart-env` local, donc ne
fonctionne que sur un vault dont cette machine a le disque : un vault distant
rĂ©pond `checkable: false` avec une `reason` et **aucun avertissement** â jamais
de faux positif. Le bloc dit toujours s'il a regardé, parce que « pas
d'avertissement » et « rien à vérifier » sont deux faits différents.
##### Les journaux de session sont exclus par défaut
Sans `excludeFolders`, la recherche sémantique laisse de cÎté
`wiki-meta/Sessions` â les journaux chronologiques que la convention
`log-discipline` range là . Ce dossier représente **41,6 % des pages indexées du
parc** (1212 sur 2915 ; 498 sur 803 pour le vault du routeur lui-mĂȘme), c'est du
log brut par construction, et aucun chemin de navigation (hot â catalog â page)
n'y passe.
Le défaut est **mesuré, pas deviné** : `.trash` et `Templates` n'existent sur
aucun des 23 vaults, et `wiki-meta/graph`, `wiki-meta/digests`,
`wiki-meta/presence` ne portent rien que l'index contienne â aucun des quatre
n'est livré. Un défaut qui n'exclut rien est pire que pas de défaut : il se lit
comme une protection.
Parce que la coupe est grosse, elle n'est jamais silencieuse. Chaque réponse
porte `folderExclusion` â les dossiers, `chosenBy` (`caller` ou `default`),
`excludedHits` â et si la page revient quand mĂȘme courte, `shortPage` le dit au
lieu de la laisser paraĂźtre pleine. Passez `excludeFolders` explicitement pour
remplacer le défaut, `excludeFolders: []` pour n'exclure rien, ou réglez
`OBSIDIAN_ROUTER_DEFAULT_EXCLUDE_FOLDERS` (séparé par virgules ; vide = désactivé)
pour un vault dont les conventions diffĂšrent. Le tier BM25 applique la mĂȘme
exclusion : un repli ne fait jamais remonter ce que le tier remplacé cachait.
##### `webpage_to_markdown` â les liens inline en notes de bas de page
Avec `citations: true`, les liens inline d'une page capturée sortent de la prose
et deviennent des notes numĂ©rotĂ©es, avec une liste `## References` Ă la fin â
une note par **destination**, numérotée par premiÚre apparition, en démarrant
au-dessus des notes que la page utilise déjà . Laissés tranquilles : les liens
dans du code ou un commentaire HTML, les images, les wikilinks, et les cibles
non-http (une ancre `#section` est de la navigation, pas une citation). Sans le
drapeau, la sortie est **identique Ă l'octet** prĂšs.
Combiné à `relevanceQuery`, le filtre passe **d'abord** : marqueurs et
définitions se correspondent alors un pour un, sans référence orpheline vers un
bloc que le lecteur ne voit plus.
##### `get_wiki_context_pack` â la provenance sur chaque Ă©lĂ©ment
Chaque entrée du pack porte désormais `source` : `index` (classée depuis
`wiki-meta/catalog.md`), `graph` (un wikilink d'une page effectivement lue) ou
`semantic` (un chunk Smart Connections). L'enveloppe déclare le vocabulaire
fermĂ© dans `provenance` et dit quelle moitiĂ© fait autoritĂ© â la navigation est
primaire, le sémantique est une augmentation. Quand la moitié navigationnelle
revient **vide** alors que des chunks sémantiques existent, le pack lÚve
`answer-relies-on-semantic-only` : cette réponse n'a aucun ancrage de
navigation et ne doit pas ĂȘtre l'unique support d'une affirmation factuelle.
#### Ăcriture
```jsonc
// write_file â crĂ©e ou remplace.
{
"vault": "tradingview",
"path": "Trades/2026-05-02 - GLE Long.md",
"content": "---\nstatus: open\nticker: GLE\n---\n\n# GLE Long\n\nEntrée: ..."
}
// Refuser l'écrasement si le fichier existe :
{ "vault": "tradingview", "path": "...", "content": "...", "ifNew": true }
```
```jsonc
// append_to_file â utile pour journaux/logs.
{
"vault": "tradingview",
"path": "Sessions/2026-05-02.md",
"content": "\n## 14:32 â TSLA breakout invalidĂ©\n\nStop touchĂ© Ă 178.40\n"
}
```
```jsonc
// patch_file â Ă©dit chirurgicale, pas de réécriture intĂ©grale.
// Insertion sous un heading (chemin complet avec délimiteur ::) :
{
"vault": "tradingview",
"path": "Sessions/2026-05-02.md",
"operation": "append",
"targetType": "heading",
"target": "Session 2026-05-02::Trades du jour",
"content": "- TSLA: stop touché -1.2%\n"
}
// Modifier une seule clé de frontmatter :
{
"vault": "tradingview",
"path": "Trades/2026-05-02 - GLE Long.md",
"operation": "replace",
"targetType": "frontmatter",
"target": "status",
"content": "closed"
}
// Remplacer un bloc par id :
{
"vault": "tradingview",
"path": "Indicators/ATP/notes.md",
"operation": "replace",
"targetType": "block",
"target": "atp-config",
"content": "Config mise Ă jour pour v2.3"
}
```
```jsonc
// delete_file â protĂ©gĂ©. confirm: true obligatoire.
{ "vault": "tradingview", "path": "_scratch/old.md", "confirm": true }
```
#### Templater
```jsonc
// execute_template â rend et sauvegarde optionnellement.
// Le template doit exister dans le vault. Les arguments sont accessibles
// dans le template via tp.mcpTools.prompt("clĂ©") â note : directement sous
// tp, PAS sous tp.user.
{
"vault": "tradingview",
"name": "Templates/Trade.md",
"arguments": {
"ticker": "AAPL",
"direction": "long",
"entry": "175.20",
"stop": "172.50"
},
"createFile": true,
"targetPath": "Trades/2026-05-02 - AAPL Long.md"
}
// Rendu seul (preview), sans sauvegarder :
{
"vault": "tradingview",
"name": "Templates/Trade.md",
"arguments": { "ticker": "AAPL" }
}
```
### TLS
Le plugin Local REST API génÚre un certificat auto-signé par défaut. Pour les vaults localhost, mets `tlsInsecure: true` (c'est le défaut pour les vaults chargés depuis `portRegistry`). Pour les vaults distants derriÚre un vrai certificat TLS (par exemple un reverse proxy avec Let's Encrypt), mets `tlsInsecure: false`.
### Licence
Apache 2.0 â voir [LICENSE](./LICENSE) et [NOTICE](./NOTICE). Aucune restriction d'usage.
This server cannot be deployed
Maintenance
ActivityActive
ResponsivenessNo issues