VAULT
sokojh

Vault

ov — Agent-first CLI for Obsidian vaults. JSON-only output, schema introspection, --dry-run safety. Built for AI agents.

4 Stars
GitHub

ov — Obsidian Vault CLI

Agent-first CLI for Obsidian vaults. All output is JSON. Designed for AI agent consumption.

ov list --json '{"tag":"#devops","limit":5}' --fields title,path,tags
ov read --json '{"note":"docker","no_body":true}'
ov create --json '{"title":"K8s Guide","tags":["k8s","devops"],"sections":["Overview","Setup"],"dry_run":true}'
ov schema commands --fields name,has_side_effects

Install

Pre-built binaries (recommended)

Download from GitHub Releases:

# macOS (Apple Silicon)
curl -LO https://github.com/sokojh/obsidian-vault/releases/latest/download/ov-aarch64-apple-darwin.tar.gz
tar xzf ov-aarch64-apple-darwin.tar.gz
sudo mv ov /usr/local/bin/

# macOS (Intel)
curl -LO https://github.com/sokojh/obsidian-vault/releases/latest/download/ov-x86_64-apple-darwin.tar.gz
tar xzf ov-x86_64-apple-darwin.tar.gz
sudo mv ov /usr/local/bin/

# Linux (x86_64)
curl -LO https://github.com/sokojh/obsidian-vault/releases/latest/download/ov-x86_64-unknown-linux-gnu.tar.gz
tar xzf ov-x86_64-unknown-linux-gnu.tar.gz
sudo mv ov /usr/local/bin/

# Linux (ARM64)
curl -LO https://github.com/sokojh/obsidian-vault/releases/latest/download/ov-aarch64-unknown-linux-gnu.tar.gz
tar xzf ov-aarch64-unknown-linux-gnu.tar.gz
sudo mv ov /usr/local/bin/

From crates.io

cargo install obsidian-vault

From source

git clone https://github.com/sokojh/obsidian-vault.git
cd obsidian-vault
cargo install --path .

Setup

Set your vault path once:

# ~/.zshrc or ~/.bashrc
export OV_VAULT="$HOME/Library/Mobile Documents/iCloud~md~obsidian/Documents/MyVault"

Or pass --vault <path> to any command.

Agent-First Design

Why agent-first?

Traditional CLIs optimize for human discoverability and forgiveness. Agent CLIs optimize for predictability and defense-in-depth. This CLI is built from the ground up for AI agents:

  • JSON-only output — no colored/human formatting to parse
  • JSON payload input--json on every command, no flag guessing
  • Schema introspection — CLI describes itself at runtime
  • Deterministic matching — exact match by default, no fuzzy surprises
  • Structured errors — machine-readable codes + actionable hints
  • Safety by default--dry-run, --if-not-exists, path traversal blocking

Self-Discovery

An agent encountering ov for the first time can learn everything it needs:

# What commands exist? Which have side effects?
ov schema commands --fields name,has_side_effects,supports_dry_run

# How do I use "create"? What fields, what constraints?
ov schema describe --command create

# Give me a skill file to inject into my context
ov schema skill

Input Modes

Every command supports two input styles:

# Named flags
ov create --title "My Note" --tags "k8s,devops" --dir Zettelkasten

# JSON payload (preferred for agents)
ov create --json '{"title":"My Note","tags":["k8s","devops"],"dir":"Zettelkasten"}'

JSON input accepts arrays natively — "tags":["a","b"] and "tags":"a,b" are equivalent.

Output Contract

Every successful response:

{"ok":true, "count":1, "data":{...}, "meta":{"total":8, "has_more":false}}

Every error response:

{"ok":false, "error":{"code":"NOTE_NOT_FOUND", "message":"...", "hint":"Use --fuzzy flag"}}

Context Window Management

# Select only needed fields (saves ~84% tokens)
ov read --note "docker" --no-body --fields title,tags,links

# NDJSON streaming — one object per line, no wrapper
ov list --jsonl --fields title,tags

# Pagination
ov list --json '{"limit":10,"offset":0}'
# → check meta.has_more, increment offset

Safety Features

# Preview before writing (no side effects)
ov create --json '{"title":"Test","dry_run":true}'

# Idempotent create (safe to retry)
ov create --json '{"title":"Test","if_not_exists":true}'

# Exact match by default (no fuzzy hallucinations)
ov read --note "docker"        # exact match only
ov read --note "dock" --fuzzy  # opt-in fuzzy

Commands

CommandSide EffectsDry RunDescription
listList notes with filtering by dir/tag/date and sorting
readRead a note by name (exact match default)
searchFull-text search with prefix filters (requires index)
tagsList all tags with occurrence counts
statsVault-wide statistics
linksOutgoing [[wiki-links]] from a note
backlinksIncoming links pointing to a note
graphLink graph (JSON, DOT, or Mermaid format)
dailyYesYesOpen or create today's daily note
createYesYesCreate a new note (plain, frontmatter, or template)
appendYesYesAppend to an existing note (section-aware)
indexYesManage Tantivy search index (build/status/clear)
configYesGet or set configuration values
schemaIntrospect CLI schema (commands/describe/skill)

Global Options

--vault <PATH>       Vault path (or OV_VAULT env)
--json <JSON>        JSON payload input (alternative to flags)
--jsonl              NDJSON output (one object per line)
--fields <FIELDS>    Select specific output fields (comma-separated)

