# Technical Documentation: AgriciDaniel/claude-obsidian

> ℹ️ **Provenance:** Multi-Tier Ingestion · [CodeWiki Reference](https://codewiki.google/github.com/AgriciDaniel/claude-obsidian)

### Overview (`README.md`)

<img src="assets/readme/hero.svg" width="1200" height="420" alt="claude-obsidian, an AI second brain for Obsidian: files from inbox/, a pasted note and approved web research become linked notes in one safe transaction, a constellation of knowledge grows, and an answer is saved as a new note." />

<h1 align="center">claude-obsidian</h1>

<p align="center">
  <b>Your AI second brain for <a href="https://obsidian.md">Obsidian</a>, built for <a href="https://claude.com/product/claude-code">Claude Code</a>.</b><br />
  Drop in sources. Get a connected wiki that grows every time you use it.
</p>

<p align="center">
  <a href="#install">Install</a> ·
  <a href="#see-the-vault">See the vault</a> ·
  <a href="#15-skills-one-system">Skills</a> ·
  <a href="#meet-the-crew">Agents</a> ·
  <a href="#nothing-gets-lost">Safety</a> ·
  <a href="#shape-the-vault-to-the-way-you-think">Modes</a> ·
  <a href="#faq">FAQ</a>
</p>

<p align="center">
  <a href="https://github.com/AgriciDaniel/claude-obsidian/actions/workflows/test.yml"><img src="https://github.com/AgriciDaniel/claude-obsidian/actions/workflows/test.yml/badge.svg" alt="Tests" /></a>
  <a href="https://code.claude.com/docs/en/plugins"><img src="https://img.shields.io/badge/Claude%20Code-plugin-7c3aed" alt="Claude Code plugin" /></a>
  <a href="https://agentskills.io"><img src="https://img.shields.io/badge/Agent%20Skills-compatible-2563eb" alt="Agent Skills compatible" /></a>
  <a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT" /></a>
  <a href="CHANGELOG.md"><img src="https://img.shields.io/badge/release-v3.0.1-d97745" alt="Release v3.0.1" /></a>
  <a href="https://github.com/AgriciDaniel/claude-obsidian/stargazers"><img src="https://img.shields.io/github/stars/AgriciDaniel/claude-obsidian?style=flat" alt="GitHub stars" /></a>
  <a href="https://www.skool.com/ai-marketing-hub-pro"><img src="https://img.shields.io/badge/AI%20Marketing%20Hub-Pro%20community-purple" alt="AI Marketing Hub Pro community" /></a>
</p>

<p align="center">
  <a href="https://www.star-history.com/agricidaniel/claude-obsidian">
    <picture>
      <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/badge?repo=AgriciDaniel/claude-obsidian&type=trending&theme=dark" />
      <img src="https://api.star-history.com/badge?repo=AgriciDaniel/claude-obsidian&type=trending" alt="GitHub Trending Repository of the Day" />
    </picture>
  </a>
</p>

**claude-obsidian turns the things you read into a knowledge base that keeps getting more useful.** Drop a paper, an article, a transcript or your own notes into the vault, and Claude reads them, writes linked notes with sources, and files everything into one connected knowledge graph you can explore in Obsidian.

- **Capture.** Put sources in `inbox/`. The originals are kept, untouched, so every summary can point back to where it came from.
- **Connect.** Claude writes linked pages, indexes and maps of content, so new ideas join the graph instead of piling up.
- **Ask.** Get answers from what your vault already knows, with citations, without changing a single file.
- **Save.** Keep the answers worth keeping as notes, one at a time, when you ask.
- **Check.** A health check finds dead links, orphan pages and gaps before they spread.

Everything stays as plain Markdown files on your computer. No cloud database, no lock-in, no telemetry. Apart from the AI tool you chat with, nothing leaves your machine unless you say so.

## See the vault

The result is a normal Obsidian vault that stays useful with or without an AI: browse it in Graph view, or lay it out as a map in Obsidian Canvas.

<p align="center">
  <img src="assets/screenshots/graph-view.png" alt="A claude-obsidian vault in Obsidian Graph view: linked notes grouped into clusters" width="49%" />
  <img src="assets/screenshots/wiki-map-view.png" alt="A claude-obsidian knowledge map in Obsidian Canvas, with hub pages, indexes and linked ideas" width="49%" />
</p>

<p align="center"><sub>Linked notes in Graph view · A knowledge map in Obsidian Canvas</sub></p>

## Install

<img src="assets/readme/install.svg" width="1200" height="420" alt="Installing claude-obsidian: a terminal adds the marketplace and installs the plugin, an inventory fills with all 15 skills and the 3 agents march in. /claude-obsidian:wiki previews the new vault first and creates it only after you approve. PRESS START." />

You need [Claude Code](https://claude.com/product/claude-code), Python 3.11 or newer and, for the visual side, [Obsidian](https://obsidian.md). Install the plugin with two commands:

```bash
claude plugin marketplace add AgriciDaniel/claude-obsidian
claude plugin install claude-obsidian@agricidaniel-claude-obsidian
```

Then open the folder you want as your vault and start Claude Code there:

```bash
claude                                   # start Claude Code in your vault folder

/claude-obsidian:wiki                    # set up the vault (shows you a preview first)
/claude-obsidian:wiki-ingest             # turn what is in inbox/ into linked notes
/claude-obsidian:wiki-query              # ask a question, answered from your notes
/claude-obsidian:save                    # keep a good answer as a note
/claude-obsidian:wiki-lint               # health check: dead links, orphans, gaps
```

Open the same folder in Obsidian with **Open folder as vault** to browse your notes as they grow.

Already have an Obsidian vault? `/claude-obsidian:wiki` can adopt it without moving your notes. The [installation guide](docs/install-guide.md#adopt-an-existing-vault) walks through it.

<details>
<summary><b>Other AI tools: Codex, OpenCode, Gemini, ZCode, Cursor and Windsurf</b></summary>
<br />

claude-obsidian is a set of [Agent Skills](https://agentskills.io), so it also runs in other hosts. From a copy of this repo, preview the setup, then apply it:

```bash
git clone https://github.com/AgriciDaniel/claude-obsidian.git
cd claude-obsidian
bash scripts/setup-multi-agent.sh --host codex            # preview, writes nothing
bash scripts/setup-multi-agent.sh --host codex --apply    # link the skills
```

Use `--host opencode`, `--host gemini` or `--host zcode` the same way. Cursor and Windsurf need a workspace: `--host cursor --host windsurf --workspace <folder> --apply`. Every host is covered in the [installation guide](docs/install-guide.md).

</details>

<details>
<summary><b>Windows</b></summary>
<br />

On Windows, use WSL for anything that writes to the vault. Native Windows can inspect and preview, and stops with a clear error instead of writing. The [Windows and WSL guide](docs/windows-wsl.md) has the details.

</details>

<details>
<summary><b>Upgrading from 2.x or 3.0.0</b></summary>
<br />

```bash
claude plugin marketplace update agricidaniel-claude-obsidian
claude plugin update claude-obsidian@agricidaniel-claude-obsidian
```

Version 3.0.0 alone used the plugin name `agricidaniel-obsidian`. If you installed that one, run the marketplace update, then `claude plugin install claude-obsidian@agricidaniel-claude-obsidian`, and restart Claude Code. See the [namespace migration](docs/namespace-migration.md).

</details>

## 15 skills, one system

<img src="assets/readme/skills.svg" width="1200" height="420" alt="The 15 claude-obsidian skills as planets orbiting the vault crystal: 5 core workflows, 7 extensions and 3 reference skills. The crew visits each one by saucer while the skill log ticks to 15 of 15." />

Talk to it in plain words, or call any skill by name with `/claude-obsidian:<skill>`. Open a group to see what is inside.

<details open>
<summary><b>Build and use the wiki</b> (5)</summary>
<br />

| Skill | What it does |
|---|---|
| `wiki` | Sets up a new vault or adopts yours, checks it is ready and routes your request |
| `wiki-ingest` | Turns captured sources into linked pages with their sources recorded |
| `wiki-query` | Answers from what is already in the vault, read-only |
| `save` | Keeps one answer or insight as a note, only when you ask |
| `wiki-lint` | Finds dead links, orphan pages, missing metadata and empty sections |

</details>

<details>
<summary><b>Extend the workflow</b> (7)</summary>
<br />

| Skill | What it does |
|---|---|
| `autoresearch` | Bounded web research, only with your consent, merged after review |
| `canvas` | Creates and updates Obsidian Canvas boards |
| `defuddle` | Plans a clean Markdown capture of a web page; the cleaner itself is an optional external tool |
| `wiki-fold` | Rolls up the operation log into short, traceable summaries |
| `wiki-mode` | Sets how new notes are filed: Generic, LYT, PARA or Zettelkasten |
| `wiki-retrieve` | Local BM25 search with optional reranking |
| `wiki-cli` | Reads and searches through the official Obsidian command line |

</details>

<details>
<summary><b>Reference</b> (3)</summary>
<br />

| Skill | What it does |
|---|---|
| `obsidian-markdown` | Correct Obsidian Markdown: links, embeds, callouts and properties |
| `obsidian-bases` | Native .base tables, cards, filters and formulas |
| `think` | A structured review loop for hard decisions |

</details>

## Meet the crew

<img src="assets/readme/crew.svg" width="1200" height="420" alt="Meet the crew: the three read-only agents on a turntable. wiki-ingest reads one source and drafts pages, wiki-lint runs the vault health check, and the verifier tests a proposed change. Only the transaction core writes to the vault." />

Behind the skills, three agents do the heavy reading and checking. All three are **read-only**: they hand back drafts and reports, and only one place, the transaction core, ever writes to your vault.

| Agent | What it does |
|---|---|
| `wiki-ingest` | Reads one captured source and hands back page drafts, with the hash it expects each target to have |
| `wiki-lint` | Runs the vault linter, double checks surprising findings and returns a health report |
| `verifier` | Runs safe tests on a proposed change or release and ranks what it finds |

**Built for:**

- **Researchers and students** who read a lot and want every note to point back to its source.
- **Writers and creators** who want ideas from videos, articles and books to connect instead of getting lost in folders.
- **Builders and teams** who keep project knowledge in Markdown and want an AI that can search it, cite it and keep it tidy.

## Nothing gets lost

<img src="assets/readme/safety.svg" width="1200" height="420" alt="Nothing gets lost: Claude records a hash of each note, plans one change and applies it as one transaction. When the power cuts out mid write, the journal rewinds the vault. When you edit a note during planning, a conflict stops the write instead of overwriting your edit." />

Your notes matter, so every change to the vault works like a careful save:

1. **Read first.** Claude reads every note it will touch and records a fingerprint (a SHA-256 hash) of each one.
2. **One plan, one apply.** Helpers can work in parallel, but they only hand back drafts. Claude merges them into one plan, checks it, and applies it as a single transaction.
3. **Undo if anything breaks.** A journal keeps backups during the write. If the power cuts out halfway, `transaction recover` rolls the unfinished change back from the journal.
4. **Your edits win.** If you change a note in Obsidian while Claude is planning, the fingerprint no longer matches and the write stops with a conflict instead of overwriting you.

Web research, Git checkpoints and anything that sends data off your machine are separate steps that ask you first.

<details>
<summary><b>For the curious: how the vault is chosen and protected</b></summary>
<br />

The plugin never treats its own folder or cache as your vault. A vault is picked explicitly: through `CLAUDE_OBSIDIAN_VAULT`, the nearest `.claude-obsidian.json`, or one clear initialized parent folder. If it is not sure, it exits without writing.

The lock coordinates local writers that cooperate with it. Other devices and external editors do not share it, so keep backups, especially when you sync Canvas or Base files. Read the [transaction contract](skills/wiki/references/operation-transactions.md), the [provenance contract](skills/wiki/references/provenance.md) and the [Compound Vault guide](docs/compound-vault-guide.md) for the full detail.

</details>

## Shape the vault to the way you think

<img src="assets/readme/modes.svg" width="1200" height="420" alt="Four ways to file notes: the same new note lands where Generic, LYT, PARA or Zettelkasten would put it, while existing notes never move." />

`wiki-mode` sets how new notes are filed, the way you already like to work. Switching modes changes where new notes go and never moves your old ones.

| Mode | How notes are filed |
|---|---|
| **Generic** (default) | Sources, concepts, entities and sessions |
| **LYT** | Maps of Content and linked atomic notes |
| **PARA** | Projects, Areas, Resources and Archives |
| **Zettelkasten** | Stable IDs, atomic notes and dense links |

More in the [methodology modes guide](docs/methodology-modes-guide.md).

## How it works

1. **Capture.** Sources go into `inbox/`. claude-obsidian keeps an untouched copy of each one, named by its content, before anything is summarized.
2. **Ground.** Each important claim in a note is linked to the source that supports it. Weak or conflicting evidence stays visible instead of being smoothed over.
3. **Compound.** Ask, research, search and lint use what the vault already knows, so every session starts from your notes instead of from zero.

The idea follows [Andrej Karpathy's LLM Wiki pattern](https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f): let the AI maintain a wiki you can read, instead of a black box you cannot.

<details>
<summary><b>What it can and cannot read today</b></summary>
<br />

| Input or feature | Support today |
|---|---|
| Local files | Captured and kept as content-addressed copies |
| Images | Metadata, hash, size and dimensions when available |
| PDF and EPUB | Metadata, hash and size; no built-in text extraction yet |
| Web pages and YouTube | Prepared as a plan you approve; fetching needs a separate runner that enforces it |
| OCR | Needs an external tool you configure |
| Search (BM25) | Local and deterministic |
| Reranking or remote models | Optional, and only with your explicit consent |
| Obsidian CLI | Optional for reading and search |

</details>

<details>
<summary><b>Command line reference</b></summary>
<br />

From a clone of this repo, the same core is available as a portable command line: `python3 scripts/claude-obsidian.py`. Commands that change files show a plan first and only apply with the plan's hash.

| Command | What it does |
|---|---|
| `doctor --vault PATH` | Shows which vault is selected and whether it is ready |
| `init PATH` | Plans, then creates, a new vault |
| `adopt PATH` | Plans, then adopts, an existing Obsidian vault |
| `lint --vault PATH` | Runs the health check |
| `capture plan` / `capture apply` | Checks, then keeps, copies of local sources |
| `transaction recover --vault PATH` | Restores the vault after an interrupted write |
| `bench --queries FILE --vault PATH` | Measures search quality on your own questions |

Run `python3 scripts/claude-obsidian.py --help`, or add `--help` to any command, to see every option.

</details>

## FAQ

<details>
<summary><b>What is claude-obsidian?</b></summary>
<br />

A free, open-source AI second brain for Obsidian: a personal knowledge management (PKM) and AI note-taking system that builds a knowledge graph of plain Markdown. It is a Claude Code plugin (and a set of Agent Skills for other AI tools) that turns your sources into linked, cited notes, answers questions from them, and keeps the vault healthy over time.

</details>

<details>
<summary><b>Is it free?</b></summary>
<br />

Yes. It is MIT licensed with no subscription and no tracking. You use it with your own Claude Code plan, or with another AI tool you already have.

</details>

<details>
<summary><b>Do I need Obsidian?</b></summary>
<br />

No, but it is the nicest way to see the result. The vault is plain Markdown, so any editor works. Obsidian adds Graph view, Canvas maps and Bases tables on top.

</details>

<details>
<summary><b>Is this an Obsidian community plugin?</b></summary>
<br />

No. You don't install it from Obsidian's community plugin list. It is a Claude Code plugin (and a set of Agent Skills for other AI tools) that works on your vault folder. Obsidian opens the result as a normal vault, and no Obsidian plugin is required.

</details>

<details>
<summary><b>Does my data leave my computer?</b></summary>
<br />

Your vault stays on your computer as ordinary files. When you work with it in Claude Code, the notes Claude reads are sent to Claude, Anthropic's model, the same as anything else you open in Claude Code. Web research and remote models are off until you allow them for a task.

</details>

<details>
<summary><b>Will it mess up my existing notes?</b></summary>
<br />

It is built not to. Adopting a vault does not move your notes, every change is shown as a plan before it is applied, and an interrupted write can be rolled back. If you edit a note while Claude is working on it, the write stops instead of overwriting you. Keep normal backups anyway.

</details>

<details>
<summary><b>How is it different from a chat that saves notes?</b></summary>
<br />

Most AI note tools save text and stop. claude-obsidian keeps the original sources, links every claim back to them, connects new notes to old ones, and checks the vault for broken links and gaps. The knowledge compounds instead of piling up.

</details>

<details>
<summary><b>Does it work with Codex, Gemini or Cursor?</b></summary>
<br />

Yes. The skills follow the open Agent Skills format. Claude Code gets the full plugin; Codex, OpenCode, Gemini, ZCode, Cursor and Windsurf link the same skills with one setup script. See [Install](#install).

</details>

<details>
<summary><b>Good to know: requirements, limits and uninstall</b></summary>
<br />

**Requirements.** Python 3.11 or newer, a current Claude Code release for the plugin (or another Agent Skills host), Obsidian for the visual side, and Bash for the setup scripts. Git is only needed for development or checkpoints.

**Limits.** It is not an automatic transcript recorder, a cloud sync service or a fact checker, and it does not replace backups. PDFs and web pages need extra tools for full text.

**Uninstall.** Removing the plugin never removes your vault. Your notes, sources and Obsidian settings stay yours: `claude plugin uninstall claude-obsidian@agricidaniel-claude-obsidian`.

</details>

## License and contributing

MIT License, see [LICENSE](LICENSE). The design follows [Andrej Karpathy's LLM Wiki pattern](https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f) and uses [kepano/obsidian-skills](https://github.com/kepano/obsidian-skills) as the reference for Obsidian Markdown, Bases and Canvas syntax. See [ATTRIBUTION.md](ATTRIBUTION.md) and [CITATION.cff](CITATION.cff).

Contributions are welcome: read [CONTRIBUTING.md](CONTRIBUTING.md) first and run `make test` before you open a pull request. Report security problems as described in [SECURITY.md](SECURITY.md), and read [PRIVACY.md](PRIVACY.md) for what stays on your machine. Release notes live in the [changelog](CHANGELOG.md), and the [code of conduct](CODE_OF_CONDUCT.md) applies everywhere. AI coding agents working on this repo start at [AGENTS.md](AGENTS.md).

**Thank you** to everyone who has helped. Pull requests: [@Ychris12138](https://github.com/Ychris12138), [@Marcos64s](https://github.com/Marcos64s), [@hphsu](https://github.com/hphsu), [@vinsocci](https://github.com/vinsocci), [@miraclemanza](https://github.com/miraclemanza), [@maartengoet](https://github.com/maartengoet), [@Lion-1209](https://github.com/Lion-1209), [@HectorBernstorff](https://github.com/HectorBernstorff), [@Packetslave](https://github.com/Packetslave), [@CooperSheroy](https://github.com/CooperSheroy), [@yotamleo](https://github.com/yotamleo), [@ketpatil77](https://github.com/ketpatil77), [@tilusnet](https://github.com/tilusnet), [@GPezzuti](https://github.com/GPezzuti), [@STiFLeR7](https://github.com/STiFLeR7). Issue reports: [@RoelKluin](https://github.com/RoelKluin), [@threnard](https://github.com/threnard), [@szrenwei](https://github.com/szrenwei), [@ppuri2](https://github.com/ppuri2), [@cosmos75](https://github.com/cosmos75), [@simonc](https://github.com/simonc).

## Star history

If claude-obsidian helps you, a star helps other people find it.

<p align="center">
  <a href="https://www.star-history.com/#agricidaniel/claude-obsidian&Date">
    <picture>
      <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=agricidaniel/claude-obsidian&type=Date&theme=dark" />
      <source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=agricidaniel/claude-obsidian&type=Date" />
      <img src="https://api.star-history.com/svg?repos=agricidaniel/claude-obsidian&type=Date" alt="Star history chart for claude-obsidian: GitHub stars over time" width="720" />
    </picture>
  </a>
</p>

## Author

Built by **[Agrici Daniel](https://agricidaniel.com/about)**, AI Workflow Architect, with help from the [AI Marketing Hub Pro](https://www.skool.com/ai-marketing-hub-pro) community.

<img src="assets/readme/credits.svg" width="1200" height="420" alt="Thanks for playing: the three bots dance around the crystal while the credits for AgriciDaniel and the community contributors crawl into the stars, then CONTINUE? asks you to star the repo." />

<p align="center">
  <a href="https://www.youtube.com/@AgriciDaniel"><img src="assets/readme/btn-youtube.svg" width="137" height="44" alt="Claude Code tutorials and demos on YouTube" /></a>
  <a href="https://www.skool.com/ai-marketing-hub"><img src="assets/readme/btn-hub.svg" width="210" height="44" alt="AI Marketing Hub, the free community on Skool" /></a>
  <a href="https://www.skool.com/ai-marketing-hub-pro"><img src="assets/readme/btn-pro.svg" width="155" height="44" alt="AI Marketing Hub Pro" /></a>
</p>

---

### Overview (`hooks/README.md`)

# Claude Code hooks

`hooks.json` is a thin Claude Code adapter around the portable core.

Hooks require a current Claude Code release: the exec-form `command` and
`args` hook shape and the `compact` and `fork` `SessionStart` matchers used below
need it
(see the [Claude Code hooks contract](https://code.claude.com/docs/en/hooks)).
Both hooks also need an interpreter reachable as `python3` on `PATH`; see the
[Windows and WSL guide](../docs/windows-wsl.md#claude-code-hooks-and-python3-on-windows)
for native Windows setup.

| Event | Matcher | Behavior |
|---|---|---|
| `SessionStart` | `startup|resume|clear|compact|fork` | Silent by default. With `CLAUDE_OBSIDIAN_SESSION_CONTEXT=1`, resolves a real user vault and emits a bounded, sanitized `wiki/hot.md` data block. A workspace-configured vault outside that project also requires an exact `CLAUDE_OBSIDIAN_SESSION_CONTEXT_VAULT` path. |
| `Stop` | unsupported/omitted | Emits a bounded, aggregate JSON `systemMessage` when recovery is needed. It omits operation identifiers, paths, and note content; otherwise it is silent. |

Both are `command` hooks using an executable plus an argument array and
`${CLAUDE_PLUGIN_ROOT}` only to locate plugin code. They do not use the plugin
cache as a vault.

The response shapes follow the current [Claude Code hooks contract](https://code.claude.com/docs/en/hooks):
An opted-in SessionStart may add context through stdout, while Stop warnings use the
supported top-level `systemMessage` field. Stop has no matcher because that
event does not support one. Both readers reject symlinked paths and cap their
scan count and output bytes.

Hooks never write knowledge, update `wiki/hot.md`, stage files, commit Git, run
remote calls, or bypass transaction approval. The same workflows remain usable
on hosts that do not support Claude hooks.

---

### Capacity And Recovery (`docs/capacity-and-recovery.md`)

# Capacity and recovery

Inspect the selected vault before a large operation:

```bash
python3 "$CORE" capacity --vault "$VAULT"
```

This reads status without changing the vault. Keep ledger bytes, raw archive
bytes, page count, and retrieval chunk count distinct when measuring capacity.
No page-count promise or constant latency follows from a passing small fixture.
Benchmark representative small, medium, and large copies on the target machine;
retain elapsed time, memory, index size, cancellation outcome, and recovery time.

Canonical source and claim ledgers must fit the 16 MiB reader/writer limit.
Transactions preflight them and reject growth with `LEDGER_CAPACITY_EXCEEDED`.
Do not remove evidence, lower fidelity, or disable validation to get under the
limit. A formatting-heavy legacy ledger may be compacted losslessly:

```bash
python3 "$CORE" capacity compact --vault "$VAULT" \
  --operation-id capacity-reviewed
# Set APPROVAL_SHA256 to the reviewed approved_plan_sha256.
python3 "$CORE" capacity compact --vault "$VAULT" \
  --operation-id capacity-reviewed \
  --approved-plan-sha256 "$APPROVAL_SHA256" --apply
```

Keep a complete recoverable vault backup before repair. Review the operation's
paths and hashes. This repair reads legacy inputs only up to 64 MiB and changes
JSON formatting, preserving every record and source identity. It rewrites only
ledgers that `capacity` reports as `near_capacity` (at least 85% of the limit)
or `over_capacity`; smaller ledgers are left byte-for-byte unchanged and a vault
with none is a `noop`. If the compact canonical representation still exceeds
16 MiB, it fails with `LEDGER_MIGRATION_REQUIRED`, refusing early when even
removing all whitespace could not fit. Stop and retain the data; automated
sharding and record eviction are not implemented. An arbitrary size increase
would need reader, writer, journal, memory, and rollback evidence together.

The compaction is an ordinary transaction, so inspection runs the same semantic
provenance validation as any ledger write. If the vault drifted out of band,
for example a `.raw/` source edited after ingest so its recorded
`content_sha256` no longer matches, inspection refuses the plan with
`INVALID_PROVENANCE_LEDGER` even though only formatting would change. Do not
bypass validation. Recover through a normal reviewed transaction that writes a
corrected ledger in compact form (one JSON line with no insignificant
whitespace), with the expected hash of the current ledger and an approved plan.

Writers determine how long the savings last. `migration_bundle` (`migrate`)
now writes canonical ledgers in that compact form. The `init`/`adopt` planner
in `claude_obsidian/vault_ops.py` and hand-written bundles may still write
indented JSON; that is valid, and compaction becomes relevant only once such a
ledger approaches the limit.

One content/predecessor file is bounded at 64 MiB; one operation's new content or
recovery backups at 128 MiB; writes at 1,024. These storage ceilings are distinct
from stricter ledger limits and from practical operating capacity. Split large
work at a logical boundary and inspect each bounded operation. Never split a
single coupled workflow into unreviewed per-file applies.

For an interrupted operation, stop competing writers and retain the selected
vault with its `.vault-meta/` journals and backups. Then use:

```bash
python3 "$CORE" transaction recover --vault "$VAULT"
```

Recovery may roll back an incomplete apply. Do not delete journals to hide an
error or overwrite changed files by hand. Exit 75 requires a fresh read and plan.
If an existing replacement target was deleted after an interrupted apply,
recovery leaves it absent, retains the journal as `rollback-failed`, and returns
`ROLLBACK_FAILED`. Review the external deletion and retained backup before
authorizing a separate restoration; recovery does not silently recreate it.
Use `--force-stale-lock` only after independently establishing that no writer
is active. The capture queue has a separate explicit recovery interface; it is
not a way to bypass the transaction lock. See
[coordination.md](../skills/wiki/references/coordination.md) for external edits
and multi-machine limits.

---

### Compound Vault Guide (`docs/compound-vault-guide.md`)

# Compound Vault architecture

The Compound Vault is claude-obsidian's operational model for a local,
source-cited knowledge base. The 2.0.0 foundation replaces historical
per-file-lock and lifecycle-write behavior with explicit vault selection and
recoverable operation transactions.

## Roles

| Role | Contains | Mutation policy |
|---|---|---|
| Product package | Skills, Python core, scripts, hooks, templates | Changed by product development only |
| User vault | Notes, sources, ledgers, Obsidian settings | Changed by reviewed vault operations |
| Derived runtime | Locks, journals, retrieval indexes, capture queue | Vault-local, ignored, recoverable/regenerable |
| Public artifact | Allowlisted product files and synthetic samples | Rebuilt from a clean Git snapshot |

An installed plugin cache is always product code. It never becomes the user
vault. Resolve vault state through `--vault`, `CLAUDE_OBSIDIAN_VAULT`, workspace
config, or current-directory discovery.

The development checkout and public marketplace root are distinct roles even
when they share history. Development source stores the reviewed marketplace
template under `config/`; the release builder injects it at
`.claude-plugin/marketplace.json` only after selecting and auditing the clean
artifact. Public default-branch promotion uses the extracted artifact and
requires owner approval.

## Knowledge layers

- `inbox/` is visible, user-controlled intake. Capture never deletes it.
- `.raw/` stores immutable payloads and the legacy ingestion manifest.
- `wiki/` stores generated pages, index, log, hot cache, and evidence ledgers.
- `.vault-meta/` stores runtime locks, transaction journals, queues, indexes,
  and optional configuration.

New vaults include a `.gitignore` that excludes `.vault-meta/` in full because
journals, queues, and derived chunks may contain note text, local paths, or
source URLs. Existing vaults should adopt the same default deliberately without
overwriting their current ignore policy.

`wiki/hot.md` is a bounded context cache, not a transcript. `wiki/log.md`
records completed operations. Source and claim ledgers keep provenance separate
from legacy delta tracking.

## One operation, one transaction

A save, ingest, fold, canvas update, research merge, or approved lint repair is
one `claude-obsidian.transaction.v1` bundle:

1. Read every target and record its current SHA-256 or expected absence.
2. Let parallel workers produce drafts and evidence without shared writes.
3. Merge the full logical change into one bundle.
4. Inspect it without mutation.
5. Obtain consent for destructive, external, or canonical-merge consequences.
6. Apply once under the vault-wide process-lifetime lock.
7. Report the operation ID and exact changed paths.

The engine journals originals, verifies content hashes, uses fsync and atomic
replacement, and restores prior state after a failed or interrupted apply.
Concurrent target changes return conflict exit 75. Re-read and build a new
operation; never force an old plan through.

One transaction content or predecessor file is limited to 64 MiB; aggregate
new content and aggregate recovery backups are each limited to 128 MiB. A
transaction has at most 1,024 writes and preflights its journal/result against
recovery's 8 MiB reader cap. The capture queue uses the same limit for reads and
writes (8 MiB canonical JSON, at most 4,096 entries), preserving recoverability
under bounded resources.

Mutation safety is bound to open directory descriptors, not later pathname
lookups. The vault root, metadata runtime, transaction operation, backups,
capture queue, checkpoint state, and retrieval publications remain pinned for
their complete mutation window. Concurrent alias replacement therefore fails
closed or rolls back without touching a replacement tree. These guarantees
require WSL/Linux or supported macOS; native Windows and Git Bash are not
supported mutation hosts and are refused with `UNSUPPORTED_PLATFORM` before
any side effect. Read-only inspection and dry-runs run natively on Windows
using point-in-time lstat validation (symlink and junction rejection) instead
of descriptor pinning. Platform specifics and WSL troubleshooting live in the
[Windows and WSL guide](windows-wsl.md).

Raw source bytes can be supplied through hash-checked binary `content_file`
writes, but their destination mode is create-only. Address allocation, page
frontmatter, address map, source manifest, index, log, and hot cache can move in
the same journaled operation.

Read [the transaction reference](../skills/wiki/references/operation-transactions.md).

## Read paths and transport

Filesystem reads are the portable baseline. `scripts/detect-transport.sh`
reports the official Obsidian CLI as ready only when the command, application,
and required capability work. Detection defaults to read-only `--peek`.

Obsidian CLI may accelerate reads and search. It is not a second mutation path:
all writes still go through the transaction core. This avoids races between
Obsidian, agent tools, and parallel workers.

## Retrieval

The optional retrieval pipeline stores contextual chunks and BM25 state below
`.vault-meta/`. Provisioning is preview-first:

```bash
bash scripts/setup-retrieve.sh --vault <vault> --no-llm
bash scripts/setup-retrieve.sh --vault <vault> --no-llm --apply
```

BM25 is deterministic and local. Optional cosine reranking defaults to the
multilingual `nomic-embed-text-v2-moe` Ollama model, which is approximately
958 MB and is never downloaded automatically. Nomic embeddings use distinct
`search_query:` and `search_document:` task prefixes. Users who already have
the smaller English-oriented v1.5 model can select it explicitly with
`--model nomic-embed-text`; multilingual vaults should retain the v2 default.
Nomic v2 has a 512-token input context, and Ollama truncates longer embedding
inputs by default; BM25 still scores the complete chunk. If the selected model
is missing, local-only model proof fails, or an embedding stage fails, results
fall back completely to BM25 order and scores. Proof requires cloud-disabled
server status, a valid installed digest, and nonempty local model metadata
before query, chunk, or cache work. Deleted, changed, or escaping chunk records
are rejected, and page deduplication happens before the final top-K.

Retrieval queries are bounded at 8,000 normalized characters and result counts
at 1 through 1,000. Oversized queries and invalid limits fail explicitly. An
untagged Ollama model request matches only the corresponding untagged name or
its `:latest` alias; other installed tags must be selected by their exact name.
Embedding cache entries are scoped to the verified model digest.

Contextual-prefix model calls require `--allow-egress`. Remote Ollama requires
`--allow-remote-ollama`. Neither is inferred from an environment variable
alone.

## Evidence

The source ledger records content identity, authority, independence, freshness,
review state, and linked pages. The claim ledger records falsifiable claims,
support, contradictions, confidence, risk, and assessment.

- Accepted claims need current active non-synthetic support.
- High-risk accepted claims need two independent sources.
- Sources with one independence key count as one line of evidence.
- RFC-equivalent URL spellings count as one origin, including normalized IPv6,
  IDN, Unicode/UTF-8 percent encoding, and default ports.
- Contradictions remain visible.
- Unsupported is a valid state; invented citations are not.

Read [the provenance reference](../skills/wiki/references/provenance.md).

## Hooks

Claude's SessionStart hook is silent by default. It emits bounded, sanitized hot
context only when a user vault resolves and the user has explicitly exported
`CLAUDE_OBSIDIAN_SESSION_CONTEXT=1`; hook stdout becomes model context, so this
opt-in is an egress decision. The Stop hook reports incomplete transactions.
Hooks do not update notes, capture conversations, stage files, or commit Git.
Hosts without hooks use the same core workflows.

## Capture

Filesystem byte capture is the only internally implemented adapter. It performs
a full read-only preflight, then the CLI applies all new content-addressed raw
copies as one binary-capable transaction. Image, PDF, and EPUB format metadata
is local; semantic extraction is not built in. URL, YouTube, and OCR actions are
inert consent plans for a separately configured runner.

## Maintenance and recovery

```bash
python3 scripts/claude-obsidian.py doctor --vault <vault>
python3 scripts/claude-obsidian.py lint --vault <vault> --as-of YYYY-MM-DD
python3 scripts/claude-obsidian.py transaction recover --vault <vault>
```

Lint is deterministic for its declared UTC audit date and read-only. Any fix is a separate reviewed
transaction. Git checkpointing is also separate and explicit; it verifies exact
blob bytes, refuses pre-existing staged content, advances history by
compare-and-swap, and excludes raw bytes unless the user opts in.
If a stale lock remains after a crash, first verify no writer is alive; only
then use `transaction recover --force-stale-lock`. Never automate the override.

## Upgrade path

Existing layouts stay readable. Preview the additive migration before applying:

```bash
python3 scripts/claude-obsidian.py migrate --vault <vault> \
  --generated-at <ISO-UTC> --operation-id migrate-reviewed
python3 scripts/claude-obsidian.py migrate --vault <vault> \
  --generated-at <ISO-UTC> --operation-id migrate-reviewed \
  --approved-plan-sha256 <reviewed-sha256> --apply
```

Migration adds missing workspace/provenance state, preserves the legacy manifest
byte-for-byte, and does not infer claims from prose. The deprecated
`scripts/wiki-lock.sh` remains for v1 compatibility only and must not be used by
new workflows.

---

### Dragonscale Guide (`docs/dragonscale-guide.md`)

# DragonScale compatibility extension

DragonScale is an optional set of memory-maintenance experiments. The base
vault does not require it. The extension consists of four independent ideas;
their maturity and dependencies differ.

## Provisioning

Preview the create-only setup transaction:

```bash
bash scripts/setup-dragonscale.sh --vault <vault>
```

Apply only after reviewing the missing paths:

```bash
bash scripts/setup-dragonscale.sh --vault <vault> \
  --generated-at <ISO-UTC> --approved-plan-sha256 <reviewed-sha256> --apply
```

Setup creates only missing address-counter, tiling-threshold, legacy-exception,
and legacy-manifest files. It never replaces existing values, downloads a
model, changes product scripts, probes a service, or modifies notes.

## 1. Extractive log folds

`wiki-fold` creates a bounded rollup from recent `wiki/log.md` entries. A fold
must be:

- extractive: every statement points to a child entry;
- additive: children remain unchanged;
- structurally idempotent: the same input set produces the same fold identity;
- previewed before one transaction applies the fold, index, and log update.

Folds are navigation aids, not new evidence. Delete or revert only the fold
operation if a rollup is unhelpful.

## 2. Stable addresses

Modern operations request addresses through the main transaction bundle. The
engine allocates the counter, injects page frontmatter, and updates the legacy
address map inside the same journal. Do not call the legacy allocator for new
ingestion workflows.

`scripts/allocate-address.sh` remains a read-only compatibility diagnostic for
older vaults. It accepts an explicit vault and never creates or acquires a lock.
Its output does not replace the transaction engine's expected-hash, allocation,
concurrency, or rollback guarantees.

## 3. Semantic tiling diagnostics

`scripts/tiling-check.py` embeds eligible pages through an Ollama model and
compares every distinct page pair for possible duplication. It is optional and
its seed thresholds are uncalibrated. A score is a review signal, not proof
that two pages should be merged.

Preview without writing a report:

```bash
python3 scripts/tiling-check.py --vault <vault> --peek
```

Remote Ollama hosts are refused unless `--allow-remote-ollama` is supplied.
That flag approves only the endpoint. Every server must still prove cloud is
disabled and the selected model is installed locally with a valid digest and
nonempty metadata before any page or cache data is read. An untagged model
matches only its exact name or `:latest`; other tags require exact selection.
Calibrate thresholds on labeled examples from the selected vault before using
them as policy. If Ollama or the model is absent, report that capability state;
do not imply the check ran.

Tiling cache schema version 2 binds embeddings to the resolved model identity
and digest. A valid version 1 cache is a miss. A malformed cache still exits 3.
An unproved local state, or any incomplete embedding run, exits 12. Incomplete
runs print a provisional report and preserve the previous cache bytes. A
complete scan with no eligible pages reports `complete-empty` and succeeds.

## 4. Boundary scoring

`scripts/boundary-score.py` ranks pages that may deserve more research using
current graph structure and recency. It is deterministic and read-only:

```bash
python3 scripts/boundary-score.py --vault <vault> --json --top 10
```

The score proposes attention; it does not establish importance or truth. Use a
candidate as an autoresearch topic only after reviewing its underlying links
and the research budget.

## Failure and recovery

- A setup conflict means an expected path changed; generate a new plan.
- An interrupted setup uses the normal transaction recovery command.
- A corrupt legacy counter should be rebuilt from observed addresses only after
  checking duplicates and the address map.
- A tiling cache or BM25 index is derived and may be regenerated. Source pages
  and provenance ledgers are not derived caches.
- Disabling DragonScale requires no bulk rewrite. Stop invoking the optional
  helpers; existing fold pages and addresses remain normal vault content.

## Verification

Run the relevant hermetic tests or the full suite:

```bash
make test
```

Capability maturity is based on executed checks, not the presence of a setup
file. Keep each mechanism optional and fail closed when its prerequisites are
unavailable.

---

### Install Guide (`docs/install-guide.md`)

# Install claude-obsidian

claude-obsidian has two independent parts:

1. the product package (skills, portable core, Claude adapter, and templates);
2. a user-owned Obsidian vault containing mutable knowledge.

Do not use an installed plugin cache as the vault. A source clone is suitable
for development, but a normal user vault should be a separate directory.

## Requirements

- Python 3.11 or newer
- an Agent Skills compatible host, or Claude Code for the plugin adapter
- Obsidian when you want its visual editor
- Bash for installer and optional legacy-extension scripts
- Git only for source development, release builds, or explicit checkpoints
- On Windows: WSL for vault writes; native Windows supports read-only
  inspection and dry-runs, see the [Windows and WSL guide](windows-wsl.md)

## Claude Code marketplace

Add the public catalog and install the namespaced plugin:

```bash
claude plugin marketplace add AgriciDaniel/claude-obsidian
claude plugin install claude-obsidian@agricidaniel-claude-obsidian
claude plugin list
```

Upgrading from 2.x or 3.0.0? See [Upgrade and rollback](#upgrade-and-rollback).

A private development tree that holds contributor-vault state has no
`.claude-plugin/marketplace.json`; it can contain contributor-vault state and
must not be added as a marketplace. The deterministic release builder injects
that manifest only into its audited output. The public default branch must be
promoted from the extracted audited artifact before these marketplace commands
are advertised for a new version.

Invoke skills as `/claude-obsidian:wiki`,
`/claude-obsidian:wiki-ingest`, and `/claude-obsidian:save`.

The plugin cache contains read-only product assets. Run Claude from the user
vault, set `CLAUDE_OBSIDIAN_VAULT`, or pass `--vault` to portable commands.

## Local Claude plugin development

From a product clone:

```bash
claude --plugin-dir <product-repository>
```

This mode is for testing uninstalled changes. Skills remain namespaced. It does
not convert the product repository into a user vault.

## Portable Agent Skills hosts

The installer defaults to a no-write preview for Codex, OpenCode, and Gemini:

```bash
bash scripts/setup-multi-agent.sh
bash scripts/setup-multi-agent.sh --apply
```

It links each canonical `skills/<name>/` directory into the host's direct
`<skill-root>/<name>/SKILL.md` discovery layout. Destinations are created only
when absent; an existing skill or link is never replaced. Check readiness
without writing:

```bash
bash scripts/setup-multi-agent.sh --check
```

Cursor and Windsurf use workspace-local discovery and require an explicit
workspace:

```bash
bash scripts/setup-multi-agent.sh --host cursor --host windsurf \
  --workspace <workspace> --apply
```

ZCode is opt-in and user-level (no `--workspace` needed):

```bash
bash scripts/setup-multi-agent.sh --host zcode
bash scripts/setup-multi-agent.sh --host zcode --apply
```

You can also create equivalent per-skill links manually. For each `<name>` under
the product's `skills/` directory, link that directory at:

```text
Codex:     ~/.agents/skills/<name>          -> <product-repository>/skills/<name>
OpenCode:  ~/.config/opencode/skills/<name> -> <product-repository>/skills/<name>
Gemini:    ~/.gemini/skills/<name>          -> <product-repository>/skills/<name>
ZCode:     ~/.zcode/skills/<name>           -> <product-repository>/skills/<name>
Cursor:    <workspace>/.cursor/skills/<name>   -> <product-repository>/skills/<name>
Windsurf:  <workspace>/.windsurf/skills/<name> -> <product-repository>/skills/<name>
```

## Create a new vault

Review the initialization plan first:

```bash
python3 scripts/claude-obsidian.py init <new-vault> \
  --generated-at <ISO-UTC> --operation-id init-reviewed
```

Apply the same operation only after the destination and changed paths look
correct:

```bash
python3 scripts/claude-obsidian.py init <new-vault> \
  --generated-at <ISO-UTC> --operation-id init-reviewed \
  --approved-plan-sha256 <reviewed-sha256> --apply
```

The generated vault contains:

- `.gitignore`, privacy-safe defaults excluding `.vault-meta/` runtime state,
  Obsidian workspace state, and live `.mcp.json` launch configuration;
- `.claude-obsidian.json`, workspace identity and vault selection;
- `inbox/`, visible source intake;
- `.raw/`, immutable source payloads and legacy delta manifest;
- `wiki/`, index, log, hot cache, overview, and generated notes;
- `.obsidian/`, minimal non-destructive Obsidian defaults;
- `.vault-meta/`, ignored runtime state created when needed.

Initialization does not add an upstream Git remote or install community
plugins. Open the new directory through Obsidian's vault picker.

## Adopt an existing vault

Adoption scans an existing Obsidian directory and proposes only missing product
metadata and foundation files:

```bash
python3 scripts/claude-obsidian.py adopt <existing-vault> \
  --generated-at <ISO-UTC> --operation-id adopt-reviewed
python3 scripts/claude-obsidian.py adopt <existing-vault> \
  --generated-at <ISO-UTC> --operation-id adopt-reviewed \
  --approved-plan-sha256 <reviewed-sha256> --apply
```

It preserves existing notes and Obsidian JSON. `--force` is intentionally
separate and should be used only after inspecting replacement targets.

For older claude-obsidian layouts, add provenance ledgers and workspace config
with an additive migration:

```bash
python3 scripts/claude-obsidian.py migrate --vault <existing-vault> \
  --generated-at <ISO-UTC> --operation-id migrate-reviewed
python3 scripts/claude-obsidian.py migrate --vault <existing-vault> \
  --generated-at <ISO-UTC> --operation-id migrate-reviewed \
  --approved-plan-sha256 <reviewed-sha256> --apply
```

Migration is idempotent, does not infer claims from prose, and leaves the
legacy `.raw/.manifest.json` byte-for-byte unchanged.

## Vault selection

Mutable commands use this precedence:

1. `--vault <path>`
2. `CLAUDE_OBSIDIAN_VAULT`
3. nearest `.claude-obsidian.json`
4. nearest unambiguous initialized vault from the current directory

If selection fails, the command exits without mutation. The product/plugin root
is rejected as an implicit vault.

Verify a selected vault:

```bash
python3 scripts/claude-obsidian.py doctor --vault <vault>
python3 scripts/claude-obsidian.py contracts --verify --vault <vault>
```

Doctor checks metadata directory safety and write/search permissions without
creating files. A missing `.vault-meta` is ready only when the vault directory
permits its creation. Readiness is a point-in-time diagnostic; transactions
still recheck paths, conflicts and permissions before applying changes.

## Optional configuration

The portable filesystem transport always remains available. Detect an active,
supported Obsidian CLI without persisting a snapshot:

```bash
bash scripts/detect-transport.sh --peek --vault <vault>
```

Optional extensions are explicit and vault-scoped:

```bash
bash scripts/setup-mode.sh --vault <vault>
bash scripts/setup-retrieve.sh --vault <vault>
bash scripts/setup-dragonscale.sh --vault <vault>
```

Read each script's preview before applying. Retrieval may use local BM25 alone;
model-based contextual prefixes or remote endpoints require explicit egress
consent. Optional tools such as Ollama and defuddle are capability-detected.

## First operation

Place a source in `inbox/`. Inspect the byte-capture plan:

```bash
python3 scripts/claude-obsidian.py capture plan --vault <vault>
```

Create immutable content-addressed copies only when the plan is correct:

```bash
python3 scripts/claude-obsidian.py capture apply --vault <vault> \
  --generated-at <ISO-UTC> --operation-id capture-reviewed
python3 scripts/claude-obsidian.py capture apply --vault <vault> \
  --generated-at <ISO-UTC> --operation-id capture-reviewed \
  --approved-plan-sha256 <reviewed-sha256> --apply
```

Then invoke the host's `wiki-ingest` skill. Image, PDF, and EPUB semantic
extraction is not built into the core; those formats currently receive bounded
metadata unless a separately configured adapter is explicitly approved.

## Upgrade and rollback

The Claude plugin is `claude-obsidian`, and its commands are
`/claude-obsidian:*`. From 2.x, update it as usual:

```bash
claude plugin marketplace update agricidaniel-claude-obsidian
claude plugin update claude-obsidian@agricidaniel-claude-obsidian
```

Version 3.0.0 alone was named `agricidaniel-obsidian`. If you installed it,
the marketplace `renames` map moves you back, then you install once:

```bash
claude plugin marketplace update agricidaniel-claude-obsidian
claude plugin install claude-obsidian@agricidaniel-claude-obsidian
```

Restart Claude Code after either path. Managed settings that name
`agricidaniel-obsidian` need an administrator change. See the
[namespace migration](namespace-migration.md) for verification and rollback.

Upgrade product code independently from the user vault. Before a migration or
large ingest, make a normal backup or snapshot. Knowledge writes are journaled;
run recovery after an interrupted operation:

```bash
python3 scripts/claude-obsidian.py transaction recover --vault <vault>
```

Recovery conservatively preserves stale or foreign lock identities. Only after
confirming no writer is active may an operator add `--force-stale-lock`.

Git history is optional and never automatic. To checkpoint exactly one
completed transaction:

```bash
python3 scripts/claude-obsidian.py checkpoint <operation-id> --vault <vault> \
  --as-of YYYY-MM-DD
```

Checkpointing uses a temporary index, verifies exact Git blob bytes, refuses
pre-existing staged state, and resumes an interrupted ref/index finalization
from its vault-local pending record.

## Uninstall

Remove the host integration, not the vault:

```bash
claude plugin uninstall claude-obsidian@agricidaniel-claude-obsidian
claude plugin marketplace remove agricidaniel-claude-obsidian
```

For portable hosts, remove only the per-skill links that the installer reported.
User notes, sources, ledgers, and Obsidian settings remain untouched.

## Troubleshooting

| Symptom | Check |
|---|---|
| Skill is not discovered | Confirm `<host-skill-root>/<name>/SKILL.md` resolves to the matching product skill; rerun installer `--check`. |
| Command says no vault selected | Run from the vault, pass `--vault`, or set `CLAUDE_OBSIDIAN_VAULT`. |
| Plugin-root refusal | Select a separate user vault; plugin cache writes are unsupported. |
| Transaction conflict / exit 75 | Another operation is active or a target changed; reread, rebuild, and inspect a new bundle. |
| Obsidian CLI is unavailable | Use filesystem reads; start/update Obsidian before retrying CLI transport. |
| Capture adapter is not implemented | Inspect `capture adapters`; configure a separate runner only with explicit consent. |
| Writes refused with `UNSUPPORTED_PLATFORM` on Windows | Vault mutation requires WSL; see the [Windows and WSL guide](windows-wsl.md). |
| WSL installed but `wsl --status` hangs | Follow Microsoft's diagnostic and reporting flow in the [Windows and WSL guide](windows-wsl.md#wsl-troubleshooting); do not assume a cause without evidence. |
| Native dry-run approval fails in WSL with `PLAN_CHANGED` | Approval hashes bind the reviewing environment; redo the dry-run inside WSL ([details](windows-wsl.md#wsl-troubleshooting)). |

Run `make test` in the product repository when developing or packaging changes.

---

### Methodology Modes Guide (`docs/methodology-modes-guide.md`)

# Methodology modes

Methodology mode is a vault-local routing preference for new notes. It does not
change evidence rules, transaction safety, or existing files.

## Modes

| Mode | Principle | Typical routes |
|---|---|---|
| Generic | Familiar typed folders without a named methodology | `wiki/sources/`, `entities/`, `concepts/`, `sessions/` |
| LYT | Maps of Content connect atomic notes | `wiki/mocs/`, `wiki/notes/` |
| PARA | Organize by current actionability | `wiki/projects/`, `areas/`, `resources/`, `archives/` |
| Zettelkasten | Stable IDs, atomic notes, dense links | Flat `wiki/<id>-<slug>.md` |

Absent configuration means Generic. Choose based on retrieval and maintenance
habits, not prestige:

- Use Generic when types are enough and migration cost should stay low.
- Use LYT when curated maps are the primary navigation surface.
- Use PARA when project/action status should control filing.
- Use Zettelkasten when atomicity and dense linking are practiced consistently.

## Read the current mode

```bash
python3 scripts/claude-obsidian.py mode get --vault <vault>
```

The compatibility router can preview a destination without writing:

```bash
python3 scripts/wiki-mode.py --vault <vault> route concept "Example concept" --mode lyt
```

Treat router output as a proposal. Validate it as a vault-relative path and
include the chosen path in the operation preview.

## Configure safely

Mode changes use one configuration transaction and default to dry-run:

```bash
python3 scripts/claude-obsidian.py mode set para --vault <vault> \
  --generated-at <ISO-UTC> --operation-id mode-reviewed
python3 scripts/claude-obsidian.py mode set para --vault <vault> \
  --generated-at <ISO-UTC> --operation-id mode-reviewed \
  --approved-plan-sha256 <reviewed-sha256> --apply
```

The compatibility wrapper has the same behavior:

```bash
bash scripts/setup-mode.sh --vault <vault> --mode para
bash scripts/setup-mode.sh --vault <vault> --mode para \
  --generated-at <ISO-UTC> --approved-plan-sha256 <reviewed-sha256> --apply
```

Existing custom route settings are preserved. The command does not create
folders, move notes, rewrite links, or migrate old content.

The Zettelkasten allocator has one canonical format:
`YYYYMMDDHHMMSSffffff-UUID4HEX`. A legacy timestamp-only `id_format` is
normalized in read output and in the next reviewed `mode set` transaction;
existing filenames are not renamed. Routed filename components are bounded to
255 UTF-8 bytes with a deterministic hash suffix when a title must be shortened.

## Switching modes

Switching affects only later routing. Before changing:

1. identify which workflows consume routes;
2. preview representative source, entity, concept, session, and research names;
3. check for filename or basename collisions;
4. apply the configuration transaction;
5. run the deterministic linter after the first operation in the new mode.

Bulk reorganization is a separate migration project. It needs a complete move
map, link rewrite plan, expected hashes, collision handling, rollback, and
explicit user approval. Never make it a side effect of `mode set`.

## How skills consume mode

`wiki-ingest`, `save`, and `autoresearch` may call the router while drafting.
Workers include the proposed route in their draft packet; the parent
orchestrator decides and applies the final path. Query, lint, and retrieval do
not change behavior based on methodology mode.

Mode is organizational metadata, not evidence. A note's folder does not raise
its authority, freshness, or confidence.

## Troubleshooting

| Symptom | Response |
|---|---|
| Missing config | Generic is active; nothing is broken. |
| Invalid JSON | Repair or replace it through a reviewed transaction; the router fails safely. |
| Unexpected route | Preview with an explicit `--mode`, inspect custom settings, and sanitize the name. |
| Mixed old/new layout | Expected after switching; do not auto-move older notes. |
| Zettelkasten ID concern | New IDs combine a sortable UTC microsecond prefix with a UUIDv4 nonce. Existing timestamp-only IDs remain valid; still detect target collisions before apply. |

---

### Namespace Migration (`docs/namespace-migration.md`)

# Claude plugin namespace

The Claude plugin identity is `claude-obsidian`, so its commands are
`/claude-obsidian:*`. The repository, portable Python core, vault configuration
and transaction schemas use the same name. User vaults never need a rename or
content migration for a plugin identity change.

Version 3.0.0 alone shipped the plugin as `agricidaniel-obsidian`. Version 3.0.1
restores `claude-obsidian`, the name users have invoked since the first release.

`claude plugin validate` reports a plugin name starting with `claude-` as an
error, because the name reads as one of Anthropic's own plugins, and
`claude plugin init` and `claude plugin tag` refuse it. Claude Code still
installs and loads such a plugin. The owner kept the published name, so the
host validator's reserved-name error is an approved exception; the project's
own `package validate` accepts `claude-obsidian` and still rejects every other
reserved name. Anthropic's plugin directory would not list the plugin under
this name. See the
[current manifest reference](https://code.claude.com/docs/en/plugins-reference#name).

## Try it from a checkout

Load a local checkout without changing the installed plugin:

```bash
claude --plugin-dir /absolute/path/to/claude-obsidian
```

Invoke `/claude-obsidian:wiki`, `/claude-obsidian:save`, or the other skills
under the same prefix. If the same name is installed from the marketplace,
disable that copy for the session so the checkout is the one that loads.

## Published upgrade

The marketplace channel is `agricidaniel-claude-obsidian`. Its plugin entry is
`claude-obsidian`, matching the manifest, and the marketplace carries a
top-level `renames` map from `agricidaniel-obsidian` to `claude-obsidian`, as
the [documented rename mechanism](https://code.claude.com/docs/en/plugins/host-marketplace#rename-or-remove-a-plugin)
describes.

**From 2.x.** The plugin name never changed for you, so this is an ordinary
update:

```bash
claude plugin marketplace update agricidaniel-claude-obsidian
claude plugin update claude-obsidian@agricidaniel-claude-obsidian
```

Restart Claude Code. Commands stay `/claude-obsidian:*`.

**From 3.0.0.** Update the marketplace copy:

```bash
claude plugin marketplace update agricidaniel-claude-obsidian
```

Claude Code rewrites the `agricidaniel-obsidian@agricidaniel-claude-obsidian`
key in `enabledPlugins` and `pluginConfigs` back to `claude-obsidian` in the
user, project and local scopes, then uninstalls the 3.0.0 plugin. Install the
restored name once and restart:

```bash
claude plugin install claude-obsidian@agricidaniel-claude-obsidian
```

Managed settings cannot be rewritten by Claude Code. An administrator who
enabled `agricidaniel-obsidian@agricidaniel-claude-obsidian` there must change
the key back to `claude-obsidian@agricidaniel-claude-obsidian`.

The 3.0.0 map from `claude-obsidian` to `agricidaniel-obsidian` is removed,
because a name cannot be both current and former. `package validate` rejects a
renames chain that does not end in a listed plugin or `null`, and the test
suite requires the 3.0.0 identity to keep migrating to the current one.

### Verify

After the update or install, check that the expected version and source are
installed and enabled, and that no entry reports an error:

```bash
claude plugin list --json
```

In a disposable vault, confirm the namespace exposes `wiki`, `save`,
`wiki-ingest`, and `wiki-query` once; confirm explicit vault selection, a
reviewed Save/Ingest operation and a Query that leaves the vault unchanged. An
installed-plugin listing or metadata validator alone does not establish skill
loading or successful operation.

### Roll back

To return to the last 2.x release, replace the channel with one pinned to it:

```bash
claude plugin marketplace remove agricidaniel-claude-obsidian
claude plugin marketplace add AgriciDaniel/claude-obsidian#v2.2.0
claude plugin install claude-obsidian@agricidaniel-claude-obsidian
claude plugin list --json
```

Removing a marketplace also removes the plugins installed from it, so
reinstall from the pinned channel as shown. To move forward again, remove the
pinned channel, add `AgriciDaniel/claude-obsidian` without a ref, and install
`claude-obsidian@agricidaniel-claude-obsidian`. Vault files require no
rollback for an adapter-only change.

## Evidence and limits

With Claude Code 2.1.306 on 2026-10-21, an isolated `CLAUDE_CONFIG_DIR` and a
git marketplace served over local smart HTTP that moved between the v2.2.0,
3.0.0 and 3.0.1 trees:

- From 2.2.0, `marketplace update` then `plugin update` moved
  `claude-obsidian` from 2.2.0 to 3.0.1 with the enabled key unchanged.
- From 3.0.0, `marketplace update` rewrote the enabled key to
  `claude-obsidian@agricidaniel-claude-obsidian` and uninstalled
  `agricidaniel-obsidian`; one `plugin install` then installed 3.0.1, enabled.
- In a session loaded with `--plugin-dir`, all 15 skills and 3 agents were
  listed under `claude-obsidian:`.
- `claude plugin validate` reported one error, the reserved name, for the
  plugin and the marketplace.

Earlier 3.0.0 runs with Claude Code 2.1.302 and 2.1.304 established that a
marketplace without a `renames` entry leaves existing installs reporting
`Plugin not found in marketplace`, and that the pinned `v2.2.0` rollback
reinstalls files identical to the tag.

The GitHub-hosted 3.0.1 channel, project and local scopes, managed settings and
Windows were not exercised. Check the target host's help before use. Local
source changes do not authorize installation, commits, publishing, or account
changes. See the [installation and uninstall guide](install-guide.md).

---

### Native Conformance (`docs/native-conformance.md`)

# Native Obsidian conformance procedure

Offline parser checks do not establish application behavior. Use a disposable
fixture vault and a declared supported Obsidian version for native acceptance.
No real vault, plugin installation, or account change is needed for static work.
Do not install a runtime or plugin merely to make an unverified check look green.

Record app version, desktop/mobile platform, timezone, enabled core/community
plugins and versions, fixture hashes, date, and evidence class. Classify each
observation as source/static, official CLI, or actual application. Mark anything
not exercised as unverified. Retain screenshots or exact CLI output and a small
machine-readable result per case; do not infer a passing render from valid YAML.

## Original fixture cases

| Case | Fixture and expected observation to record |
|---|---|
| Markdown paths | Space and percent-encoded filenames, alias labels, duplicate basenames in different folders, same-note links, nested headings and block IDs. Record the resolved target or native unresolved state. |
| Protected regions | Indented code, nested fences, comments, escaped syntax, math, callouts and footnotes. Record which apparent links are actual links and preserve bytes during unrelated edits. |
| Properties | Flat typed properties, quoted wikilinks and link lists, semantic year tags and Unicode tags. Numeric-only values belong in a number property, not tags. |
| Embeds | Named `.base` view, PDF page/options, image size, and Canvas file nodes. Record named-view selection and render-time network behavior separately. |
| Bases | Table/cards on 1.9+, list on 1.10+, kanban on 1.14+, groupOrder absent/empty/explicit/null, and read-only formula grouping. Record result rows and group visibility. |
| Date formulas | Fixed one-day, multi-month, negative, missing-date and timezone cases. Compare subtraction/division, Duration day field, and explicit number conversion. Record actual result type/value and errors without choosing a winner in advance. |
| Optional syntax | Existing DQL, DataviewJS, Templater, Tasks recurrence/indentation and trailing spaces. Normal read/ingest/lint must not execute code; unrelated prose edits preserve those bytes. |
| CLI selection | Two open fixture vaults. Run the read-only allowlist through explicit selected-vault identity; prove the other vault is unchanged. |

Use original fixture text and synthetic records. Do not copy a third party's test
corpus without its license and attribution. A portable lint result is compared
to the recorded native target graph only for the supported subset. Keep ambiguous
or unsupported cases visible. Native app observations do not establish Windows
write support, cloud-sync convergence, provider quality, or every plugin version.

When a discrepancy appears, retain the failing case, app evidence, exact parser
result and intentional divergence before changing an implementation or source
pin. Never update expected fixtures automatically on a normal test run.

---

### Prefix Providers (`docs/prefix-providers.md`)

# Optional contextual prefix providers

Synthetic prefixes remain the default. Preview with `--peek`; use `--no-llm`
to keep a build deterministic and provider-free. Selecting a provider changes
derived caches only, and requires an explicit build. Canonical pages remain
unchanged.

## An installed local Ollama model

Pass `--local-ollama --ollama-model name:tag` to contextual-prefix.py. The
adapter never starts a server, pulls a model or falls back to a cloud provider.
It requires a cloud-disabled status response, an exact installed model tag and
digest, and local model metadata before transmitting page text. Older servers
without these fields fall back to synthetic prefixes. A changing model digest
invalidates cached prefixes on the next explicit build.

When that preflight is unavailable, for example during a transient outage, the
cause is reported once for the operation and no page text is sent. Existing
records produced by the same endpoint and model under the current prompt
revision are kept for unchanged chunks, because their digest cannot be
re-proved until the server returns. Only new or changed chunks receive a
synthetic prefix. If no chunk changes, chunk records and the ready BM25 index
remain byte identical.

The default endpoint is the numeric loopback `127.0.0.1`. The exact hostname
`localhost` is normalized to `127.0.0.1` before any request, and literal `::1`
remains available. This prevents local mode from relying on host resolver
configuration. An explicit non-loopback endpoint additionally requires the
user's approval and `--allow-remote-ollama`. Select HTTPS for a remote server
when page contents need transport encryption. Standard certificate hostname
validation applies to the normalized numeric endpoint, so a certificate for
only `localhost` does not cover `https://127.0.0.1`. Proxies and redirects are
disabled. JSON requests and responses are bounded, as are output and timeouts.
Each timeout is one wall-clock deadline for the complete request: connection,
upload, response headers and every body read. A server that drips bytes cannot
extend it. Responses are parsed strictly, and a duplicate object key at any
depth rejects the response.
Unsupported responses, refusal, truncation and mismatched model identity
produce synthetic prefixes and open the operation circuit. Later chunks and
pages then fall back without another generation request. A user-supplied
server can misrepresent its configuration; the preflight does not attest the
remote machine.

The optional Ollama embedding endpoints used by reranking and semantic tiling
apply the same exact `localhost` to `127.0.0.1` normalization before their
first transport request. They preserve an explicit port and base path, keep a
literal `::1`, and retain the separate remote endpoint consent requirement.
Before any query, page, chunk, or embedding cache work, all Ollama callers
require server status to report `cloud.disabled` as the Boolean value `true`.
They also require a selected installed model with a valid digest and nonempty
local `model_info`. Cloud tags, remote model fields, malformed identity, and
conflicting alias digests fail this proof. Remote fields are decisive in the
status and model metadata responses, in the model list envelope, and in the
list entries for the selected model or its permitted alias. Another installed
cloud model never receives the request and does not block a local model. An
explicit tag matches exactly. An
untagged embedding model matches only its exact name or `:latest`, preferring
the exact name. `--allow-remote-ollama` approves the server endpoint only. It
does not approve cloud delegation by that server.
Every embedding GET and POST disables ambient proxies, refuses redirects,
requires the final response URL to match the requested URL, and reads a bounded
JSON response within the same per-request wall-clock deadline and strict
duplicate-key parsing. A malformed HTTP status line or other protocol error is
treated as an unavailable server: reranking falls back to BM25 order and
tiling exits 10, or 12 once local proof has begun. Reranking validates a nonempty query within 8,000 NFKC-normalized
characters and at most 1,000 object candidates with safe relative paths and
finite scores before model, endpoint, cache, or embedding work. Semantic tiling
validates threshold schema version 1 and finite bands satisfying
`0 <= review < error <= 1` before provider probes or cache reads and writes.
Malformed input exits through the documented controlled interface and sends no
query, candidate, or page body to Ollama.
Embedding responses and semantic tiling cache vectors must be bounded lists of
finite, nonboolean numbers with one consistent dimension per model. Malformed
cache documents exit through the cache corruption contract before any cache
publication. Rerank cache keys include the verified model digest. Semantic
tiling cache schema version 2 binds reusable entries to the resolved model name
and digest. A valid version 1 cache is treated as a miss, while a malformed
legacy cache remains corruption. The first semantic tiling embedding failure,
including an HTTP error, remote identity in the response, or a dimension
change, opens a circuit for the run: no further page body is sent, and cached
vectors still contribute to the report. Partial or total embedding failure
preserves the previous cache, prints a provisional incomplete report, and
exits 12.
Malformed rerank chunk JSON,
including non-text selected content, preserves the whole candidate set through
the documented BM25 fallback.
JSON parser conversion or nesting limits follow the same cache, threshold and
candidate error or fallback contracts as other malformed JSON. They do not
escape as raw parsing exceptions.

The source contract was reviewed against Ollama revision
`d1ed3fd13ac73f60cff00bbead8af24a68c4200d`, including its
[status handler](https://github.com/ollama/ollama/blob/d1ed3fd13ac73f60cff00bbead8af24a68c4200d/server/routes.go)
and [response types](https://github.com/ollama/ollama/blob/d1ed3fd13ac73f60cff00bbead8af24a68c4200d/api/types.go),
checked 2026-10-17. Mocked protocol tests are separate from live server
compatibility. No minimum released Ollama version has been established.

## An explicitly approved cloud provider

`--allow-egress` permits direct Anthropic API requests when
`ANTHROPIC_API_KEY` is present. An API key never counts as consent. State which
provider will receive the selected page bodies before an approved build.

The API defaults to `--anthropic-model claude-haiku-5-5`, using low effort,
adaptive thinking and a bounded 2,048-token output budget. The explicit legacy
profile is `--anthropic-model claude-haiku-4-5-20251001`. Unsupported model
identifiers are refused. Refused, truncated, empty or unusable output falls
back directly to a synthetic prefix and opens the operation circuit. Remaining
chunks and pages use synthetic prefixes without repeating a failed generation
request. Local Ollama mode never selects the API.

Generation request bodies have three configurable operation guards. Defaults
are 128 requests, 16,777,216 serialized request-body bytes for the complete
operation, and 1,048,576 serialized UTF-8 request-body bytes for one request.
Use `--provider-max-requests`, `--provider-max-bytes`, and
`--provider-max-request-bytes` to change those values within their hard caps.
The hard caps are 512 requests, 134,217,728 operation bytes, and 4,194,304
bytes for one request.
Local Ollama keeps its smaller 131,072-byte request limit. The complete set of
planned requests for one page is measured before that page's first generation
call. A page that cannot fit the remaining count or byte budget uses synthetic
prefixes without partial provider execution. Counts and bytes are shared
across every selected page in one command. Synthetic and preview paths consume
none of this generation budget, including explicit `--no-llm` runs.

The 512-token cache floor is model-specific. The character-count estimate is
only a heuristic; actual cache reads and writes require provider telemetry.
Changing the model or generation profile refreshes derived prefixes on the
next explicit build. These
profiles were checked against current [model guidance](https://platform.claude.com/docs/en/models/haiku-5-5/migration-guide)
and [caching guidance](https://platform.claude.com/docs/en/build-with-claude/prompt-caching)
on 2026-10-17. Mocked payload tests establish request behavior, not live model
quality, cache savings or billing.

The ambient Claude CLI generation tier has been removed. No `claude -p`
subprocess, subscription fallback, host tools, hooks or loaded host settings
participate in prefix generation. With no usable API key or selected local
Ollama provider, the result is synthetic. Older `claude-cli` records and records
from previous prompt revisions regenerate on the next explicit build, even
when their text happens to match the synthetic result. They are never retained
as an equivalent safe fallback.

## Source envelopes and output validation

Every current wiki page is decoded as strict UTF-8 during the address
preflight, before optional provider preflight, page envelope construction,
provider requests, or derived cache and index mutation. Malformed UTF-8 exits
with the controlled page error and preserves existing chunks and index bytes.
The direct single-page processing entry point applies the same strict decode
before it inspects derived state.

Both generators use the same system trust policy. Each processed page creates
one fresh random identifier shared by all of that page's page and chunk tags.
The identifier is named in each request's system instruction. Titles, bodies
and chunks are JSON encoded and HTML escaped inside those tags, so embedded
quotes, matching closing tags, control characters and fake role text remain
data. Request identifiers do not enter the derived
prefix profile, so randomness alone never causes a rebuild. Reusing the same
identifier within one page keeps its system and page blocks byte stable across
chunk requests. Eligibility markers are retained, but only provider telemetry
establishes actual cache use.

A model response must be UTF-8 encodable, nonempty, a single line, at most 34
whitespace-separated words and at most 280 Unicode characters. The character
bound also applies to writing without spaces. Validation allows non-Latin
scripts and the joining characters used by some languages. It rejects control
characters, line separators, directional formatting controls and markup
syntax. Invalid responses fall back as a whole; the adapter never silently
keeps the first line or truncates an invalid model response. The deterministic
synthetic prefix retains its separate 1,000-character bound.

Every replacement record for a page is serialized before its ready BM25 index
is invalidated. An encoding failure therefore preserves that page's previous
chunks and index. If a provider fails and its synthetic fallback exactly matches
an existing generation with the current safe synthetic profile and matching
source identity, the original chunk and ready index remain byte identical.
Fallback records identify the effective provider as synthetic. A changed chunk
set still invalidates the old index before publication and needs the usual
explicit BM25 rebuild. Filesystem publication failures after invalidation also
leave retrieval unavailable until rebuilt, preventing a mixed-generation read.

An existing chunk is skipped only when its schema, source hashes, raw text,
character count, contextualized text, effective provider, and complete prefix
profile agree with the current page and requested generation profile. Unknown
schemas, duplicate JSON keys, non-object JSON values, and malformed or
internally inconsistent derived records regenerate. A synthetic record must
also match the deterministic prefix recomputed from the current page and chunk.

These checks establish structural boundaries and bounded output, not semantic
sentence correctness, instruction detection or prompt-injection resistance.
The tests use mocked responses and temporary vaults; live provider behavior,
retrieval quality and adversarial model acceptance remain unverified.

---

### Prompt Evaluation (`docs/prompt-evaluation.md`)

# Offline prompt evaluation

The prompt evaluator packages four invented Query and Save cases and checks a
supplied JSON response. It does not execute a provider, model, plugin, command
from source material, or vault operation.

## Build the packet

Choose a physical directory for packet and response files. Readers reject
symlinks in every supplied path component. This includes macOS aliases such as
`/tmp` and `/var`, so resolve the trusted temporary directory first:

```bash
PROMPT_EVAL_DIR="$(python3 -c 'from pathlib import Path; import tempfile; print(Path(tempfile.gettempdir()).resolve())')"
python3 scripts/evaluate-prompts.py packet > "$PROMPT_EVAL_DIR/prompt-packet.json"
```

The packet contains the current full `wiki-query` and `save` skill text plus
only the Claim rules section from the provenance reference. It records the
SHA-256 of each complete source, the selected excerpt, the frozen cases, and a
binding hash over those inputs. The synthetic fixture contains no user vault
files, credentials, provider output, or external passages.

Packet generation is deterministic for identical source and fixture bytes.
Changing either canonical skill, the provenance reference, or the cases changes
the binding. Evaluation against a saved packet also rechecks its source hashes
against the current checkout.

## Supply and evaluate a response

The evaluator expects one strict `claude-obsidian.prompt-response.v1` JSON
object. Use the packet's `response_contract` as the exact shape, copy its
`binding_sha256`, and provide all four case results. Then run:

```bash
python3 scripts/evaluate-prompts.py evaluate "$PROMPT_EVAL_DIR/response.json" \
  --packet "$PROMPT_EVAL_DIR/prompt-packet.json"
```

The response contract describes field names, JSON types and the frozen Save
title grammar without embedding the expected case values. The supplied case
evidence determines those values. For this English fixture, titles contain
one to eight ASCII-letter words separated by single spaces, use at most 80
characters, and draw every word from the selected content without regard to
letter case. This bounded test grammar is not a rule for ordinary Save titles.

Exit codes are:

- `0`: the response is valid and every frozen check passed;
- `1`: the response is valid but one or more checks failed;
- `2`: the packet or response is malformed, stale, unsafe, or outside schema.

JSON parsing rejects duplicate keys and non-finite numbers. Required booleans
must be actual JSON booleans. Missing cases, fields, or the note body are schema
errors rather than silent failures.

Each report records the SHA-256 of the evaluator source file and the canonical
supplied response. These distinguish a later checker revision or a different
response even when the packet's skill and case binding is unchanged.

The Query cases return typed fact fields rather than free prose. The checks
require `opens_at` to equal `09:30`, retain tentative status, and use the exact
Juniper citation. They require `closing_times` to equal `17:00` and `18:00` in
source order, use both exact Cedar citations, keep the claim contested and
unresolved, and exclude the injected `23:00` instruction. The absent case must
return a null telephone, false availability, unsupported status, and no
citations. Extra prose fields are schema errors.

The Save case must return an actual note body containing only the selected
text, preserve `Friday`, avoid an invented calendar date, classify the
conversation assertion as synthetic, require review, and avoid applying it.
The no-added-date check requires both the exact selected note body and the
source-word title contract. It does not attempt general natural-language date
recognition or certify that an arbitrary title is useful. Human review still
assesses the title's meaning.
Exact scope means equality of the decoded note-body text, including surrounding
whitespace. Added spaces, tabs or line breaks fail the exact-body checks.

## Evidence boundary

A passing report is offline response-contract validation against four frozen
synthetic cases. It shows that a supplied structured response met bounded
schema and exact-value checks. It does not validate natural-language answer
quality. A handcrafted passing response does not establish prompt reasoning or
model quality. A model-produced response would also need a separate
authenticated trial receipt before it could support a controlled prompt
reasoning claim.

The report does not prove full skill operation, host activation, filesystem
behavior, native Obsidian behavior, human semantic acceptance, or superiority
over another product. Human review remains required because deterministic
checks cannot establish universal semantic correctness.

The evaluator never calls a provider. It can score a response produced
elsewhere, but authorization, cost controls, model identity, trial execution,
and retention of raw provider evidence remain separate responsibilities.

---

### Retrieval Evaluation (`docs/retrieval-evaluation.md`)

# Offline retrieval evaluation

Use explicit relevance labels to measure a prebuilt local BM25 index. Evaluation
reads selected pages and caches without rebuilding them. It always passes
`--no-rerank`, makes no model or provider calls, and never provisions a user
vault. Reported results describe the selected snapshot and query set.

Use physical paths for query-label files. The reader rejects symlinks in every
path component, including macOS aliases such as `/tmp` and `/var`. Resolve the
trusted containing directory before creating ordinary label files there;
explicit symlink inputs remain invalid.

```bash
python3 scripts/evaluate-retrieval.py --vault /absolute/vault \
  --queries /absolute/queries.json
```

The core API is `run_quality(vault, query_file, product_root, top_k=10)`. The
portable CLI also exposes the same evaluation through `bench --queries FILE
--vault VAULT`. Both require an existing index. Missing or corrupt derived
state exits 10 with a rebuild diagnostic. Invalid labels, unsafe paths, or a
changing snapshot exit 2. Rebuilding an index is a separate explicit operation.

## Write useful labels

Each query needs an ID, exact query text, and an explicit list of relevant
vault-relative Markdown page paths. Empty lists mean that no answer is
expected from this corpus. They are evaluated as negative cases; they never
inflate recall or MRR by dividing by zero or counting a free success.

```json
{
  "schema": "claude-obsidian.retrieval-queries.v1",
  "queries": [
    {
      "id": "journal-recovery",
      "query": "recover interrupted writes",
      "expected_pages": ["wiki/concepts/Recovery.md"],
      "category": "recovery"
    },
    {
      "id": "unanswerable",
      "query": "a topic absent from the vault",
      "expected_pages": [],
      "category": "negative"
    }
  ]
}
```

Write labels from the information need before tuning retrieval. Include exact
identifiers, natural-language formulations, synonyms, Unicode names, several
relevant pages, stale material, conflicting records, and genuinely absent
topics. Keep a separate holdout set for future changes. Do not rewrite labels
to fit the candidate ranking.

An optional `skip_reason` marks an explicit exclusion. The report records it.
A query with a missing gold page is also excluded from aggregate metrics and
listed with its missing paths. Unsafe symlinks, junctions, traversal, absolute
paths, duplicate labels, and nonregular files fail closed. Label files are
bounded at 1 MiB, query sets at 1,000 entries, each query at 8,000 normalized
characters, and each gold set at 64 pages. Reads use no-follow descriptors on
platforms that support them and reject link components on other platforms.

## Interpret the report

Recall at 1, 5, and 10 is the fraction of a query's gold pages present in the
first 1, 5, or 10 distinct returned pages. The aggregate is the mean across
scored queries with non-empty gold lists. A query with two relevant pages and
one correct first result therefore has recall at 1 of 0.5.

MRR is the mean reciprocal rank of the first relevant page. A first relevant
page at rank 2 contributes 0.5; an absent relevant page contributes 0. The
default cutoff is 10. `--top` can increase the measured depth; the minimum
depth remains 10 so all three recall cutoffs have comparable observations.

Every metric includes its own `n` and status. `n = 0` is rendered as `no data`
with a null value. Empty-gold queries have a separate no-result accuracy with
their own denominator. Missing-gold and explicitly skipped queries have
separate counts. Per-query rows expose rankings, misses at each cutoff,
reciprocal rank, process latency, and retrieval diagnostics.

The candidate pool is at least 20 BM25 chunks and grows with requested depth.
Usable-candidate recall is reported separately after stale filtering and page
deduplication. Raw BM25 candidate recall is unavailable because `retrieve.py
--explain` exposes raw candidate counts without their page identities. This
scope is recorded directly in the report. Latency includes process startup,
index loading, and query work. P50 and P95 use nearest-rank percentiles.

Exact label and index byte hashes bind a run to its inputs. Changes to either
during evaluation invalidate the run. Page hash checking in retrieval rejects
stale chunks. A benchmark does not hold the vault-wide mutation lock; run it
while other writers are idle. The report is returned to stdout and can be
saved outside the user vault when a durable comparison is needed.

Malformed or undecodable chunk records are counted as skipped stale candidates;
healthy candidates remain available. Page freshness requires strict UTF-8, so
corrupt page bytes cannot match a cached literal replacement character through
lossy decoding. Rejected chunks and pages are not repaired during a query.

## Run the original fixture

The source checkout includes 14 original labeled queries over 10 invented
pages. It contains French, Japanese, Romanian, exact-address lookup,
multiple-gold retrieval, an intentional lexical synonym miss, stale conflicting
history, and explicit unanswerable cases.

```bash
python3 scripts/evaluate-retrieval.py --fixture
python3 tests/test_retrieval_eval.py
```

Fixture mode creates only a temporary directory, copies the invented pages,
runs `contextual-prefix.py --all --no-llm`, builds BM25, and changes one fixture
page after indexing to test stale-cache rejection. It then runs the same
read-only evaluator and deletes the temporary directory. It refuses an
existing destination through its provisioning API. The report includes hashes
for all original fixture files. Index hashes can vary between builds because
derived records include build metadata; fixture content hashes are stable.

Distributions that exclude source test fixtures still support evaluation with
user-supplied labels. In that case `--fixture` reports that its source fixture
is unavailable. It never silently switches to a different corpus.

This fixture establishes executable behavior and a repeatable local baseline.
It does not establish real-vault quality, native Obsidian behavior, semantic
model quality, or a quality improvement over a previous release. Compare
candidates on the same unchanged labels and corpus and retain every query's
result, including misses and exclusions.

---

### Upstream References (`docs/upstream-references.md`)

# Reconciling upstream references

`config/upstream-references.json` records the primary repository revisions
reviewed on 2026-10-17, inspected paths, license boundaries, local references,
and intentional divergences. These are dated source observations, not installed
runtime versions or current-head claims. This update imports no upstream code,
fixture corpus, branding, or substantial prose.

Check local fallback hashes before reviewing a change. A hash mismatch means
local text needs reconciliation; it is not permission to restore old text.
Compare a proposed upstream revision against the pinned revision using an
explicitly requested network read. Review behavior, source authority, license
changes and application compatibility before moving a pin. Never download and
replace skills automatically. Keep the 15-skill interface and transactional
vault boundary unless a separately reviewed design changes them.

When updating a reference, record the candidate SHA, exact inspected paths,
retrieval date, native evidence when available, and new intentional divergences.
Then update affected local hashes and run the reference-example tests plus
package/contracts checks. Moving a pin without reviewing content is not an
upstream refresh. No scheduler or installation is implied by this manifest.

MIT references still require their notices for copied substantial portions.
Templater is an interoperability target under AGPLv3; do not copy its code into
this MIT product without a separate license decision. Smart Connections uses a
custom restricted license, so its implementation is excluded from copying.
Web Clipper's marketing assets are outside its source-code MIT grant. A skill
mentioning Knap grants no license to Knap itself, which was not inspected.

Known disagreement: official Bases Help describes date differences in
milliseconds while the 1.9.3 changelog and upstream skill describe a Duration.
The recorded `obsidian.d.ts` excerpts do not include a Duration definition and
are not cited for either side. Preserve this as unresolved until supported
native app tests exist.
The local Defuddle skill intentionally stops at inert plans/filesystem capture,
even when an upstream recipe would run the CLI. Local transaction, no automatic
installation, selected-vault and network-consent rules remain authoritative.

---

### Windows Wsl (`docs/windows-wsl.md`)

# Windows and WSL guide

claude-obsidian supports native Windows as a read-only platform and WSL as the
full-capability platform. This guide covers what works where, why the boundary
exists, and how to unstick WSL when it misbehaves.

## Platform support

| Capability | WSL / Linux / macOS | Native Windows (incl. Git Bash) |
|---|---|---|
| Inspection, dry-run previews, retrieval | Yes | Yes |
| Vault writes (`transaction apply`, `init`, `adopt`, `migrate`, `capture apply`, `mode set`) | Yes | No: refused with `UNSUPPORTED_PLATFORM` |
| Capture queue commands (including read-only `capture queue list`) | Yes | No: currently refused; tracked in [#151](https://github.com/AgriciDaniel/claude-obsidian/issues/151) |
| Git checkpoints (`checkpoint`) | Linux and macOS only | No |
| Bash setup scripts and shell test suites | Yes | No (POSIX-only) |
| Claude Code hooks (`SessionStart`, `Stop`) | Yes (works out of the box) | Partial: requires `python3` on `PATH`; see [below](#claude-code-hooks-and-python3-on-windows) |

Vaults must live on a filesystem with stable file identity: NTFS is fine, but
FAT/exFAT volumes (typical USB sticks) and some network shares are refused with
`UNSAFE_VAULT_IDENTITY`: move the vault to NTFS or work inside WSL.

## Claude Code hooks and python3 on Windows

`hooks/hooks.json` spawns each hook using the [Claude Code exec-form command
hook](https://code.claude.com/docs/en/hooks): `"command": "python3"` with an
`args` array. Claude Code resolves `python3` as an executable on `PATH` and
spawns it directly; there is no shell, so no `.bat` shim, alias function, or
shell profile is consulted.

WSL and most Linux and macOS Python installs provide a `python3` on `PATH` by
default, so hooks work there without extra setup. Native Windows commonly does
not:

- python.org installer: installs `python.exe`, not `python3.exe`. Either add
  a `python3.exe` shim earlier on `PATH` than the interpreter, install a
  distribution that provides `python3.exe`, or run Claude Code from WSL so
  hooks resolve the WSL `python3`.
- Microsoft Store Python: the `python3` app execution alias can be a stub
  that opens the Store instead of running Python. Disable the `python3` app
  execution alias in Windows Settings, then install Python from python.org
  or WSL and confirm the real interpreter is on `PATH`.

When `python3` cannot be resolved, Claude Code fails to spawn the hook
process, so claude-obsidian's own code never runs and cannot emit a
diagnostic. SessionStart context and Stop recovery warnings are both silently
absent in that case; the rest of claude-obsidian (skills and the CLI) is
unaffected, since only the optional hook path depends on `python3`.

## Why writes require WSL

Mutation safety is bound to POSIX directory descriptors: the vault root and
every runtime directory stay pinned for the whole write, so a concurrently
swapped symlink or replaced folder fails closed instead of redirecting the
write (see the [compound vault guide](compound-vault-guide.md)). Native Windows
cannot provide those primitives, so writes are refused up front rather than
silently running with weaker guarantees.

A degraded native-Windows write mode (default-off, behind an explicit
reduced-guarantees flag) is under consideration in
[#151](https://github.com/AgriciDaniel/claude-obsidian/issues/151). If WSL is a
blocker for you, that issue is the place to weigh in.

## WSL troubleshooting

WSL being "installed" does not always mean WSL is working. Symptoms and checks,
roughly in the order worth trying:

| Symptom | Check |
|---|---|
| `wsl --install` completed but `wsl --status` or `wsl -l -v` hangs indefinitely | This field-reported hang has no confirmed cause. Follow Microsoft's WSL hang diagnosis and reporting flow below. |
| `wsl` reports a kernel or version error | Run `wsl --update`, then `wsl --shutdown`, then retry. |
| WSL worked before and stopped after an update or software change | Do not assume a cause. Update Windows and WSL, then follow Microsoft's WSL troubleshooting flow. |
| Approval hash from a native dry-run fails inside WSL with `PLAN_CHANGED` | By design: the approval hash binds the reviewing environment's filesystem identity. Run the dry-run review inside WSL when the apply will happen there; a natively produced `approved_plan_sha256` cannot be replayed from WSL. |
| Writes fail with `UNSAFE_VAULT_IDENTITY` mentioning stable file identity | The vault sits on FAT/exFAT or an unsupported network share. Move it to NTFS, or keep it inside the WSL filesystem. |

WSL troubleshooting checklist:

1. Start with Microsoft's official
   [WSL troubleshooting guide](https://learn.microsoft.com/en-us/windows/wsl/troubleshooting).
2. Confirm virtualization is enabled in BIOS/UEFI and that both "Virtual
   Machine Platform" and "Windows Subsystem for Linux" are enabled. Reboot
   after enabling either feature.
3. Run `wsl --update` from an elevated prompt, then `wsl --shutdown`, and retry
   `wsl --status`.
4. Confirm the hypervisor launch setting is enabled. If a third-party
   hypervisor is installed, use a current version that supports Hyper-V or
   temporarily turn it off while diagnosing the conflict.
5. If WSL still hangs, follow Microsoft's
   [WSL hang data-collection steps](https://learn.microsoft.com/en-us/windows/wsl/troubleshooting-guide#wsl-hangs)
   and file the resulting report with the WSL project. Do not attribute the
   hang to a specific cause without supporting diagnostics.

## Working across the boundary

The supported native-Windows workflow is: inspect and review natively, mutate
inside WSL. Because approval hashes bind to the environment that produced
them, do the reviewed dry-run in the same environment that will run the apply.
Keeping the vault inside the WSL filesystem (rather than on a mounted Windows
drive) avoids both the identity caveats above and cross-boundary performance
overhead.

---

### Workflow Acceptance (`docs/workflow-acceptance.md`)

# Knowledge workflow acceptance

Storage validity and knowledge completeness are separate checks. A recoverable
transaction can omit the note's catalog entry or operation history. Save and
Ingest use the named workflow gate:

```bash
python3 "$CORE" workflow inspect "$BUNDLE" --vault "$VAULT"
# Read storage_valid, workflow_complete, findings, and approval_sha256.
python3 "$CORE" workflow apply "$BUNDLE" --vault "$VAULT" \
  --approved-plan-sha256 "$APPROVAL_SHA256"
```

Set `APPROVAL_SHA256` only from the reviewed inspect result. Generic
`transaction inspect/apply` remains the storage interface for other authorized
operations; it does not substitute for this workflow acceptance check.

Save and Ingest content operations couple the note/page writes, a hash-bound active
catalog write, a log entry, and hot context of at most 500 words and 32 KiB (the
SessionStart context bound). The default catalog is `wiki/index.md`. To declare
another active methodology MOC, place its exact vault-relative path in the bundle:

```json
{
  "workflow": {
    "catalogs": ["wiki/maps/Research.md"]
  }
}
```

This is a field inside a complete `claude-obsidian.transaction.v1` bundle, not a
standalone applyable document. Include expected hashes and writes for declared
catalogs. Every selected canonical content page, whether created or replaced,
requires a visible reference in the prospective content of a declared catalog
write. Use a path-qualified wikilink or relative Markdown link from that catalog
to each selected canonical page. An already indexed note can keep the catalog
bytes unchanged: supply its existing content as a `replace` write and bind its
current SHA-256 in `expected_hashes`. No meaningless catalog text edit is needed.
An unchanged catalog that omits a selected page fails the gate. `changed_paths`
lists transaction targets; its presence alone does not prove that bytes changed.
The catalog write and hash remain required for existing pages, keeping navigation
and the note update in the same approved, recoverable operation and replay.
Catalogs and selected content pages must use visible canonical paths: a
dot-prefixed directory or filename cannot satisfy this navigation obligation.
An active catalog is a catalog declared in `workflow.catalogs` that is either
`wiki/index.md` or already exists (a hash-bound `replace` write). A catalog that
the same bundle creates must be linked from one of those active catalog writes;
otherwise inspection reports `INACTIVE_CATALOG`, so a new throwaway page cannot
stand in for navigation. To link a new MOC from the root index, list
`wiki/index.md` beside the MOC so it is also declared in `workflow.catalogs`; an
undeclared index write does not count. The gate cannot judge whether an
existing declared page is a useful MOC.

Ingest includes both source and claim ledger writes; Save includes ledgers when
evidence records changed. Ingest also needs a source: a create-only `.raw/`
payload in the bundle, or a recorded source in the prospective source ledger
(`MISSING_INGEST_SOURCE`). At least one ledger must change; byte-identical
rewrites of both report `UNCHANGED_PROVENANCE`. The new first log entry, the
first `## ` heading of the log body, uses `## [YYYY-MM-DD] save|...` or
`## [YYYY-MM-DD] ingest|...`. It may sit directly below the log title and
introduction, as in the log that `init` seeds. Every existing entry stays
unchanged after the new one, and every other existing line keeps its order;
otherwise inspection reports `LOG_HISTORY_NOT_PRESERVED`. The date must be a
real calendar date, not only a matching digit pattern. Do not manufacture claims
or source records to satisfy a validator.

Workflow inspection materializes hash-bound UTF-8 `content_file` drafts. After
a successful apply, replay can use the committed target bytes when a disposable
draft no longer exists. For managed address assignment, it can reverse only
the exact generated second-line insertion, and only when those recovered bytes
match the declared original hash. The declared content hash, original materialized bundle,
approval and completed result still have to match. An available draft whose
bytes changed is refused, as are changed targets, modes or expected hashes.
Moving a draft with identical declared bytes does not change the materialized
operation. Completed replay also requires the retained journal and expanded
bundle: their identities, write coverage and content hashes must correlate
with the result under the transaction lock. A receipt alone cannot certify a
completed operation. Historical receipt-only records remain untouched during
unrelated operations, but cannot authorize replay. This equivalence does not
relax the transaction engine's replay or result-drift checks.

Malformed retained result JSON, invalid UTF-8, excessive JSON nesting, null or
incomplete result metadata, non-text modes or operation types, and malformed
replay writes return controlled transaction errors. A retained journal still in
the `prepared` state cannot certify completion. A FIFO or other non-regular
file at a receipt, log, or target path is refused without blocking. Completed
replay does not read the current `wiki/log.md`; the retained result binds it.
Reusing a completed operation ID with another operation type reports
`OPERATION_ID_REUSED`, as `transaction apply` does for another bundle.
Rejected replay does not authorize a new operation or modify the vault.
An existing log with invalid UTF-8 returns `WORKFLOW_LOG_NOT_TEXT` before
inspection can approve its replacement. Retain its original bytes and review
a separate repair rather than silently discarding unparseable history.

Lint and workflow navigation share protection for HTML and Obsidian `%%` comments,
matching-length inline code, backtick or tilde fences (closed by an equal or longer
run), indented code, escaped opening brackets, and `$` or `$$` math. Masking preserves
source offsets and line numbers. List continuation links remain visible when their
indentation is relative to a list item rather than a code block. A list-contained
fence ends when its list container ends, including an unclosed fence followed by
an outside paragraph or sibling item. Literal destination text is not parsed as
prose math or comments; enclosing code or comments still hide the whole link.
Inline Markdown links support single-line labels with balanced brackets, angled
destinations, balanced parentheses, and optional quoted or parenthesized titles.
Escapes and inline code inside a label do not create extra label brackets.
Decoded destinations still undergo vault confinement checks.
Heading labels render supported inline links outside code; requested fragments
name the resulting text and are not reinterpreted as new links. A literal
wikilink inside inline code therefore remains a distinct heading label.
This is a static navigation subset, not a full CommonMark or Obsidian renderer:
reference-style links, HTML anchors, blockquote container parsing, plugin syntax,
and native rendering are outside its coverage.

The deterministic gate checks declared structure and coupling. It cannot decide
whether prose is useful, a citation supports a claim, or an answer is complete.
Use the frozen Save/Ingest/Query scenarios under `tests/fixtures/forward/` for
fresh-context host acceptance. Retain host/model versions, prompts and outputs,
before/after hashes, operation receipts, reviewer findings, and exact missing
checks. Query must leave every vault byte unchanged and abstain when support is
missing. Test both a clean vault and a representative legacy copy.

Mocked/offline fixtures, actual model runs, native Obsidian rendering, and human
acceptance are distinct evidence classes. Do not promote one into another.
The [offline prompt evaluator](prompt-evaluation.md) checks explicit response
contracts and negative controls before any separately approved model trial;
it does not replace the disposable-vault workflow acceptance above.

---

--- METRICS ---
- Files Extracted: 15
- Estimated Token Budget: ~26833 tokens
- Canonical Reference: https://codewiki.google/github.com/AgriciDaniel/claude-obsidian
