Vault Reader — The Other Half of the Bridge
The mirror of vault-writer. Where vault-writer turns conversation → vault (write-side), vault-reader turns vault → conversation (read-side). Together they form the read/write half of the vault-bridge suite: skills can both persist artifacts to the vault and query it before doing expensive external work.
vault-reader does one thing per invocation. It is not an orchestrator, not a search engine, and not a semantic-similarity engine. It reads the vault's existing frontmatter and content with schema-aware filters and returns honest results — including honest "nothing matched."
Guiding principles
These are non-negotiable:
- Read-only, always. No writes. No cache files. No side effects on the vault. The
allowed-toolsdeclaration enforces this; the skill's discipline enforces it more strongly. - Schema-strict. Reject queries whose field names or enum values are not in the vault's
_meta/frontmatter-schema.md. Silent mismatch (caller queriesConfidencewhen field isconfidence) destroys query reliability over time. - Fail closed on missing schema or invalid vault path. Do not "best effort" against a non-vault directory.
- Always read fresh from disk. No in-memory caching in v1. The vault is small; caching is premature.
- Folder-scope by default. Default scope
[Notes/, skill-lab/]. ExcludingRaw/,_meta/,_bases/,_templates/,00-Inbox/by default. Override via explicitfoldersparameter. - Honest about confidence and staleness. Return
confidenceandlast_verifiedin every result. Apply them as filters when requested, but do not rank — the caller decides what matters. - Excerpts by default. Full bodies opt-in via
full_body: true. Defaultexcerpt_chars: 300. status: archivedexcluded by default. Opt-in viainclude_archived: true.- Distinguish "not found" from "couldn't search." No matches is a finding. Failed to access the vault is an error.
- No semantic search in v1. Substring + regex + frontmatter filter + 1-hop wikilink graph only.
How to Run
Input
| Parameter | Required | Notes |
|---|---|---|
vault_path |
yes | Absolute path to the vault root |
operation |
yes | One of: lookup, filter, graph, text-search |
slug |
conditional | Required for lookup and graph |
where |
conditional | Required for filter; map of field: value constraints. Field names and values validated against schema |
pattern |
conditional | Required for text-search; substring (default) or regex (with regex: true) |
regex |
optional | Boolean. Default false. When true, pattern is treated as a regex |
folders |
optional | List of subfolder names relative to vault root. Default ["Notes/", "skill-lab/"]. Special value ["all"] includes all vault folders |
full_body |
optional | Boolean. Default false |
excerpt_chars |
optional | When full_body: false. Default 300 |
max_results |
optional | Cap on results. Default 25 |
min_confidence |
optional | One of confirmed | likely | contested | speculative | unverifiable. Notes whose confidence field meets-or-exceeds this in the ordering (confirmed > likely > contested > speculative > unverifiable) pass. Notes missing the confidence field are excluded by this filter |
max_staleness_days |
optional | Filter to notes whose last_verified >= today - N. Notes with empty or missing last_verified pass through (never-verified ≠ stale) |
include_archived |
optional | Boolean. Default false |
resolve_wikilinks |
optional | For graph, whether to resolve [[name]] to absolute paths. Default true |
known_skill_refs |
optional | For graph and unresolved-link reporting, a list of names that should be tagged as skill-library-ref rather than unresolved. Caller-provided |
Steps
Step 1 — Read the vault schema
Read <vault_path>/_meta/frontmatter-schema.md. Extract:
- The
type:enum values (from the Type registry section) - The
domain:enum values (from the Domain registry section) - The
status:enums per type - The
confidence:andmaturity:enum values (from extension sections) - The
source_type:enum
Read <vault_path>/CLAUDE.md if present to detect any vault-specific overrides.
If the schema file is missing, fail the call before any further work: this is not a vault that vault-reader can operate against.
Step 2 — Validate query parameters
operationis one of the four valid values- Conditional parameters present for their operation (slug for lookup/graph; where for filter; pattern for text-search)
- For
filter: every key inwhereis a field name documented in the schema; every value inwherefor an enum field is a valid enum value - For
min_confidence: value is in the confidence enum - For
folders: each folder (after resolving["all"]) exists as a directory under<vault_path>
Reject on any validation failure with a specific message pointing to the violating parameter and what the schema says is valid.
Step 3 — Execute the operation
3a — lookup
- For each folder in scope (in order), test
<vault_path>/<folder>/<slug>.md - First match wins; return full record with frontmatter + body (or excerpt)
- If no match, return empty results with explicit "tried these folders" in the report
3b — filter
- Walk all
.mdfiles in scoped folders (Glob) - For each file, parse frontmatter (first
----delimited YAML block) - Skip files without frontmatter; count them in the skipped list for the report
- Apply each predicate in
where: a note matches when every predicate evaluates true- Scalar fields: exact match (
domain: metamatches when frontmatter hasdomain: meta) - List fields (tags): set intersection (
tags: [pkm]matches whenpkmis in the note's tags)
- Scalar fields: exact match (
- Return matching set
3c — graph
- Read the target note (same lookup logic as 3a)
- Extract outbound wikilinks from the body via regex:
\[\[([^\]|]+)(\|[^\]]+)?\]\] - For each, if
resolve_wikilinks: true, searchNotes/<name>.md,skill-lab/<name>.md,_meta/<name>.mdin that order; tag resolution as one ofvault | skill-library-ref | unresolved - For inbound: grep across scoped folders for
\[\[<target-slug>(\|[^\]]+)?\]\] - Return both lists with resolution categories
3d — text-search
- Walk all
.mdfiles in scoped folders - For each file, read post-frontmatter body (strip the leading
---block) - Apply
patternas substring (default) or regex (whenregex: true) - For each match, capture line number and 2 lines of surrounding context
- Return matches grouped by file
Step 4 — Apply post-filters
include_archived: falseexcludes notes withstatus: archivedmin_confidence: applies the confidence ordering filter (notes withoutconfidenceexcluded by this filter when set)max_staleness_days: applies thelast_verifiedfilter (notes withoutlast_verifiedset, or withlast_verified: "", pass)
Step 5 — Apply max_results cap
Truncate to max_results. Report the truncation count in the report's Limitations section.
Step 6 — Compose the retrieval report
Read references/retrieval-report-format.md for the format spec.
Output
A structured retrieval report (see references/retrieval-report-format.md):
RETRIEVAL REPORT — vault-reader
────────────────────────────────
Query: {operation: <op>, parameters: <…>}
Searched: <N> markdown files across <M> folders [<folder list>]
Schema validated against: <vault_path>/_meta/frontmatter-schema.md (modified <YYYY-MM-DD>)
Matched: <K> notes (after filters)
Results:
1. <absolute path>
- frontmatter: {type, domain, status, confidence, maturity, last_verified, tags}
- excerpt: "<first <excerpt_chars> chars of body>"
- inbound links: <count> [list if requested via graph op]
- outbound links: <count> [list if requested via graph op]
2. ...
Skipped (no valid frontmatter):
- <path>
(or: "(none)")
Limitations:
- Folders not searched: <list>
- Filters applied: <list>
- Truncated to <max_results>; <N_total> total matches before cap (when applicable)
Status: success | partial | failed
Error Handling
| Failure | Response |
|---|---|
| Schema file missing | Fail before any read; report <vault_path>/_meta/frontmatter-schema.md not found |
| Vault path is not a directory | Fail with the resolved path |
where contains field not in schema |
Fail; list valid field names |
where value not in enum (e.g., domain: investng) |
Fail; list valid enum values for that field |
slug for lookup matches no file in any scoped folder |
Return success with empty results + "tried" list; not an error |
regex flag set but pattern is not a valid regex |
Fail with the regex compile error |
folders includes a path that doesn't exist |
Fail with the missing folder name |
| File has no frontmatter | Skip; counted in report's Skipped section |
Note matches where but is status: archived and include_archived: false |
Excluded; counted in report's archived-excluded line |
Scope Boundaries
vault-reader handles: Single-query reads against an Obsidian vault following CLAUDE.md conventions. Frontmatter parsing and validation against the schema. The 4 operations above. Confidence and staleness filters. Retrieval reporting with audit trail.
vault-reader does NOT:
- Write to the vault (use vault-writer)
- Create cache files, indexes, or any side effect on disk
- Run embedding-based / semantic / similarity search (deferred to v2; current
data/skill_embeddings.npyis empirically broken) - Run multi-hop graph traversal (1-hop only in v1)
- Aggregate or summarize across results (caller's job)
- Execute
.basequeries (read them as files if scoped, but don't interpret) - Rank results (return order is filesystem walk order plus tie-breakers; caller ranks if needed)
- Resolve cross-vault links (single vault per call)
- Mutate any global state, including in-memory caches in v1