Quick Start

# Build search index (makes search ~25ms)
ov index build

# Browse notes
ov list --fields title,path,tags --limit 10
ov list --json '{"dir":"Zettelkasten","tag":"#devops","sort":"title"}'

# Full-text search
ov search --json '{"query":"docker networking","limit":5}'
ov search --json '{"query":"tag:#aws in:Zettelkasten"}'

# Read a note
ov read --note "docker"
ov read --note "docker" --no-body --fields title,tags,links

# Explore structure
ov tags --json '{"sort":"count","min_count":2}' --fields tag,count
ov stats --fields total_notes,unique_tags
ov links --json '{"note":"docker"}' --fields target
ov backlinks --json '{"note":"docker"}' --fields source,source_path
ov graph --json '{"center":"docker","depth":1,"graph_format":"mermaid"}'

# Create with dry-run first
ov create --json '{"title":"K8s Guide","tags":["k8s"],"sections":["Overview","Setup"],"dry_run":true}'
ov create --json '{"title":"K8s Guide","tags":["k8s"],"sections":["Overview","Setup"],"if_not_exists":true}'

# Append to a section
ov append --json '{"note":"K8s Guide","section":"Setup","content":"Install kubectl first"}'

# Daily note
ov daily --dry-run
ov daily

Search Prefixes

Combine free-text with structured filters:

ov search --json '{"query":"tag:#devops in:Zettelkasten kubernetes"}'
ov search --json '{"query":"title:아키텍처 date:2024-01"}'
ov search --json '{"query":"type:person"}'
PrefixExampleFilters by
tag:tag:#awsTag (auto-adds #)
in:in:ZettelkastenDirectory
title:title:kubeTitle substring
date:date:2024-01Modified date prefix
type:type:personFrontmatter type

Error Codes

All errors are JSON with machine-readable codes and actionable hints:

CodeExitMeaningHint
GENERAL_ERROR1Unclassified error
VAULT_NOT_FOUND2Vault path invalidSet OV_VAULT env or use --vault
INDEX_NOT_BUILT3Search index missingRun ov index build
QUERY_PARSE_ERROR4Invalid search query
ALREADY_EXISTS5Note already existsUse --if-not-exists
INVALID_INPUT6Bad input (path traversal, etc.)
MISSING_FIELD6Required field not providedUse ov schema describe

Note Creation

Templates

ov create --json '{"title":"John Doe","template":"person","vars":"role=SRE,team=Infra"}'

Templates live in Obsidian's template folder. Variables: {{title}}, {{date:YYYY-MM-DD}}, {{time:HH:mm}}, custom via vars.

Frontmatter

ov create --json '{"title":"Meeting","frontmatter":"{\"type\":\"meeting\",\"attendees\":[\"alice\",\"bob\"]}","tags":"meeting"}'

Note: template and frontmatter are mutually exclusive. vars requires template.

Section-Aware Append

ov append --json '{"note":"Project Log","section":"Timeline","content":"Deployed v2"}'

Inserts before the next same-or-higher level heading.

Architecture

src/
├── main.rs              # Entry point, Ctx struct, cmd_* handlers, schema definitions
├── cli/                 # clap derive args (all derive Deserialize for --json support)
│   ├── schema.rs        # Schema introspection (commands, describe, skill)
│   └── serde_helpers.rs # Custom deserializers (string_or_array)
├── vault/               # Vault scanning, exact/fuzzy matching, ObsidianConfig
├── extract/             # Note parsing, frontmatter, link/tag regex
├── model/               # Note, Link, Tag, Graph structs
├── index/               # Tantivy schema, reader, writer (incremental), tokenizer
├── search/              # Query parsing with prefix support
├── service/             # Shared business logic (list, tags, stats, backlinks)
├── config/              # AppConfig (TOML), XDG paths
└── output/              # JSON output (ApiResponse, ErrorResponse), field filtering

Key design decisions:

  • Agent-first: All output is JSON. No human/colored output. colored dependency removed.
  • Unified output: All commands use print_output()--fields works everywhere.
  • Index-first reads: list, tags, stats read from Tantivy index (zero file I/O), falling back to vault scan.
  • Parallel I/O: rayon::par_iter() for vault scanning when index unavailable.
  • Incremental indexing: File hash tracking (file_hashes.json) for fast rebuilds.
  • Input hardening: Path traversal blocking, control character stripping, constraint validation.

Performance

ScenarioTime
list/tags/stats (with index)~25ms
search (with index)~25ms
index rebuild (incremental, no changes)~19ms

The index-first architecture reads directly from Tantivy — zero file I/O for read-heavy commands. Always run ov index build first.

Configuration

Stored at ~/.local/share/ov/config.toml:

ov config --key vault_path
ov config --key vault_path --value "/path/to/vault"

Vault resolution priority: --vault flag > OV_VAULT env > config file > auto-detect (.obsidian/ walk-up or iCloud).

License

MIT

Related

How to Install

  1. Download the ZIP or clone the repository
  2. Open the folder as a vault in Obsidian (File → Open Vault)
  3. Obsidian will prompt you to install required plugins

Stats

Stars

4

Forks

0

License

MIT

Last updated 1mo ago

Tags

agent-firstai-agentclifull-text-searchjson-apiknowledge-managementllm-toolsmarkdownmcpnote-takingobsidianrusttantivyzettelkasten