Obsidian skill
Agent Skill for Obsidian vault operations via the official CLI. Read, write, search, and mutate vault files and tasks.
npx -y skills add wernerbatt/obsidian-skillAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Obsidian vault operations via the official CLI. Read, write, search, and mutate vault files and tasks without direct file I/O.
SKILL.md
13.7 KB, ~3.9k tokens by cl100k_base, as published. Nobody here has run it
Obsidian Vault Operations
All vault reads and writes go through the Obsidian CLI binary so the app stays in sync. No direct file I/O.
Setup
- Install the Obsidian CLI (ships with Obsidian desktop)
- Copy
config.example.yamltoconfig.yamlin your project root (or the skill directory) and setobsidian_binandvault_name - Make sure Obsidian is running — the CLI connects to the running app
Operation Preference Order
- Native Obsidian CLI commands first —
read,search,tasks,task done,append,create, etc. - Skill script wrappers second —
scripts/obsidian_tasks.pyfor task mutations,scripts/obsidian_notes.pyfor note edits,scripts/obsidian_cli.pyfor lower-level helpers. - Raw
evallast — only when no native command or reusable wrapper exists yet.
If you find yourself about to write an ad hoc eval snippet for a mutation that could recur, suggest adding a reusable script first.
CLI Binary
Read obsidian_bin and vault_name from config.yaml:
# Read config (do this once per session)
OBS=$(python3 -c "import yaml; print(yaml.safe_load(open('config.yaml'))['obsidian_bin'])")
VAULT=$(python3 -c "import yaml; print(yaml.safe_load(open('config.yaml'))['vault_name'])")
If the active repo only has a different config shape (for example vault_path but not obsidian_bin / vault_name), fall back to discovery:
OBS=$(which obsidian)
$OBS vaults verbose
# Pick the vault name from the output, e.g. "Obsidian"
VAULT="Obsidian"
Every command follows the pattern:
$OBS vault=$VAULT <command> [args...]
Skill Script Wrappers
When running scripts, resolve paths relative to this skill's directory.
scripts/obsidian_tasks.py
Task-shaped operations that prefer native CLI first and hide any necessary eval internally.
# List filtered tasks via native tasks JSON + Python-side filtering
python3 scripts/obsidian_tasks.py list --status todo --context @quick --exclude-lowest --exclude-blocked
# Mark done (uses native `task ... done`)
python3 scripts/obsidian_tasks.py done \
--path 'Daily/2026-03-20.md' \
--match 'Buy groceries @quick'
# Cancel a task (checked + ❌ YYYY-MM-DD, no ✅)
python3 scripts/obsidian_tasks.py cancel \
--path 'Daily/2026-03-20.md' \
--match 'Old task I no longer need @quick'
# Rewrite a task body safely
python3 scripts/obsidian_tasks.py rewrite \
--path 'Daily/2026-01-05.md' \
--match 'Check next meeting @quick' \
--new-text 'Check next meeting @quick ⏬'
# Set priority safely
python3 scripts/obsidian_tasks.py set-priority \
--path 'Daily/2026-02-26.md' \
--match 'Research topic @quick ⏬' \
--priority high
scripts/obsidian_notes.py
Note-level mutations: insert text before/after markers, update frontmatter fields, replace text blocks. Avoids shell quoting issues by going through Python.
# Insert content before a marker line
python3 scripts/obsidian_notes.py insert-before \
--path 'References/My Note.md' \
--marker 'Section Two' \
--content '## New Section'
# Insert content after a marker line
python3 scripts/obsidian_notes.py insert-after \
--path 'References/My Note.md' \
--marker '## Phase 1' \
--content '### 1b. New sub-section'
# Replace text
python3 scripts/obsidian_notes.py replace \
--path 'References/My Note.md' \
--old 'old text' --new 'new text'
# Set a scalar frontmatter field
python3 scripts/obsidian_notes.py fm-set \
--path 'References/My Note.md' \
--field last --value 2026-04-04
# Append to a frontmatter list field
python3 scripts/obsidian_notes.py fm-append \
--path 'References/My Note.md' \
--field related --value '"[[Other Note]]"'
All commands support --dry-run.
scripts/obsidian_cli.py
Lower-level Python wrapper around native CLI commands and guarded eval helpers. Use when Python is more convenient than shell, but still prefer the higher-level wrappers above.
Bases / Base Files
Base filters can behave differently than expected via the CLI, so test filters incrementally.
Known-good simple filter pattern:
filters:
and:
- file.folder == "References"
- in_collection == true
Filters that may need extra caution / verification in CLI-driven workflows:
file.hasLink(...)file.hasTag(...)- path
includes(...)style expressions
Always verify with:
$OBS vault=$VAULT base:query path="Categories/My Base.base" format=md
Native Commands
List tasks
# All incomplete tasks (JSON with file, line, text, status)
$OBS vault=$VAULT tasks todo verbose format=json
# All completed tasks
$OBS vault=$VAULT tasks done verbose format=json
Mark task done
$OBS vault=$VAULT task path="Daily/2026-03-21.md" line=52 done
Read a file
$OBS vault=$VAULT read path="GTD/Dashboard.md"
Append to a file (EOF)
$OBS vault=$VAULT append path="GTD/Projects/My Project.md" content="- [ ] New task @quick"
Append to today's daily note
# Get today's daily note path
$OBS vault=$VAULT daily:path
# Append to it
$OBS vault=$VAULT append path="$($OBS vault=$VAULT daily:path)" content="- [ ] New task @quick"
Create a file
$OBS vault=$VAULT create path="Notes/New Note.md" content="# New Note"
# With overwrite
$OBS vault=$VAULT create path="Notes/New Note.md" content="..." overwrite
Note: create is reliable for markdown notes. For non-markdown artifacts such as standalone .html attachments, some setups may coerce the file to .md. If that happens, prefer writing the artifact directly into the vault attachment folder and then linking to it from a note.
Search
# Full-text search
$OBS vault=$VAULT search query="kitchen renovation"
# With line-level context
$OBS vault=$VAULT search:context query="@waiting"
# Scoped to folder
$OBS vault=$VAULT search query="next action" path="GTD/Projects"
List files
$OBS vault=$VAULT files folder="GTD/Projects"
Eval Patterns — Dataview Queries
Use eval to access the Dataview plugin API for structured queries. Wrap in an async IIFE and return JSON for parseable output.
All incomplete tasks (excluding someday/blocked)
$OBS vault=$VAULT eval 'code=
(async () => {
const dv = app.plugins.plugins["dataview"]?.api;
const excludeFolders = ["Checklists", "Templates", "Recurring"];
const today = new Date().toISOString().slice(0,10);
const tasks = dv.pages().file.tasks
.where(t => !t.completed)
.where(t => !t.text.includes("⏬"))
.where(t => !t.text.includes("⛔"))
.where(t => !excludeFolders.some(f => (t.path||"").includes(f)))
.where(t => {
const sm = t.text.match(/⏳\s*(\d{4}-\d{2}-\d{2})/);
return (!sm || sm[1] <= today);
});
const arr = tasks.array();
return JSON.stringify({count: arr.length, tasks: arr.slice(0,50).map(t => ({
path: t.path, line: t.line, text: t.text
}))});
})()'
Tasks by context tag
$OBS vault=$VAULT eval 'code=
(async () => {
const dv = app.plugins.plugins["dataview"]?.api;
const tag = "@quick";
const tasks = dv.pages().file.tasks
.where(t => !t.completed && t.text.includes(tag) && !t.text.includes("⏬"));
return JSON.stringify(tasks.array().map(t => ({
path: t.path, line: t.line, text: t.text
})));
})()'
Overdue tasks
$OBS vault=$VAULT eval 'code=
(async () => {
const dv = app.plugins.plugins["dataview"]?.api;
const today = new Date().toISOString().slice(0,10);
const tasks = dv.pages().file.tasks
.where(t => !t.completed && !t.text.includes("⏬"))
.where(t => {
const sm = t.text.match(/⏳\s*(\d{4}-\d{2}-\d{2})/);
const dm = t.text.match(/📅\s*(\d{4}-\d{2}-\d{2})/);
return (sm && sm[1] < today) || (dm && dm[1] < today);
});
return JSON.stringify(tasks.array().map(t => ({
path: t.path, line: t.line, text: t.text
})));
})()'
Completed tasks (last N days)
$OBS vault=$VAULT eval 'code=
(async () => {
const dv = app.plugins.plugins["dataview"]?.api;
const since = new Date(Date.now() - 7*86400000).toISOString().slice(0,10);
const tasks = dv.pages().file.tasks
.where(t => t.completed)
.where(t => {
const m = t.text.match(/✅\s*(\d{4}-\d{2}-\d{2})/);
return m && m[1] >= since;
});
return JSON.stringify({count: tasks.length, tasks: tasks.slice(0,20).array().map(t => ({
path: t.path, line: t.line, text: t.text
}))});
})()'
Stale projects (no next action)
$OBS vault=$VAULT eval 'code=
(async () => {
const dv = app.plugins.plugins["dataview"]?.api;
const projects = dv.pages("\"GTD/Projects\"").where(p => {
const status = String(p.status || "active").toLowerCase();
if (status !== "active") return false;
const tasks = p.file.tasks.where(t => !t.completed);
return tasks.length === 0;
});
return JSON.stringify(projects.map(p => p.file.path).array());
})()'
Eval Patterns — File Mutations
For line-level edits the CLI has no native command. Use eval with app.vault.process() which atomically reads, transforms, and writes the file.
Important: prefer scripts/obsidian_notes.py or scripts/obsidian_tasks.py over raw eval. These are fallback patterns for when no wrapper exists yet.
Edit a task line
$OBS vault=$VAULT eval 'code=
(async () => {
const path = "Daily/2026-03-21.md";
const lineNum = 52;
const newText = "- [ ] Updated task description @deep ⏳ 2026-03-25";
const f = app.vault.getAbstractFileByPath(path);
await app.vault.process(f, (content) => {
const lines = content.split("\n");
lines[lineNum - 1] = newText;
return lines.join("\n");
});
return "edited line " + lineNum;
})()'
Insert task under a heading
$OBS vault=$VAULT eval 'code=
(async () => {
const path = "Daily/2026-03-21.md";
const heading = "Day planner";
const task = "- [ ] New task @quick";
const f = app.vault.getAbstractFileByPath(path);
await app.vault.process(f, (content) => {
const lines = content.split("\n");
let headingIdx = -1;
for (let i = 0; i < lines.length; i++) {
if (lines[i].match(new RegExp("^#+\\\\s+" + heading))) {
headingIdx = i;
break;
}
}
if (headingIdx === -1) return content;
let insertIdx = headingIdx + 1;
for (let i = headingIdx + 1; i < lines.length; i++) {
if (lines[i].match(/^#+\s/)) break;
insertIdx = i + 1;
}
lines.splice(insertIdx, 0, task);
return lines.join("\n");
});
return "inserted under " + heading;
})()'
Move a task between files
$OBS vault=$VAULT eval 'code=
(async () => {
const srcPath = "Daily/2026-03-21.md";
const dstPath = "GTD/Projects/My Project.md";
const lineNum = 52;
const src = app.vault.getAbstractFileByPath(srcPath);
const dst = app.vault.getAbstractFileByPath(dstPath);
let taskLine = "";
await app.vault.process(src, (content) => {
const lines = content.split("\n");
taskLine = lines[lineNum - 1];
lines.splice(lineNum - 1, 1);
return lines.join("\n");
});
await app.vault.process(dst, (content) => {
return content.trimEnd() + "\n" + taskLine + "\n";
});
return "moved: " + taskLine.trim();
})()'
Task Metadata Format
Tasks follow Obsidian Tasks plugin emoji format:
- [ ] Description @context ⏳ 2026-03-25 📅 2026-03-30 🔼
Cancelled tasks use a custom convention:
- [x] Description @context ❌ 2026-03-29
Use ❌ YYYY-MM-DD only for cancelled tasks — do not add ✅ as well.
| Symbol | Meaning | Format |
|---|---|---|
⏳ | Scheduled date | ⏳ YYYY-MM-DD |
📅 | Due date | 📅 YYYY-MM-DD |
🛫 | Start date | 🛫 YYYY-MM-DD |
✅ | Done date | ✅ YYYY-MM-DD |
❌ | Cancelled date | ❌ YYYY-MM-DD |
⏫ | Highest priority | |
🔼 | High priority | |
| (none) | Normal priority | |
🔽 | Low priority | |
⏬ | Lowest / someday | |
🆔 abc | Task ID | |
⛔ abc | Depends on ID |
Context Tags
Define your own context tags. Common examples:
| Tag | Meaning |
|---|---|
@deep | Deep focus, 2+ hours |
@quick | Quick win, <15 min |
@batch | Group similar tasks |
@read | Reading tasks |
@waiting | Delegated / waiting |
@out | Errands / outside |
Safety Rules
- Never run parallel writes to the same file.
app.vault.process()is atomic per call, but two concurrent evals on the same file will race. - Always confirm before destructive ops (delete, move, bulk edit) unless the user has explicitly selected the action.
- Use match-based targeting over line numbers when possible — line numbers shift after edits.
- Prefer reusable wrappers over one-off scripts. If a raw
evalwould solve a recurring problem, suggest updating the scripts first. - For task cancellation, use the convention
- [x] ... ❌ YYYY-MM-DDwithout✅.
Troubleshooting
- Empty output from eval: The query may have timed out (default 30s) or returned too much data. Add
.slice(0, N)to limit results. - Exit code 255: Usually a JS error. Check quoting — single quotes around the whole
code=..., escape internal single quotes. - "Loading updated app package" noise: Ignore these lines; they're Electron startup messages.
- "CLI is unable to find Obsidian": Make sure Obsidian desktop is running. The CLI connects to the running app via IPC.
- Attachment links not opening as expected: Prefer wikilinks like
[[Attachments/file.html|Label]]over markdown links when linking local vault attachments. property:removebehaving inconsistently: In practice, prefer argument orderproperty:remove name=... path=....