Skip to content

Instantly share code, notes, and snippets.

@galligan
Created February 5, 2026 20:30
Show Gist options
  • Select an option

  • Save galligan/b186446d5df378a9d3de9c777a7d10b7 to your computer and use it in GitHub Desktop.

Select an option

Save galligan/b186446d5df378a9d3de9c777a7d10b7 to your computer and use it in GitHub Desktop.
obc-cli: Filesystem-first Obsidian vault CLI — project overview

obc-cli: Project Overview

What It Is

obc-cli is a command-line interface for managing Obsidian vaults. Its defining design principle is filesystem-first: the vast majority of operations work directly on vault files without Obsidian running. The Obsidian Local REST API plugin is only required for two capabilities: Dataview DQL queries and Obsidian command execution.

This reverses the typical dependency model. Most Obsidian tooling assumes the app is running — obc assumes it isn't, and gracefully adds REST API features when available.

TRADITIONAL:  CLI → REST API (required) → Obsidian → Files
OBC:          CLI → Filesystem (default) → Files
                  ↘ REST API (optional, for DQL/commands only)

Why It Exists

Obsidian's power lies in its plugin ecosystem and linking model, but interacting with a vault programmatically — from scripts, agents, or other CLIs — traditionally requires Obsidian to be running. obc eliminates that constraint for the operations that matter most: reading, writing, searching, patching content, managing periodic notes, and maintaining link integrity during renames and deletes.

The project was inspired by Yakitrak/obsidian-cli (759 stars), which proved that a filesystem-first approach works at scale.

Core Capabilities

File Operations

Full CRUD on vault files: read, write/overwrite, append, prepend, delete (to .trash/ or permanent), list directories, and batch-read multiple files. Content can come from positional arguments, --content/-c flags, --file flags, or stdin pipes — resolved in that priority order.

Smart Content Patching

The --at targeting system allows surgical modifications at specific locations within a file:

  • Headings (--at "h:Section Name") — Insert, prepend, or replace content under a specific heading. Supports partial matching with automatic resolution: if you target h:Notes and there's only one heading named "Notes" in the document, it resolves automatically. If ambiguous (multiple headings with the same name under different parents), it lists the full paths so you can specify h:Parent::Notes.
  • Block references (--at "block:my-block-id") — Target Obsidian's ^block-id annotations.
  • Frontmatter fields (--at "fm:tags") — Modify specific YAML frontmatter values, with support for array operations (append/prepend items to arrays).

