English | Español
headless-vault-kit
Puts Obsidian's own functionality back on a vault that lives on a headless server, where the app never opens: a SQLite index, backlinks, Bases queries, and agent-driven automation running 24/7. The CLI installs as
hvk.
The problem
Move an Obsidian vault to a headless server — a machine with no screen, so an agent and its automations can work on the notes around the clock — and the files arrive fine: Obsidian Headless keeps them in sync. What never happens is Obsidian itself opening, and with it you lose everything the app computes at startup: backlinks, Dataview queries, Bases, the CLI, plugins. The result: synced notes, and nothing that can answer a question about them.
What it does
Don't emulate Obsidian — replicate its data. Everything the app derives at startup is state that can be rebuilt from the files themselves. This project rebuilds it on the server:
- Indexer: parses the vault into SQLite the same way the app's metadata cache does (frontmatter, tags, links, backlinks, tasks, headings, full text), with incremental updates as sync delivers changes.
hvkCLI: search, backlinks, tasks and properties in milliseconds, so agents can query the vault without burning tokens reading files one by one.- Queries without the app: Bases (
.base) executed against the index, plus materialized views rendered into your notes as Markdown — visible from any device. - The vault as a job queue: order-notes with their state in frontmatter; a runner executes them with Claude Code and the results sync back to all your devices.
- Harness: permissions, hooks and auditing via Claude Code's native features + git.
- MCP server:
hvk mcpserves the vault to any MCP client over stdio — read-only by default, and able to write only when the instance was started with--write.
The scope is governed by a three-tier model: the app's native behavior is replicated exactly; Obsidian's official formats (Bases, Canvas, templates) get full support; and the most popular community plugins are included only when their state lives in parseable files — everything else goes through an extensible parser interface so anyone can contribute an adapter. Obsidian Kanban boards are read through that interface as the worked example. Plugin code is never executed and the UI is never reproduced.
Requirements
There are two different things you might want, and they ask for very different amounts.
To use the hvk command — index a vault, ask it questions, materialise views, run jobs:
| Python | 3.11 or newer, and nothing else |
| Operating system | Linux, macOS or Windows. Tested on Linux and Windows |
| Obsidian | Not required. hvk reads the files; the app never has to be installed or open |
| A vault | Any folder of Markdown. A .obsidian/ directory is only needed if you want hvk to find the vault by itself |
To run the whole 24/7 system on a server — sync, an agent on Telegram, scheduled jobs — you also need Linux with systemd, Node.js 22+, Bun, tmux, git and an Obsidian Sync subscription. That has its own runbook and its own preflight check — do not start there.
Install
Not on PyPI yet, so both routes install from this repository. Pick one.
A. As a command, with uv — recommended if you just want to
use it. hvk lands on your PATH in its own isolated environment:
uv tool install --from git+https://github.com/angelsaez/headless-vault-kit headless-vault-kit
uv tool upgrade headless-vault-kit updates it later; uv tool uninstall headless-vault-kit
removes it completely.
B. From a checkout — if you want to read the code, change it, or run the tests:
git clone https://github.com/angelsaez/headless-vault-kit
cd headless-vault-kit
python -m venv .venv
Then, on Linux or macOS:
.venv/bin/pip install -e ".[dev]"
.venv/bin/hvk --version
On Windows (PowerShell):
.venv\Scripts\pip install -e ".[dev]"
.venv\Scripts\hvk --version
In Git Bash use forward slashes instead: .venv/Scripts/pip, .venv/Scripts/hvk.
The [dev] part adds pytest and nothing else. Leave it out if you are not running tests.
Check it worked
Point it at a vault — a real one is fine, hvk only reads, and its index is written outside the vault (ADR-0002):
hvk --vault /path/to/vault scan
hvk --vault /path/to/vault info
hvk --vault /path/to/vault backlinks "Some Note"
scan prints how many files it indexed and how long it took; on a few hundred notes that is
well under a second. If backlinks names the notes you expected, everything below this line
works too.
Two things worth knowing straight away:
- Run it inside a vault and
--vaultcan be dropped. hvk walks up from the working directory until it finds a.obsidian/folder. hvk rebuildis always safe. The index is derived from your files and nothing else, so deleting it costs time and nothing more. Nothing inscan,search,backlinks,links,tags,tasks,props,orphans,baseorinfoever writes to your vault; onlyviews --applyandjobs --rundo, and both say so in their names.
| Command | What it answers |
|---|---|
hvk scan / hvk rebuild |
Index new and changed files, or rebuild from scratch |
hvk search "text tag:project path:Areas" |
Full-text search, with optional tag and path filters |
hvk backlinks "Note" |
What links here, by note name or by path |
hvk links [Note] [--broken] [--ambiguous] |
Outgoing links, unresolved ones, or ones where more than one file matched |
hvk tags [--count] [--prefix home] |
Every tag, with how many files carry it; a prefix includes nested tags |
hvk tasks [--pending] [--due-before 2026-09-01] |
Tasks across the vault, by state, due date or path |
hvk props --where "status=open" |
Files by property; repeat --where to combine with AND, or omit it for the catalogue of keys |
hvk orphans [--attachments] |
Files nothing links to |
hvk watch |
Index changes as they land, until interrupted; meant to run as a service |
hvk verify |
Re-hash every file as a safety net; run it nightly from cron |
hvk base File.base [--view Name] |
Run a view from a .base file against the index, as a Markdown table |
hvk views [Path] [--apply] |
Refresh the base tables materialised inside notes; without --apply it only lists what is stale |
hvk canvas Board.canvas [--edges] |
What is on a whiteboard: its boxes, or the arrows between them |
hvk canvas B.canvas --add-note N [--connect A B] [--create] --apply |
Put notes, text boxes and arrows on a whiteboard. Only ever adds: nothing already on the board is moved or removed |
hvk dql "LIST FROM #x" | --note N.md |
Run a Dataview query, or every dataview block in a note, against the index |
hvk jobs --dir D --profiles P [--run] |
Run the order-notes waiting in a directory; without --run it only reports |
hvk doctor [--jobs-dir D] |
Is this installation healthy? For calling from monitoring you already have |
hvk guard [--protect F] |
A PreToolUse hook: refuses rm in favour of .trash/, writes that land outside the vault, and folders you nominate. Refusals are recorded |
hvk mcp [--write] [--protect F] |
Serve the vault to any MCP client over stdio. Read-only unless --write, in which case the writing tools appear; the same guard rules apply and every write is recorded |
hvk info |
What the index currently holds |
Every command takes --json for machine-readable output; hvk watch emits JSON Lines, one
object per batch, so it can be piped into a log.
To keep the index current, run hvk watch as a service and re-hash nightly from cron:
17 4 * * * hvk --vault /path/to/vault verify
*/30 * * * * hvk --vault /path/to/vault views --apply
The second line is what keeps materialised views current. It is safe to run as often as you
like: it writes only what actually changed, and nothing at all when nothing did. Both lines,
the order-note runner and a nightly backup are installed for you by deploy/install.sh.
A systemd unit for the watcher, and everything else needed to run this on a server, is in
deploy/.
Once this is on PyPI, installing it will be uv tool install headless-vault-kit and
nothing else. The command stays hvk: what a package is called and what it puts on your
PATH are two different names, and the long one is the descriptive one.
The complete guide
Every command, what each part is for, the worked cases, and the two-language vocabulary: docs/GUIDE.md — in Spanish, docs/GUIDE.es.md.
Repository layout
src/hvk/ The package: indexer, parsers, the write layer, the MCP server and the CLI
tests/ pytest, run against the synthetic vaults below
test-vaults/ Synthetic vaults, including the awkward cases: Unicode, odd YAML,
duplicate headings, ambiguous and broken links
deploy/ systemd user units, cron and the runbook for putting it on a server
tools/ Development utilities, not part of the product (vault mirror, testbed)
skills/ Claude Code skills, so an agent knows which command to reach for
docs/adr/ Architecture decision records — the "why" behind every design choice
docs/ CHANGELOG.md, the repository journal
Written in Python 3.11+ (ADR-0001), with ruamel.yaml
and watchdog as its only runtime dependencies.
Contributing
Pull requests are welcome, and so are bug reports and questions. CONTRIBUTING.md has the whole of it: how to run the suite, the rules that are not negotiable, the one-ADR-per-decision working rule, and how to write a parser adapter for a format this does not read yet.
Contributions are licensed under this project's own MIT, and there is no CLA.
If you are reading the code, the tests are the map:
.venv/bin/pytest # the suite, a few seconds
.venv/bin/pytest -m slow # performance, against a generated 10,000-note vault
Every push and pull request runs the suite on Python 3.11 and 3.13, installs the built package
both with pip and with uv tool install and checks each answers against a vault it has never
seen, and parses every shell script (the workflow). Linux only:
Linux is where this is meant to run.
The deployment is not exercised in CI — it needs a systemd user instance and a machine to throw
away. It lives in tools/testbed/, a disposable Debian container, and that is
where to run deploy/selftest.sh before trusting a change to deploy/.
Name and command
The repository and tool are headless-vault-kit (descriptive, self-explanatory); the
CLI binary is hvk (hvk search, hvk backlinks, hvk base "...") — long clear repo
name, short comfortable command.
Releasing
How a version reaches PyPI, and why there is no API token in this repository: docs/RELEASING.md.
How far along this is
What is built, what is postponed and how mature it is: docs/ROADMAP.md.
License
MIT. Do what you like with it, including commercially; keep the copyright notice, and there is no warranty. The full text is in LICENSE.
Both runtime dependencies are permissive too — ruamel.yaml is MIT and watchdog is
Apache-2.0 — so nothing here constrains what you build on top.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file headless_vault_kit-0.1.0.tar.gz.
File metadata
- Download URL: headless_vault_kit-0.1.0.tar.gz
- Upload date:
- Size: 365.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
de52921c8ccc45ec9c0ba8491252225a0cacaa91dde06611ce6b58256a046a4b
|
|
| MD5 |
f55427e564ac4d7f6c246ede8895e7ba
|
|
| BLAKE2b-256 |
4090101873fe2a0cf580f98a93c5b669c6a8d885183b56f17cf3ef1909b3fa3c
|
Provenance
The following attestation bundles were made for headless_vault_kit-0.1.0.tar.gz:
Publisher:
release.yml on angelsaez/headless-vault-kit
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
headless_vault_kit-0.1.0.tar.gz -
Subject digest:
de52921c8ccc45ec9c0ba8491252225a0cacaa91dde06611ce6b58256a046a4b - Sigstore transparency entry: 2589389790
- Sigstore integration time:
-
Permalink:
angelsaez/headless-vault-kit@3e7dd62e3db38ba4af8d44ba47fd94757b239b84 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/angelsaez
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@3e7dd62e3db38ba4af8d44ba47fd94757b239b84 -
Trigger Event:
push
-
Statement type:
File details
Details for the file headless_vault_kit-0.1.0-py3-none-any.whl.
File metadata
- Download URL: headless_vault_kit-0.1.0-py3-none-any.whl
- Upload date:
- Size: 132.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9b4fedb49b5142ce417835609193190f347298e531d8c6c164119edbac0ed378
|
|
| MD5 |
85e4f07cd36122ce913c06acc0fbf47b
|
|
| BLAKE2b-256 |
1d9efc2038276e6bb92ef81209cd33f49fde5113bc07450f0fe94d3d83d8c28e
|
Provenance
The following attestation bundles were made for headless_vault_kit-0.1.0-py3-none-any.whl:
Publisher:
release.yml on angelsaez/headless-vault-kit
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
headless_vault_kit-0.1.0-py3-none-any.whl -
Subject digest:
9b4fedb49b5142ce417835609193190f347298e531d8c6c164119edbac0ed378 - Sigstore transparency entry: 2589390059
- Sigstore integration time:
-
Permalink:
angelsaez/headless-vault-kit@3e7dd62e3db38ba4af8d44ba47fd94757b239b84 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/angelsaez
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@3e7dd62e3db38ba4af8d44ba47fd94757b239b84 -
Trigger Event:
push
-
Statement type: