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)
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.
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.
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 targeth:Notesand 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 specifyh:Parent::Notes. - Block references (
--at "block:my-block-id") — Target Obsidian's^block-idannotations. - 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.
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.
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):
- Scan all
*.mdfiles in the vault (excluding.obsidian/,.trash/,.git/) in parallel batches of 50 - Parse wikilinks from each file, find all references to the old note name
- Update each reference in-place, preserving headings, aliases, block refs, and embed status
- Move the actual file
- Supports
--dry-runfor previewing changes and--skip-link-updatefor bare renames
Delete flow (obc rm):
- Scan for incoming links to the target note
- If links exist: refuse deletion unless
--forceis specified, showing which files would have broken links - Delete moves to
.trash/by default (matching Obsidian's convention), with--permanentfor 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
Two search modes:
- Text search (filesystem): Shells out to ripgrep with
--jsonoutput, 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.
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.
- 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)
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).
Config is loaded from a cascading search path:
--config <path>flag (explicit).obcconfig.yamlin current directory (vault-local)~/.config/obc/config.yaml(XDG global)$OBSIDIAN_SKILLS_CONFIGenvironment 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.
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)
| 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.
The plan document (.scratch/plans/obc-v1.md) outlines a longer-term vision:
- MCP Server: Extract the core logic into a shared
obc-corepackage, then build an MCP (Model Context Protocol) server on top of it for AI agent integration. The MCP server would expose a singleobsidiantool with action dispatch (read,write,search,list,periodic,open,status,help), following the "one tool, action dispatch, minimal tokens" pattern. - Daemon: An
obc-daemonpackage 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 fmcommand for frontmatter inspection and manipulation (--get,--set,--delete,--addfor arrays). Currently frontmatter is modifiable through the--at "fm:key"target system, but a dedicated command would provide a more ergonomic interface.