The heading hierarchy is built by parsing markdown heading levels into :: delimited paths (e.g., # Parent## Child becomes Parent::Child), matching Obsidian's REST API heading path format.

Periodic Notes

Full lifecycle management for daily, weekly, monthly, quarterly, and yearly notes. The CLI reads the Periodic Notes plugin's data.json configuration directly from the vault's .obsidian/plugins/ directory to determine folder paths and filename formats (moment.js-style: YYYY-MM-DD, gggg-[W]ww, etc.).

Date input supports both ISO formats (2025-01-06, 2025-W02, 2025-Q1) and natural language aliases (today, yesterday, this-week, last-month, next-quarter). The format strings are converted to regex patterns for matching filenames when listing recent notes.

Wikilink Management

This is arguably the most critical capability. When you rename or move a note in Obsidian, the app updates all links pointing to it. obc replicates this behavior at the filesystem level.

Link parsing handles all Obsidian wikilink variants:

  • [[Note Name]] — Standard link
  • [[Note Name|Display Text]] — Aliased link
  • [[Note Name#Heading]] — Heading reference
  • [[Note Name#^block-id]] — Block reference
  • ![[Note Name]] — Embedded/transcluded note
  • ![[image.png]] — Embedded file

Rename flow (obc mv):

  1. Scan all *.md files in the vault (excluding .obsidian/, .trash/, .git/) in parallel batches of 50
  2. Parse wikilinks from each file, find all references to the old note name
  3. Update each reference in-place, preserving headings, aliases, block refs, and embed status
  4. Move the actual file
  5. Supports --dry-run for previewing changes and --skip-link-update for bare renames

Delete flow (obc rm):

  1. Scan for incoming links to the target note
  2. If links exist: refuse deletion unless --force is specified, showing which files would have broken links
  3. Delete moves to .trash/ by default (matching Obsidian's convention), with --permanent for hard deletes

Link inspection (obc links):

  • --incoming (default): Backlinks — which files link TO this note
  • --outgoing: Forward links — which files this note links TO
  • --broken: Vault-wide scan for links to non-existent notes

Search

Two search modes:

  • Text search (filesystem): Shells out to ripgrep with --json output, parses results with context, excludes .obsidian//.trash//.git/ automatically. Supports regex, case sensitivity, configurable context length, and result limits.
  • Dataview DQL (REST API): Passes raw DQL queries to the Obsidian REST API for evaluation by the Dataview plugin. This is one of only two operations that requires Obsidian.

Change Logging

All write operations log to .claude/logs/vault-changes.jsonl in the vault root. Each entry records a timestamp, the Claude session ID (from $CLAUDE_SESSION_ID environment variable), the affected file path, and the change type. This creates an audit trail when AI agents modify vault content.

Architecture

Tech Stack

  • Runtime: Bun — used for file I/O (Bun.file(), Bun.write()), glob scanning (Bun.Glob), and test runner (bun:test)
  • CLI framework: Commander.js — each command is a factory function (create*Command()) registered on the root program
  • Schema validation: Zod — config schema validation
  • Language: TypeScript with strict settings (noUncheckedIndexedAccess, noUnusedLocals, noUnusedParameters, noImplicitReturns)

Error Handling Pattern

All fallible operations return a Result<T, E> discriminated union rather than throwing:

type Result<T, E = Error> =
  | { ok: true; value: T }
  | { ok: false; error: E };

The filesystem layer (src/fs/) uses a more structured variant with error codes:

type FsOperationResult<T> =
  | { ok: true; value: T }
  | { ok: false; error: { code: "NOT_FOUND" | "IO_ERROR" | "AMBIGUOUS" | ...; message: string } };

Commands unwrap results at the boundary and exit with standardized codes: 0 (success), 1 (request/operation error), 2 (usage error).

Configuration

Config is loaded from a cascading search path:

  1. --config <path> flag (explicit)
  2. .obcconfig.yaml in current directory (vault-local)
  3. ~/.config/obc/config.yaml (XDG global)
  4. $OBSIDIAN_SKILLS_CONFIG environment variable (legacy)

Environment variables in config values are expanded using $VAR/${VAR} syntax. A .env file in the vault root is also loaded, with its values taking precedence over process.env.

Module Structure

src/
├── cli.ts                 Entry point, registers all commands
├── config.ts              Config loading (YAML, env expansion, .env)
├── types.ts               Result type, Zod config schema, error classes
├── utils.ts               ok/err constructors, prettyPrintJson
├── shared.ts              Command helpers (getConfigOrExit, executeAndExit, openInObsidian)
├── content-resolver.ts    Multi-source content input (flag → arg → stdin)
├── target-parser.ts       --at target parsing (h:, fm:, block:)
├── heading-resolver.ts    Heading hierarchy parsing and partial match resolution
├── block-resolver.ts      ^block-id extraction from markdown
├── smart-patch.ts         REST API patch with auto heading resolution + retry
├── date-parser.ts         Natural language + ISO date parsing for periodic notes
├── periodic-config.ts     Periodic Notes plugin config reader + moment.js format handling
├── change-logger.ts       JSONL audit log for vault modifications
├── http-client.ts         REST API HTTP client (auth, TLS, request/response)
│
├── fs/                    Filesystem operations layer
│   ├── operations.ts      Read, write, append, prepend, list, delete, frontmatter parsing
│   ├── patch.ts           Targeted content modification (heading/block/frontmatter)
│   ├── periodic.ts        Periodic note resolution, CRUD, recent listing
│   └── search.ts          ripgrep-based vault search
│
├── links/                 Wikilink management layer
│   ├── types.ts           WikiLink, RenameResult, ReferenceResult, ScanOptions
│   ├── parser.ts          Wikilink regex parsing, link replacement
│   ├── scanner.ts         Vault-wide reference scanning (parallel batched)
│   └── updater.ts         Rename-with-link-update orchestration
│
└── commands/              CLI command definitions (one file per command)
    ├── file.ts            show, list, create, append, prepend, patch, blocks, delete, batch
    ├── periodic.ts        show, update, append, prepend, patch, delete, recent
    ├── search.ts          text search + DQL subcommand
    ├── links.ts           incoming, outgoing, broken
    ├── mv.ts              rename/move with link updates
    ├── rm.ts              delete with link safety checks
    ├── status.ts          health check / capability report
    ├── open.ts            open file in Obsidian
    ├── cmd.ts             execute Obsidian commands (REST API)
    ├── vault.ts           vault info / recent files (REST API)
    └── active.ts          get active file from Obsidian (REST API)

What Requires Obsidian vs. What Doesn't

Capability Backend Obsidian Required?
File read/write/append/prepend/patch/delete Filesystem No
Periodic note CRUD + recent listing Filesystem No
Text search Filesystem (ripgrep) No
Wikilink analysis + rename with link update Filesystem No
Move, delete with link checks Filesystem No
Open file in Obsidian app obsidian:// URI No (but Obsidian must be installed)
Dataview DQL queries REST API Yes
Execute Obsidian commands REST API Yes
Get active file REST API Yes
Vault recent files (via DQL) REST API Yes

Running obc status reports which capabilities are available in the current environment.

Future Direction

The plan document (.scratch/plans/obc-v1.md) outlines a longer-term vision:

  • MCP Server: Extract the core logic into a shared obc-core package, then build an MCP (Model Context Protocol) server on top of it for AI agent integration. The MCP server would expose a single obsidian tool with action dispatch (read, write, search, list, periodic, open, status, help), following the "one tool, action dispatch, minimal tokens" pattern.
  • Daemon: An obc-daemon package providing a Hono-based HTTP server with bearer auth and rate limiting, enabling secure remote access to vault operations over Tailscale.
  • Frontmatter command: A dedicated obc fm command for frontmatter inspection and manipulation (--get, --set, --delete, --add for arrays). Currently frontmatter is modifiable through the --at "fm:key" target system, but a dedicated command would provide a more ergonomic interface.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment