Obsidian Cli
ASecurityUse when a task needs Claude to act on the Obsidian vault - read or search a note, list or count notes, inspect tags, tasks, links or properties, move or rename a file, or deposit a captured learning for the vault. Also use when a vault write appeared to succeed but the note is absent, when a numbered duplicate such as "Decisions 1.md" appears, or when Obsidian shows "A JavaScript error occurred in the main process". Skip for questions about the Obsidian GUI, themes, plugins or sync conflicts...
- 9 stars
- 0 votes
- 0 copies
- 1 view
- Added October 9, 2026
Works with
Security analysis
100/100Pro scans all 20 files and shows the line behind each finding
npx -y skills add LARi-UQAC/ResearchTools --skill obsidian-cli --agent claude-codeAre you the author of Obsidian Cli?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/lari-uqac-obsidian-cli-researchtools)---
name: obsidian-cli
description: Use when a task needs Claude to act on the Obsidian vault - read or search a note, list or count notes, inspect tags, tasks, links or properties, move or rename a file, or deposit a captured learning for the vault. Also use when a vault write appeared to succeed but the note is absent, when a numbered duplicate such as "Decisions 1.md" appears, or when Obsidian shows "A JavaScript error occurred in the main process". Skip for questions about the Obsidian GUI, themes, plugins or sync conflicts, where an explanation is wanted rather than a vault operation.
---
# Obsidian CLI
Vault root: `OBSIDIAN_VAULT` (default `C:\Martin Otis\Vault`). Write
vault-internal paths relative to that root, e.g.
`30_Ressources/Obsidian/_Convention_Capture.md`, never a drive letter.
## Writing to the vault
Write sequence:
1. Draft the note body to a scratch file, and keep the facts you were given,
verbatim, in a brief file.
2. Stage it with `scripts/note_fidelity.py --brief <brief> --note <draft>
--stage <slug>`, which writes `~/.claude/obsidian-outbox/<slug>.md` ONLY
when every number, identifier, file name, commit hash and `[[link]]` of
the draft appears in the brief, and otherwise stages nothing and exits 2
(measured 2026-09-26: one run's five drafts all carried invented facts,
and six writes had to be undone from the journal). It cannot see a claim
re-worded into its opposite, so the staged note is still read. First line:
`<!-- obsidian: create|append path="..." -->` - lowercase `obsidian:`,
whole line, nothing after `-->`, or the file is skipped silently and
stays in the outbox. Rest is content. Default to `create`: it degrades
to `append` on an existing file and skips a body already present, so a
replay never doubles the note.
3. `path=` is vault-relative only - no leading slash, no drive letter (a
leading slash escapes the join on Windows, landing outside the vault,
which the hook refuses). The outbox filename is scratch; only `path=`
decides the target. No folder needs preparing - the hook creates any
missing parent, so never `move` a note into place first.
4. `obsidian-outbox-flush.py` runs at SessionStart and SessionEnd only, not
continuously, so a note waits until one fires: never report a write as
done before it has. It processes files alphabetically, not by age;
competing appends follow filename order. It verifies the effect by file
size before/after and only then moves a file to `outbox/sent/`; one left
behind means the write did not land, and nothing is lost - it flushes at
the next start.
5. Obsidian watches the disk and reloads on its own once the file lands.
This is the write path, not a fallback.
### Editing one frontmatter property in place
`create`/`append` can only ADD a block after whatever is already in the file,
so both degrade to a second, dead `---` block on a note that already has
frontmatter - never the first block Obsidian and `daemon_graph.read_repo_property`
actually read. Setting or replacing a single existing key (for example `repo:`
on a project note) uses a third directive instead, added 2026-10-02 after a
staged `repo:` addition went out as `append` and would have landed in that
unread second block:
```
<!-- obsidian: set-property path="..." key="repo" -->
the-single-line-value
```
- `path=` is the same vault-relative rule as above, and the target note MUST
already exist - this directive never creates a file.
- `key=` is the frontmatter key to set or replace; every other key, the body,
and the line order are left untouched.
- The value is everything after the directive line, and it MUST be one line:
a value containing a CR or LF is refused before the file is touched, since
embedding it verbatim would insert extra frontmatter lines that a later
parse would misread as separate keys.
## Forbidden commands
`create`, `append`, `prepend` - including `daily:*` writes (`daily:append`,
`daily:prepend`): that daily-note layer was retired 2026-08-03. Also
`eval`, `dev:*`, `plugin:install`, `theme:install`, every `sync*` except
read-only `sync:history`.
`create`/`append`/`prepend` carry a measured reason:
- Failure is in the whole JSON header (content, path, `tty`/`cwd`), not
content alone: 3850 bytes passes, 4343 does not, 4096 - a Windows
named-pipe buffer - falls between. Reproduced on 1.13.4 and 1.13.7; trace
in `.claude/hooks/obsidian-outbox-flush.py`.
- Exits 0 on failure too: archives notes never written.
- `create` on an existing file writes a numbered duplicate
(`Decisions 1.md`) instead of failing.
Cause stays open: chunk reassembly, newline framing, and split UTF-8 were
checked and ruled out. Unproven: a client exiting before `drain`.
## Allowed surface
`read`, `search`, `list`, `property:get`, `property:set`, `tasks`, `links`,
`tags`, `move`, `rename`. Full syntax: `references/command-reference.md`.
Traps: `obsidian create --help` creates `Untitled.md`, not help - use
`obsidian help <command>`; parameters take no dashes (`path=`, `to=`,
`content=`); `obsidian move` to a missing folder fails `ENOENT` without
creating it, while exiting 0 - create the parent first.
Transport: Git Bash needs `~/bin/obsidian` (bare `obsidian` resolves to
`Obsidian.exe` and hangs); PowerShell calls `Obsidian.com` directly.
## Rationalization table
| Excuse | Reality |
|---|---|
| "Short note, under the threshold" | Threshold is the whole header; a note grows and breaks later. |
| "The command returned 0" | Returns 0 on failure too. Only a disk size change is evidence. |
| "create just for an empty index.md" | Forks a numbered duplicate on an existing file. Use the outbox. |
| "The reference documents append, so it's supported" | Documents an API, not this vault's measured constraints. |
| "Exit 0 is Unix convention for success" | Not measured here either: check the file's size before/after - the only real evidence. |
| "Empty stderr means no problem" | Measured empty on a run where the note never reached the vault. |
| "content= has no stated size limit" | Undocumented, not unmeasured: the limit is the header size. |
| "Command finished, so the write is done" | Hook checks size before/after; a bare call checks nothing. |
| "Colons unavailable, fall back to daily:path + append" | This vault's Windows Git Bash case, but `append` still risks truncation, targeting a daily layer retired 2026-08-03. |
## Where a captured learning goes
`scripts/vault_consolidate.py` (this skill's directory): deterministic half
of consolidation, measures shared tags/`domaine`/term overlap, proposes
links, decides nothing. `--mode links` additionally reports dead wiki-links
read-only, and `--apply <map.json> --yes` is the one guarded exception to
the outbox-only write rule: a dry-run-by-default, path-escape-refused,
map-validated, single-pass rewrite of existing links, run only by
`local-writer`.
New notes follow `30_Ressources/Obsidian/_Convention_Capture.md`: filed by
technology, never by project.
## Vault event daemon's ask queue (2026-09-26)
`vault_daemon.py` (this skill's `scripts/` directory) is the standalone
process that files raw knowledge drops into the vault. Beside that path, it
also drains a read-only ask queue (`daemon_ask.py`): a question written to
`~/.claude/obsidian-outbox/ask/requests/<id>.json`, answered by searching
the vault for relevant notes and calling the local LLM, with the answer
written to `ask/answers/<id>.json`.
This queue is gated to requests declaring `"from": "rt-dashboard"` -
anything else is answered `status: "refused"` rather than processed. It
calls `graphify query` (read-only, never `update` or `save-result`) to
answer graph-shaped questions: the vault is cross-project and a graph is
per-project, so the daemon resolves "the repository" from the vault hit's
own `index.md` property rather than assuming a fixed one (see the
`daemon-config.json` `ask_search_roots` and `ask_graph_*` keys). Vault
results and graph results are answered in two parts: vault published first
via an injected callback, then the graph portion.
This is the mechanism behind rt-dashboard's voice panel - see the
`rt-observe` skill's own SKILL.md for the other half of the exchange.
Files in this skill
- SKILL.md
- daemon-config.json
- note-fidelity.json
- references/command-reference.md
- scripts/Test/_daemon_fixtures.py
- scripts/Test/test_daemon_ask.py
- scripts/Test/test_daemon_classify.py
- scripts/Test/test_daemon_drains.py
- scripts/Test/test_daemon_graph.py
- scripts/Test/test_daemon_phantoms.py
- scripts/Test/test_daemon_queue.py
- scripts/Test/test_local_capability_probe.py
- scripts/Test/test_note_fidelity.py
- scripts/Test/test_outbox_io.py
- scripts/Test/test_vault_consolidate.py
- scripts/Test/test_vault_daemon.py
- scripts/Test/test_vault_daemon_e2e.py
- scripts/Test/test_vault_journal.py
- scripts/Test/test_vault_lock.py
- scripts/daemon_ask.py
Attribution
Comments
Loading comments…
Newsletter
Get the weekly list of safe new skills, and the ones we pulled.