app-dev/obsidian-cli/SKILL.md

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 valuespath="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:

  1. Resolve the vault path with obsidian vault info=path.
  2. 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 with read; if that turns up nothing, ask for the path rather than guessing.
  3. Copy the binary into the vault using obsidian eval and app.vault.createBinary or app.vault.modifyBinary.
  4. Insert an Obsidian embed link like ![[image-name.png]] at the requested note location.
  5. 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 the enabled field.
  • Use dev:screenshot to 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 eval is 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 contents
  • app.vault.createBinary(path, data) / app.vault.modifyBinary(file, data) — write attachments
  • app.metadataCache.getFileCache(file) — frontmatter, headings, links, tags
  • app.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).