name: obsidian-cli
description: Inspect, control, and edit a running Obsidian app/vault via the obsidian CLI (Chrome DevTools Protocol against the live app). Use to read, edit, create, or open notes; insert attachments or images; enable, disable, reload, or debug plugins; verify rendering with screenshots; list unresolved links or orphans; or run JavaScript against the Obsidian API. Trigger whenever the user wants something done to or checked in Obsidian itself — note edits, plugin state, rendering problems, screenshots — rather than in the vault's files on disk. Not for designing vault structure (obsidian-vault-structure) or Excalidraw drawings (obsidian-excalidraw).
Obsidian CLI
The obsidian CLI wraps Chrome DevTools Protocol around the running Obsidian app. It ships inside the app bundle at /Applications/Obsidian.app/Contents/MacOS/obsidian-cli; if obsidian is not on PATH, link it once (ln -s /Applications/Obsidian.app/Contents/MacOS/obsidian-cli /usr/local/bin/obsidian) so the bare name below works. It operates on whichever vault is currently open (override with vault=<name>).
Discover commands & basics
This skill does not list every command — obsidian --help is the source of truth (~100 commands). Use it.
obsidian --help # full command list with categories
obsidian <cmd> --help # per-command flags
obsidian vault info=path # active vault path — check before editing
Rough categories: file ops (open, create, read, append, rename, delete), metadata (aliases, backlinks, links, properties, tags, outline), vault-wide queries (unresolved, orphans, deadends, search), plugins/themes/snippets (plugin:*, plugins:*, theme:*, snippet:*), bases (base:*), daily notes (daily:*), workspace (tab:open, tabs, recents, history:*), and dev (dev:*, eval, command).
path=is an exact vault-relative path.file=resolves like wikilinks.- Quote shell-special values —
path="My Note.md",code='...'with single quotes around the JS body. - Use
obsidian read path=<note.md>for quick reads, but local file reads are fine when you need line numbers or large ranges.
Editing notes
For append/prepend-only changes, prefer CLI commands:
obsidian append path="note.md" content="New content"
obsidian prepend path="note.md" content="New content"
For precise in-place edits, use obsidian eval with the Obsidian API so the change still goes through Obsidian:
obsidian eval code='(async () => {
const note = app.vault.getAbstractFileByPath("note.md");
if (!note) throw new Error("Note not found");
let text = await app.vault.read(note);
text = text.replace("old text", "new text");
await app.vault.modify(note, text);
return note.path;
})()'
Adding images or attachments
When the user asks to insert an image under a specific heading/list item:
- Resolve the vault path with
obsidian vault info=path. - Identify the source image path. If none was given, check the clipboard (
osascript -e 'clipboard info') and obvious recent files (Desktop, Downloads), visually confirming a candidate withread; if that turns up nothing, ask for the path rather than guessing. - Copy the binary into the vault using
obsidian evalandapp.vault.createBinaryorapp.vault.modifyBinary. - Insert an Obsidian embed link like
![[image-name.png]]at the requested note location. - Verify with
obsidian file path=<image-name.png>, re-read the edited note section, and report the final note path plus the inserted attachment name.
Example:
obsidian eval code='(async () => {
const fs = require("fs");
const sourcePath = "/absolute/path/to/source.png";
const imagePath = "flow-builder-edge-styling-reference.png";
const notePath = "flow-builder-vertical-lifecycle-plan.md";
const source = fs.readFileSync(sourcePath);
const imageData = source.buffer.slice(source.byteOffset, source.byteOffset + source.byteLength);
const existingImage = app.vault.getAbstractFileByPath(imagePath);
if (existingImage) {
await app.vault.modifyBinary(existingImage, imageData);
} else {
await app.vault.createBinary(imagePath, imageData);
}
const note = app.vault.getAbstractFileByPath(notePath);
if (!note) throw new Error("Note not found");
let text = await app.vault.read(note);
const link = "![[flow-builder-edge-styling-reference.png]]";
if (!text.includes(link)) {
const lines = text.split("
");
const questionIndex = lines.findIndex((line) => line.startsWith("2. Edge styling."));
if (questionIndex < 0) throw new Error("Target list item not found");
lines.splice(questionIndex + 1, 0, "", " " + link);
await app.vault.modify(note, lines.join("
"));
}
return imagePath;
})()'
Plugin control (native commands)
obsidian plugin id=obsidian-excalidraw-plugin # name, version, enabled, description
obsidian plugins:enabled # list enabled plugin IDs
obsidian plugins # list all installed plugin IDs
obsidian plugin:enable id=<id> # persists across restarts
obsidian plugin:disable id=<id>
obsidian plugin:reload id=<id> # after editing plugin source
obsidian plugin:install id=<id> # from community catalog
Dev-tools
obsidian dev:errors # captured plugin/runtime errors
obsidian dev:console level=error # captured console output
obsidian dev:screenshot path=/tmp/obs.png
obsidian dev:dom # dump current DOM (large)
obsidian dev:cdp # raw Chrome DevTools Protocol passthrough
obsidian devtools # open DevTools window
Operational judgment
Things that aren't in --help but matter:
- Before filing a "rendering looks wrong" bug, check the plugin is enabled. Installed-but-disabled is silent and common —
obsidian plugin id=<id>shows theenabledfield. - Use
dev:screenshotto verify visual state, then read the PNG back with your image-capable reader. This is the only way to confirm Excalidraw canvases, Dataview blocks, and embedded files actually rendered. obsidian evalis the escape hatch. Reach for it only when no native command does the job — open, read, plugin enable, and most other common operations have first-class commands.
Eval escape hatch
obsidian eval code='<js>' runs JavaScript in the Electron renderer with full access to app.*. Return value is JSON-serialized — wrap objects in JSON.stringify, and wrap async work in (async () => { ... })().
Most useful entry points:
app.workspace.getActiveFile()— currently open file (TFile)app.vault.getAbstractFileByPath(path)— TFile or null (path is vault-relative)app.vault.read(file)/app.vault.modify(file, contents)— read/write contentsapp.vault.createBinary(path, data)/app.vault.modifyBinary(file, data)— write attachmentsapp.metadataCache.getFileCache(file)— frontmatter, headings, links, tagsapp.plugins.getPlugin("id")— plugin instance (with whatever helpers the plugin exposes)app.commands.executeCommandById("id")— invoke any command (obsidian command id=<id>is the native wrapper)
Plugins often expose helper APIs on the plugin instance. For Excalidraw: app.plugins.getPlugin("obsidian-excalidraw-plugin").ea is the ExcalidrawAutomate entry point (see obsidian-excalidraw skill).
