
Obsidian
- 221 installs
- 6 repo stars
- Updated July 22, 2026
- julianobarbosa/claude-code-skills
Query and update Obsidian vault notes, follow wikilinks, and synthesize research from a personal knowledge base while planning features, specs, or competitive insights.
About
obsidian skill connects Claude Code to Obsidian vaults so agents can read linked Markdown notes, traverse backlinks, draft new research pages, and summarize scattered ideas into actionable briefs. It supports personal knowledge management during discovery, competitive review, and early product shaping.
- Vault-aware note search and retrieval
- Wikilink and backlink traversal
- Research synthesis across linked notes
- Markdown note creation and updates
- PKM workflows for product discovery
Obsidian by the numbers
- 221 all-time installs (skills.sh)
- +3 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #1,012 of 3,282 Productivity & Planning skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/julianobarbosa/claude-code-skills --skill obsidianAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 221 |
|---|---|
| repo stars | ★ 6 |
| Last updated | July 22, 2026 |
| Repository | julianobarbosa/claude-code-skills ↗ |
What it does
Query and update Obsidian vault notes, follow wikilinks, and synthesize research from a personal knowledge base while planning features, specs, or competitive insights.
Files
Obsidian
Overview
This skill provides comprehensive guidance for working with Obsidian, a powerful knowledge management and note-taking application. It covers vault structure, the Obsidian API for plugin development, URI scheme automation, markdown extensions, and integration with external tools via the Local REST API.
Quick Reference
Vault Structure
my-vault/
├── .obsidian/ # Configuration folder
│ ├── app.json # App settings
│ ├── appearance.json # Theme settings
│ ├── community-plugins.json # Installed plugins list
│ ├── core-plugins.json # Core plugin toggles
│ ├── hotkeys.json # Custom keybindings
│ ├── plugins/ # Plugin data folders
│ │ └── <plugin-id>/
│ │ ├── main.js # Compiled plugin code
│ │ ├── manifest.json
│ │ └── data.json # Plugin settings
│ └── workspace.json # Layout state
├── Notes/ # User notes (any structure)
├── Attachments/ # Images, PDFs, etc.
└── Templates/ # Template filesObsidian URI Scheme
Native Obsidian supports obsidian:// protocol for automation:
# Open a vault
obsidian://open?vault=MyVault
# Open a specific file
obsidian://open?vault=MyVault&file=Notes/MyNote
# Create a new note
obsidian://new?vault=MyVault&name=NewNote&content=Hello
# Search the vault
obsidian://search?vault=MyVault&query=keyword
# Open daily note
obsidian://daily?vault=MyVaultURI Parameters
| Parameter | Description |
|---|---|
vault | Vault name (required) |
file | File path without .md extension |
path | Full file path including folders |
name | Note name for creation |
content | Content to insert |
query | Search query |
heading | Navigate to heading |
block | Navigate to block reference |
Workflow Decision Tree
What do you need to do?
├── Automate Obsidian from external tools?
│ ├── Simple open/create operations?
│ │ └── Use: Native obsidian:// URI scheme
│ ├── Complex automation (append, prepend, commands)?
│ │ └── Use: Advanced URI plugin
│ └── Full programmatic access?
│ └── Use: Local REST API plugin
├── Build a plugin for Obsidian?
│ └── See: Plugin Development section
├── Work with vault files directly?
│ └── Use: obsidian-cli or direct file operations
├── Extend markdown syntax?
│ └── See: Markdown Extensions section
└── Query notes and metadata?
└── Use: Local REST API or Dataview pluginPlugin Development
Plugin Structure
my-plugin/
├── main.ts # Plugin entry point
├── manifest.json # Plugin metadata
├── package.json # npm dependencies
├── styles.css # Optional styles
├── tsconfig.json # TypeScript config
└── esbuild.config.mjs # Build configmanifest.json
{
"id": "my-plugin",
"name": "My Plugin",
"version": "1.0.0",
"minAppVersion": "1.0.0",
"description": "A sample plugin for Obsidian",
"author": "Your Name",
"authorUrl": "https://github.com/username",
"isDesktopOnly": false
}Basic Plugin Template
import { Plugin, Notice, MarkdownView } from 'obsidian';
export default class MyPlugin extends Plugin {
async onload() {
console.log('Loading plugin');
// Register a command
this.addCommand({
id: 'my-command',
name: 'My Command',
callback: () => {
new Notice('Hello from my plugin!');
}
});
// Register editor command
this.addCommand({
id: 'my-editor-command',
name: 'Insert Text',
editorCallback: (editor, view: MarkdownView) => {
editor.replaceSelection('Inserted text');
}
});
// Register event listener
this.registerEvent(
this.app.workspace.on('file-open', (file) => {
if (file) {
console.log('Opened:', file.path);
}
})
);
}
onunload() {
console.log('Unloading plugin');
}
}Core API Classes
| Class | Purpose | Access |
|---|---|---|
App | Central application instance | this.app |
Vault | File system operations | this.app.vault |
Workspace | Pane and layout management | this.app.workspace |
MetadataCache | File metadata indexing | this.app.metadataCache |
FileManager | User-safe file operations | this.app.fileManager |
Plugin Lifecycle
// onload() - Called when plugin is enabled
async onload() {
// Initialize UI components
// Register event handlers
// Set up commands
// Load settings
}
// onunload() - Called when plugin is disabled
onunload() {
// Cleanup is mostly automatic
// Custom cleanup for external resources
}Local REST API
The Local REST API plugin provides HTTP endpoints to interact with Obsidian programmatically.
Installation
1. Install "Local REST API" from Community Plugins 2. Enable the plugin 3. Configure API key in settings 4. Default endpoint: https://127.0.0.1:27124
Authentication
# Using API key header
curl -H "Authorization: Bearer YOUR_API_KEY" \
https://127.0.0.1:27124/vault/Common Endpoints
# List all files
GET /vault/
# Get file content
GET /vault/{path-to-file}
# Create/Update file
PUT /vault/{path-to-file}
Content-Type: text/markdown
Body: File content here
# Delete file
DELETE /vault/{path-to-file}
# Search vault
POST /search/simple/
Content-Type: application/json
Body: {"query": "search term"}
# Execute command
POST /commands/{command-id}
# Get active file
GET /active/
# Open file in Obsidian
POST /open/{path-to-file}Python Example
import requests
class ObsidianAPI:
def __init__(self, api_key, base_url="https://127.0.0.1:27124"):
self.base_url = base_url
self.headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
}
self.session = requests.Session()
self.session.verify = False # Self-signed cert
def list_files(self, path=""):
response = self.session.get(
f"{self.base_url}/vault/{path}",
headers=self.headers
)
return response.json()
def read_file(self, path):
response = self.session.get(
f"{self.base_url}/vault/{path}",
headers=self.headers
)
return response.text
def write_file(self, path, content):
response = self.session.put(
f"{self.base_url}/vault/{path}",
headers={**self.headers, "Content-Type": "text/markdown"},
data=content.encode('utf-8')
)
return response.status_code == 204
def search(self, query):
response = self.session.post(
f"{self.base_url}/search/simple/",
headers=self.headers,
json={"query": query}
)
return response.json()Markdown Extensions
Obsidian extends standard Markdown with special syntax:
Internal Links (Wikilinks)
[[Note Name]] # Link to note
[[Note Name|Display Text]] # Link with alias
[[Note Name#Heading]] # Link to heading
[[Note Name#^block-id]] # Link to block
[[Note Name#^block-id|alias]] # Block link with aliasEmbeds (Transclusion)
![[Note Name]] # Embed entire note
![[Note Name#Heading]] # Embed section
![[Note Name#^block-id]] # Embed block
![[image.png]] # Embed image
![[image.png|300]] # Embed with width
![[image.png|300x200]] # Embed with dimensions
![[audio.mp3]] # Embed audio
![[video.mp4]] # Embed video
![[document.pdf]] # Embed PDFCallouts
> [!note] Title
> Content here
> [!warning] Caution
> Important warning message
> [!tip]+ Expandable (default open)
> Click to collapse
> [!info]- Collapsed (default closed)
> Click to expand
# Available types:
# note, abstract, summary, tldr, info, todo, tip, hint,
# important, success, check, done, question, help, faq,
# warning, caution, attention, failure, fail, missing,
# danger, error, bug, example, quote, citeBlock References
This is a paragraph. ^block-id
# Reference this block from another note:
[[Note#^block-id]]Tags
#tag
#nested/tag
#tag-with-dashesFrontmatter (YAML)
---
title: My Note
date: 2024-01-15
tags:
- tag1
- tag2
aliases:
- alternate name
cssclass: custom-class
---
# Note content starts hereComments
%%This is a comment that won't render%%
%%
Multi-line
comment
%%Math (LaTeX)
Inline: $E = mc^2$
Block:
$$
\frac{-b \pm \sqrt{b^2 - 4ac}}{2a}
$$Mermaid Diagrams
````markdown
graph TD
A[Start] --> B{Decision}
B -->|Yes| C[Action 1]
B -->|No| D[Action 2]````
CLI Tools
obsidian-cli (Yakitrak)
# Install
go install github.com/Yakitrak/obsidian-cli@latest
# Commands
obsidian-cli open "Note Name" # Open note
obsidian-cli search "query" # Fuzzy search
obsidian-cli create "New Note" # Create note
obsidian-cli daily # Open daily note
obsidian-cli list # List notesobsidian-cli (Python)
# Install
pip install obsidian-cli
# Commands
obs vault list # List vaults
obs vault create <name> # Create vault
obs note search <query> # Search notes
obs settings export # Export settingsBest Practices
1. Use separate dev vault: Never develop plugins in your main vault 2. Hot reload plugin: Install for faster development iteration 3. Use registerEvent(): Ensures proper cleanup on unload 4. Prefer UIDs over paths: File paths can change; use unique identifiers 5. Handle async properly: Use await for vault operations 6. Test with Obsidian sandbox: Use BRAT plugin for beta testing 7. Follow manifest conventions: Keep id matching folder name 8. Version carefully: Update versions.json for compatibility
Troubleshooting
Plugin Not Loading
# Check console for errors
Ctrl+Shift+I (or Cmd+Option+I on Mac)
# Verify manifest.json is valid JSON
jq . manifest.json
# Check minAppVersion compatibility
# Ensure main.js exists in plugin folderURI Not Working
# URL encode special characters
# Spaces: %20
# Slashes: %2F
# Ampersands: %26
# Test with simple vault name first
obsidian://open?vault=testREST API Connection Failed
# Verify plugin is enabled
# Check API key is correct
# Confirm HTTPS and self-signed cert handling
# Default port: 27124Resources
References
references/uri-scheme.md- Complete URI scheme documentationreferences/plugin-development.md- Plugin development guidereferences/vault-structure.md- Vault and config structurereferences/markdown-extensions.md- Obsidian markdown syntaxreferences/api-reference.md- TypeScript API reference
Scripts
scripts/obsidian-vault.sh- Vault management utilitiesscripts/obsidian-api.py- Local REST API Python client
External Documentation
- Obsidian Developer Documentation
- Obsidian API Types
- Sample Plugin Template
- Local REST API Plugin
- Advanced URI Plugin
- Obsidian Help
---
Absorbed sub-skill (post-consolidation)
This skill now subsumes the former obsidian-master skill (the "unified orchestration" umbrella). Its content is preserved as deep reference:
| Subject | Path |
|---|---|
| Original orchestration SKILL.md (multi-step vault workflows) | References/master.md |
| Master skill's reference docs | References/master-reference/ |
| Master skill's workflows | References/master-workflows/ |
| Master skill's tools | References/master-tools/ |
The former obsidian-second-brain skill has been renamed to `obsidian-claude-integration` to reflect what it actually is: integration patterns for Claude Code + Obsidian, not a generic "second brain" concept.
Narrow siblings still apply:
- `obsidian-markdown` — syntax only (wikilinks, embeds, callouts, frontmatter, tags, LaTeX, Mermaid).
- `obsidian-bases` — Bases (.base YAML, table views, filters, formulas) only.
- `obsidian-nvim` — the
obsidian.nvimNeovim plugin only. - `obsidian-vault-management` — CRUD on vault contents (notes, templates, Dataview).
- `obsidian-claude-integration` — Claude Code ↔ Obsidian integration patterns.
---
Gotchas
- Wikilink `[[Note]]` resolution picks the first alphabetical match in the vault when duplicates exist — silently. Two notes named "Index" in different folders create an invisible ambiguity.
- `obsidian://` URI scheme requires the vault to be open in the desktop app — headless contexts (CI, Claude Code without Obsidian running) silently no-op.
- Local REST API plugin must be enabled separately from the API key — default port 27124, but the plugin must be running for any HTTP request to land.
- Markdown footnotes (`[^1]`) require a blank line before the definition — without the blank line, Obsidian renders them as plain text, no error.
- Vault path with spaces/special chars: most plugins handle it; some shell-integration plugins don't. Test scripted invocations explicitly.
Obsidian TypeScript API Reference
Complete reference for the Obsidian plugin API classes and interfaces.
Core Classes
App
Central application instance providing access to all subsystems.
interface App {
// Core subsystems
keymap: Keymap;
scope: Scope;
workspace: Workspace;
vault: Vault;
metadataCache: MetadataCache;
fileManager: FileManager;
// UI managers
commands: Commands;
// Methods
loadLocalStorage(key: string): string | null;
saveLocalStorage(key: string, value: string | undefined): void;
}
// Access in plugin
const app = this.app;
const vault = this.app.vault;
const workspace = this.app.workspace;Vault
File system operations and event handling.
interface Vault extends Events {
// Properties
configDir: string; // Usually ".obsidian"
adapter: DataAdapter; // Low-level file access
// File retrieval
getAbstractFileByPath(path: string): TAbstractFile | null;
getMarkdownFiles(): TFile[];
getAllLoadedFiles(): TAbstractFile[];
getFiles(): TFile[];
// File operations
create(path: string, data: string): Promise<TFile>;
createBinary(path: string, data: ArrayBuffer): Promise<TFile>;
createFolder(path: string): Promise<void>;
read(file: TFile): Promise<string>;
readBinary(file: TFile): Promise<ArrayBuffer>;
cachedRead(file: TFile): Promise<string>;
modify(file: TFile, data: string): Promise<void>;
modifyBinary(file: TFile, data: ArrayBuffer): Promise<void>;
delete(file: TAbstractFile, force?: boolean): Promise<void>;
trash(file: TAbstractFile, system: boolean): Promise<void>;
rename(file: TAbstractFile, newPath: string): Promise<void>;
copy(file: TFile, newPath: string): Promise<TFile>;
// Events
on(name: 'create', callback: (file: TAbstractFile) => any): EventRef;
on(name: 'modify', callback: (file: TAbstractFile) => any): EventRef;
on(name: 'delete', callback: (file: TAbstractFile) => any): EventRef;
on(name: 'rename', callback: (file: TAbstractFile, oldPath: string) => any): EventRef;
}Workspace
Layout and pane management.
interface Workspace extends Events {
// Properties
leftSplit: WorkspaceSidedock;
rightSplit: WorkspaceSidedock;
leftRibbon: WorkspaceRibbon;
rightRibbon: WorkspaceRibbon;
activeLeaf: WorkspaceLeaf | null;
// Leaf operations
getLeaf(newLeaf?: boolean | PaneType): WorkspaceLeaf;
getActiveViewOfType<T extends View>(type: Constructor<T>): T | null;
getLeavesOfType(viewType: string): WorkspaceLeaf[];
getLeftLeaf(split: boolean): WorkspaceLeaf;
getRightLeaf(split: boolean): WorkspaceLeaf;
splitActiveLeaf(direction?: SplitDirection): WorkspaceLeaf;
createLeafInParent(parent: WorkspaceSplit, index: number): WorkspaceLeaf;
// File operations
openLinkText(linktext: string, sourcePath: string, newLeaf?: boolean): Promise<void>;
getActiveFile(): TFile | null;
activeEditor: MarkdownEditView | null;
// Layout
changeLayout(workspace: any): Promise<void>;
getLayout(): any;
requestSaveLayout(): void;
// Events
on(name: 'file-open',
callback: (file: TFile | null) => any): EventRef;
on(name: 'active-leaf-change',
callback: (leaf: WorkspaceLeaf | null) => any): EventRef;
on(name: 'layout-change', callback: () => any): EventRef;
on(name: 'editor-change',
callback: (editor: Editor, info: MarkdownView) => any): EventRef;
on(name: 'resize', callback: () => any): EventRef;
on(name: 'quit', callback: (tasks: Tasks) => any): EventRef;
}MetadataCache
Parsed file metadata and link resolution.
interface MetadataCache extends Events {
// Get metadata
getFileCache(file: TFile): CachedMetadata | null;
getCache(path: string): CachedMetadata | null;
getFirstLinkpathDest(linkpath: string, sourcePath: string): TFile | null;
// Link resolution
resolvedLinks: Record<string, Record<string, number>>;
unresolvedLinks: Record<string, Record<string, number>>;
// Events
on(name: 'changed',
callback: (file: TFile, data: string, cache: CachedMetadata) => any
): EventRef;
on(name: 'deleted',
callback: (file: TFile, prevCache: CachedMetadata | null) => any
): EventRef;
on(name: 'resolved', callback: () => any): EventRef;
}
interface CachedMetadata {
links?: LinkCache[];
embeds?: EmbedCache[];
tags?: TagCache[];
headings?: HeadingCache[];
sections?: SectionCache[];
listItems?: ListItemCache[];
frontmatter?: FrontMatterCache;
frontmatterPosition?: Pos;
frontmatterLinks?: FrontmatterLinkCache[];
blocks?: Record<string, BlockCache>;
}
interface LinkCache extends ReferenceCache {
link: string;
original: string;
}
interface HeadingCache extends CacheItem {
heading: string;
level: number;
}
interface TagCache extends CacheItem {
tag: string;
}FileManager
User-safe file operations with prompts.
interface FileManager {
// File operations
createNewMarkdownFile(
folder: TFolder,
name: string,
content?: string
): Promise<TFile>;
renameFile(file: TAbstractFile, newPath: string): Promise<void>;
// Frontmatter
processFrontMatter(
file: TFile,
fn: (frontmatter: any) => void
): Promise<void>;
// Links
generateMarkdownLink(
file: TFile,
sourcePath: string,
subpath?: string,
alias?: string
): string;
}File Abstractions
TAbstractFile
Base class for files and folders.
abstract class TAbstractFile {
vault: Vault;
path: string;
name: string;
parent: TFolder | null;
}TFile
Represents a file.
class TFile extends TAbstractFile {
stat: FileStats;
basename: string; // Name without extension
extension: string; // File extension
}
interface FileStats {
ctime: number; // Created time
mtime: number; // Modified time
size: number; // Size in bytes
}TFolder
Represents a folder.
class TFolder extends TAbstractFile {
children: TAbstractFile[];
isRoot(): boolean;
}Editor
Editor Interface
Abstract editor operations (CodeMirror-agnostic).
interface Editor {
// Content
getValue(): string;
setValue(content: string): void;
getLine(line: number): string;
setLine(line: number, text: string): void;
lineCount(): number;
// Selection
getSelection(): string;
setSelection(anchor: EditorPosition, head?: EditorPosition): void;
somethingSelected(): boolean;
replaceSelection(replacement: string, origin?: string): void;
replaceRange(
replacement: string,
from: EditorPosition,
to?: EditorPosition,
origin?: string
): void;
// Cursor
getCursor(string?: 'from' | 'to' | 'head' | 'anchor'): EditorPosition;
setCursor(pos: EditorPosition | number, ch?: number): void;
offsetToPos(offset: number): EditorPosition;
posToOffset(pos: EditorPosition): number;
// Scroll
scrollTo(x?: number | null, y?: number | null): void;
scrollIntoView(range: EditorRange, center?: boolean): void;
// Transactions
transaction(tx: EditorTransaction): void;
// Focus
focus(): void;
blur(): void;
hasFocus(): boolean;
// Undo
undo(): void;
redo(): void;
// Word at position
wordAt(pos: EditorPosition): EditorRange | null;
// Exec command
exec(command: EditorCommandName): void;
}
interface EditorPosition {
line: number;
ch: number;
}
interface EditorRange {
from: EditorPosition;
to: EditorPosition;
}Views
View
Base class for all views.
abstract class View extends Component {
app: App;
icon: IconName;
navigation: boolean;
leaf: WorkspaceLeaf;
containerEl: HTMLElement;
abstract getViewType(): string;
abstract getDisplayText(): string;
abstract getIcon(): IconName;
onOpen(): Promise<void>;
onClose(): Promise<void>;
getState(): any;
setState(state: any, result: ViewStateResult): Promise<void>;
}ItemView
Base for custom views.
abstract class ItemView extends View {
contentEl: HTMLElement;
addAction(
icon: IconName,
title: string,
callback: (evt: MouseEvent) => any
): HTMLElement;
onHeaderMenu(menu: Menu): void;
}MarkdownView
Markdown file view.
class MarkdownView extends TextFileView {
editor: Editor;
previewMode: MarkdownPreviewView;
getMode(): 'source' | 'preview' | 'live';
getViewType(): 'markdown';
showSearch(replace?: boolean): void;
}WorkspaceLeaf
Container for views.
class WorkspaceLeaf extends Component {
view: View;
parent: WorkspaceSplit;
// Navigation
openFile(file: TFile, openState?: OpenViewState): Promise<void>;
open(view: View): Promise<View>;
// State
getViewState(): ViewState;
setViewState(viewState: ViewState, eState?: any): Promise<void>;
// Layout
detach(): void;
setGroupMember(other: WorkspaceLeaf): void;
setGroup(group: string): void;
// Tab
getDisplayText(): string;
getIcon(): IconName;
// History
getHistoryState(): any;
setHistoryState(state: any): void;
// Pin
setPinned(pinned: boolean): void;
togglePinned(): void;
}UI Components
Modal
Dialog base class.
abstract class Modal {
app: App;
scope: Scope;
containerEl: HTMLElement;
modalEl: HTMLElement;
titleEl: HTMLElement;
contentEl: HTMLElement;
open(): void;
close(): void;
onOpen(): void;
onClose(): void;
}SuggestModal
Modal with search suggestions.
abstract class SuggestModal<T> extends Modal {
inputEl: HTMLInputElement;
resultContainerEl: HTMLElement;
emptyStateText: string;
limit: number;
abstract getSuggestions(query: string): T[] | Promise<T[]>;
abstract renderSuggestion(item: T, el: HTMLElement): void;
abstract onChooseSuggestion(item: T, evt: MouseEvent | KeyboardEvent): void;
selectSuggestion(value: T, evt: MouseEvent | KeyboardEvent): void;
}FuzzySuggestModal
Modal with fuzzy search.
abstract class FuzzySuggestModal<T> extends SuggestModal<FuzzyMatch<T>> {
abstract getItems(): T[];
abstract getItemText(item: T): string;
abstract onChooseItem(item: T, evt: MouseEvent | KeyboardEvent): void;
}Setting
Settings UI builder.
class Setting {
settingEl: HTMLElement;
infoEl: HTMLElement;
nameEl: HTMLElement;
descEl: HTMLElement;
controlEl: HTMLElement;
constructor(containerEl: HTMLElement);
setName(name: string | DocumentFragment): this;
setDesc(desc: string | DocumentFragment): this;
setClass(cls: string): this;
setTooltip(tooltip: string, options?: TooltipOptions): this;
setHeading(): this;
setDisabled(disabled: boolean): this;
// Controls
addButton(cb: (component: ButtonComponent) => any): this;
addExtraButton(cb: (component: ExtraButtonComponent) => any): this;
addToggle(cb: (component: ToggleComponent) => any): this;
addText(cb: (component: TextComponent) => any): this;
addTextArea(cb: (component: TextAreaComponent) => any): this;
addMomentFormat(cb: (component: MomentFormatComponent) => any): this;
addDropdown(cb: (component: DropdownComponent) => any): this;
addSlider(cb: (component: SliderComponent) => any): this;
addSearch(cb: (component: SearchComponent) => any): this;
addColorPicker(cb: (component: ColorComponent) => any): this;
}Menu
Context menu builder.
class Menu extends Component {
addItem(cb: (item: MenuItem) => any): this;
addSeparator(): this;
showAtMouseEvent(event: MouseEvent): this;
showAtPosition(position: MenuPositionDef, doc?: Document): this;
hide(): this;
onHide(callback: () => any): void;
}
class MenuItem {
setTitle(title: string | DocumentFragment): this;
setIcon(icon: IconName | null): this;
setChecked(checked: boolean | null): this;
setDisabled(disabled: boolean): this;
setSection(section: string): this;
setIsLabel(isLabel: boolean): this;
onClick(callback: (evt: MouseEvent | KeyboardEvent) => any): this;
setSubmenu(): Menu;
}Component Lifecycle
Component
Base class with lifecycle management.
class Component {
load(): void;
onload(): void;
unload(): void;
onunload(): void;
// Child components
addChild<T extends Component>(component: T): T;
removeChild<T extends Component>(component: T): T;
// Event registration
registerEvent(eventRef: EventRef): void;
// Interval/timeout
registerInterval(id: number): number;
// DOM events
registerDomEvent<K extends keyof WindowEventMap>(
el: Window,
type: K,
callback: (this: HTMLElement, ev: WindowEventMap[K]) => any,
options?: boolean | AddEventListenerOptions
): void;
registerDomEvent<K extends keyof DocumentEventMap>(
el: Document,
type: K,
callback: (this: HTMLElement, ev: DocumentEventMap[K]) => any,
options?: boolean | AddEventListenerOptions
): void;
registerDomEvent<K extends keyof HTMLElementEventMap>(
el: HTMLElement,
type: K,
callback: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any,
options?: boolean | AddEventListenerOptions
): void;
// Cleanup callback
register(cb: () => any): void;
}Plugin
Plugin base class.
abstract class Plugin extends Component {
app: App;
manifest: PluginManifest;
// Data persistence
loadData(): Promise<any>;
saveData(data: any): Promise<void>;
// Commands
addCommand(command: Command): Command;
// Ribbon
addRibbonIcon(
icon: IconName, title: string,
callback: (evt: MouseEvent) => any
): HTMLElement;
// Status bar
addStatusBarItem(): HTMLElement;
// Settings tab
addSettingTab(settingTab: PluginSettingTab): void;
// Views
registerView(type: string, viewCreator: ViewCreator): void;
// Extensions
registerExtensions(extensions: string[], viewType: string): void;
// Markdown processors
registerMarkdownPostProcessor(
postProcessor: MarkdownPostProcessor,
sortOrder?: number
): MarkdownPostProcessor;
registerMarkdownCodeBlockProcessor(
language: string,
handler: (
source: string, el: HTMLElement,
ctx: MarkdownPostProcessorContext
) => Promise<any> | void,
sortOrder?: number
): MarkdownPostProcessor;
// Events
registerObsidianProtocolHandler(
action: string,
handler: ObsidianProtocolHandler
): void;
registerEditorExtension(extension: Extension): void;
registerEditorSuggest(editorSuggest: EditorSuggest<any>): void;
registerHoverLinkSource(id: string, info: HoverLinkSource): void;
}Command Interface
interface Command {
id: string;
name: string;
icon?: IconName;
// Callbacks (use one)
callback?: () => any;
checkCallback?: (checking: boolean) => boolean | void;
editorCallback?: (
editor: Editor, ctx: MarkdownView | MarkdownFileInfo
) => any;
editorCheckCallback?: (
checking: boolean, editor: Editor,
ctx: MarkdownView | MarkdownFileInfo
) => boolean | void;
// Hotkeys
hotkeys?: Hotkey[];
// Flags
mobileOnly?: boolean;
repeatable?: boolean;
}
interface Hotkey {
modifiers: Modifier[]; // 'Mod' | 'Ctrl' | 'Meta' | 'Shift' | 'Alt'
key: string;
}Utility Functions
Notice
Display notifications.
class Notice {
constructor(message: string | DocumentFragment, timeout?: number);
setMessage(message: string | DocumentFragment): this;
hide(): void;
}
// Usage
new Notice('Operation complete!');
new Notice('Error occurred', 5000); // 5 second timeoutrequestUrl
HTTP requests.
function requestUrl(request: RequestUrlParam | string): Promise<RequestUrlResponse>;
interface RequestUrlParam {
url: string;
method?: string;
contentType?: string;
body?: string | ArrayBuffer;
headers?: Record<string, string>;
}
interface RequestUrlResponse {
status: number;
headers: Record<string, string>;
arrayBuffer: ArrayBuffer;
json: any;
text: string;
}debounce
Debounce function calls.
function debounce<T extends unknown[]>(
cb: (...args: T) => any,
timeout?: number,
resetTimer?: boolean
): (...args: T) => void;
// Usage
const debouncedSave = debounce(saveData, 300);normalizePath
Normalize file paths.
function normalizePath(path: string): string;
// Usage
const path = normalizePath('folder//file.md');
// Returns: 'folder/file.md'moment
Date/time handling (bundled with Obsidian).
// Access global moment
declare const moment: typeof import('moment');
// Usage
const now = moment();
const formatted = moment().format('YYYY-MM-DD');Type Definitions
Icons
type IconName =
| 'lucide-file'
| 'lucide-folder'
| 'lucide-search'
| 'lucide-settings'
| 'lucide-star'
// ... many more Lucide icons
;Events
interface Events {
on(name: string, callback: (...data: any) => any, ctx?: any): EventRef;
off(name: string, callback: (...data: any) => any): void;
offref(ref: EventRef): void;
trigger(name: string, ...data: any[]): void;
tryTrigger(evt: EventRef, args: any[]): void;
}Common Patterns
Getting Active File
const activeFile = this.app.workspace.getActiveFile();
if (activeFile) {
const content = await this.app.vault.read(activeFile);
}Getting Active Editor
const view = this.app.workspace.getActiveViewOfType(MarkdownView);
if (view) {
const editor = view.editor;
const selection = editor.getSelection();
}Processing All Files
for (const file of this.app.vault.getMarkdownFiles()) {
const cache = this.app.metadataCache.getFileCache(file);
if (cache?.frontmatter?.status === 'active') {
// Process file
}
}Registering Events Safely
// Always use registerEvent for automatic cleanup
this.registerEvent(
this.app.workspace.on('file-open', (file) => {
// Handler
})
);Creating Modals
const modal = new MyModal(this.app, (result) => {
console.log('User submitted:', result);
});
modal.open();Obsidian Markdown Extensions Reference
Complete reference for Obsidian's markdown syntax beyond standard CommonMark.
Internal Links (Wikilinks)
Basic Links
[[Note Name]] # Link to note
[[Note Name|Display Text]] # Link with alias
[[Folder/Note Name]] # Link to note in folder
[[Note Name.md]] # Explicit extensionHeading Links
[[Note Name#Heading]] # Link to heading
[[Note Name#Heading|text]] # Heading link with alias
[[#Heading]] # Link to heading in current noteBlock Links
[[Note Name#^block-id]] # Link to specific block
[[Note Name#^block-id|text]] # Block link with alias
[[#^block-id]] # Block in current noteCreating Block IDs
This is a paragraph that I want to reference. ^my-block-id
- List item ^list-block
> Quote block ^quote-blockEmbeds (Transclusion)
Note Embeds
![[Note Name]] # Embed entire note
![[Note Name#Heading]] # Embed section under heading
![[Note Name#^block-id]] # Embed specific blockImage Embeds
![[image.png]] # Basic embed
![[image.png|100]] # Width in pixels
![[image.png|100x200]] # Width x Height
![[image.png|alt text]] # Alt text (with pipe)
![[Attachments/photo.jpg]] # From subfolderOther File Embeds
![[document.pdf]] # Embed PDF
![[document.pdf#page=5]] # Embed PDF at page
![[audio.mp3]] # Embed audio player
![[video.mp4]] # Embed video player
![[file.csv]] # Embed CSV as tableExternal Embeds
 # External image
 # External with width
# YouTube (auto-detected)


# Twitter/X
Callouts
Basic Syntax
> [!note]
> This is a note callout.
> [!info] Title Here
> Content with **formatting**.
> [!warning]
> Warning message.Foldable Callouts
> [!tip]+ Expanded by Default
> This content is visible initially.
> [!example]- Collapsed by Default
> Click to expand this content.Callout Types
| Type | Aliases | Description |
|---|---|---|
note | Neutral information | |
abstract | summary, tldr | Overview or summary |
info | General information | |
todo | Action items | |
tip | hint, important | Helpful suggestions |
success | check, done | Positive outcome |
question | help, faq | Questions or inquiries |
warning | caution, attention | Potential issues |
failure | fail, missing | Errors or missing items |
danger | error | Critical warnings |
bug | Bug reports | |
example | Examples or samples | |
quote | cite | Quotations |
Custom Callout Icons
Via CSS snippet:
.callout[data-callout="custom-type"] {
--callout-color: 100, 150, 200;
--callout-icon: lucide-sparkles;
}Nested Callouts
> [!note] Outer
> Outer content
> > [!warning] Inner
> > Nested calloutTags
Basic Tags
#tag # Simple tag
#nested/tag # Nested/hierarchical tag
#tag-with-dashes # Dashes allowed
#tag_with_underscores # Underscores allowed
#123 # Numeric tags allowedTag Restrictions
- Cannot contain spaces
- Cannot start with a number
- Case-insensitive matching
- Must follow word boundary
Inline vs Frontmatter Tags
---
tags:
- tag1
- tag2
- nested/tag
---
This note has #inline-tag too.Frontmatter (YAML)
Basic Frontmatter
---
title: My Note Title
date: 2024-01-15
author: Your Name
---
# Content starts hereCommon Properties
---
# Standard properties
title: Note Title
date: 2024-01-15
created: 2024-01-01
modified: 2024-01-15
# Tags and categories
tags:
- tag1
- tag2
category: Category Name
# Aliases for linking
aliases:
- Alternate Name
- Another Alias
# Custom CSS class
cssclass: my-custom-class
cssclasses:
- class1
- class2
# Publishing
publish: true
permalink: custom-url
# Custom properties
status: draft
priority: high
rating: 5
---Property Types
---
# String
title: "String Value"
# Number
count: 42
rating: 4.5
# Boolean
published: true
draft: false
# Date
date: 2024-01-15
datetime: 2024-01-15T10:30:00
# List
tags:
- item1
- item2
# Inline list
tags: [item1, item2, item3]
# Object/Map
metadata:
key1: value1
key2: value2
# Multi-line string
description: |
This is a multi-line
string value.
---Comments
Inline Comments
This is visible. %%This is hidden.%%
More visible text.Block Comments
%%
This entire block
is hidden from
the rendered view.
%%Math (LaTeX)
Inline Math
The equation $E = mc^2$ is famous.
Inline: $\sum_{i=1}^{n} x_i$Block Math
$$
\frac{-b \pm \sqrt{b^2 - 4ac}}{2a}
$$
$$
\begin{aligned}
a &= b + c \\
d &= e + f
\end{aligned}
$$Common LaTeX Symbols
Greek: $\alpha, \beta, \gamma, \delta$
Operators: $\sum, \prod, \int$
Relations: $\leq, \geq, \neq, \approx$
Arrows: $\to, \leftarrow, \Rightarrow$
Sets: $\in, \notin, \subset, \cup, \cap$Code Blocks
Fenced Code
````markdown
const hello = "world";
console.log(hello);````
Syntax Highlighting
Supported languages include:
javascript,jstypescript,tspython,pybash,shell,shjson,yamlcss,htmlsqlmarkdown,md- And many more...
Line Numbers (Plugin)
Some themes/plugins support:
```markdown ``javascript {1,3-5} // Highlighted lines const a = 1; const b = 2; const c = 3;
Diagrams (Mermaid)
Flowchart
````markdown
graph TD
A[Start] --> B{Decision}
B -->|Yes| C[Action 1]
B -->|No| D[Action 2]
C --> E[End]
D --> E````
Sequence Diagram
````markdown
sequenceDiagram
participant A as Alice
participant B as Bob
A->>B: Hello Bob!
B-->>A: Hi Alice!````
Class Diagram
````markdown
classDiagram
Animal <|-- Duck
Animal <|-- Fish
Animal : +int age
Animal : +String gender
Animal: +isMammal()````
Gantt Chart
````markdown
gantt
title Project Schedule
dateFormat YYYY-MM-DD
section Phase 1
Task 1: 2024-01-01, 7d
Task 2: 2024-01-08, 5d````
State Diagram
````markdown
stateDiagram-v2
[*] --> Draft
Draft --> Review
Review --> Published
Review --> Draft
Published --> [*]````
Tables
Basic Tables
| Header 1 | Header 2 | Header 3 |
|----------|----------|----------|
| Cell 1 | Cell 2 | Cell 3 |
| Cell 4 | Cell 5 | Cell 6 |Alignment
| Left | Center | Right |
|:---------|:--------:|---------:|
| Left | Center | Right |Extended Features
| With Links | With Formatting |
|------------|-----------------|
| [[Link]] | **bold** text |
| `code` | *italic* text |Task Lists
Basic Tasks
- [ ] Uncompleted task
- [x] Completed task
- [ ] Another taskNested Tasks
- [ ] Parent task
- [ ] Subtask 1
- [x] Subtask 2 (done)
- [ ] Subtask 3Custom Status (Plugins)
Some plugins support extended status:
- [ ] Todo
- [x] Done
- [/] In Progress
- [-] Cancelled
- [>] Forwarded
- [<] Scheduled
- [?] Question
- [!] ImportantFootnotes
Inline Footnotes
Here is a statement[^1] with a footnote.
[^1]: This is the footnote content.Multi-line Footnotes
Complex footnote[^note].
[^note]: This footnote has multiple paragraphs.
Second paragraph of the footnote.
Even code blocks work:code here
Highlights
==Highlighted text== using double equals.Strikethrough
~~Strikethrough text~~Horizontal Rules
---
***
___External Links
Basic
[Link Text](https://example.com)
[Link with Title](https://example.com "Title")Auto-linking
https://example.com # Auto-linked
<https://example.com> # Explicit auto-link
<email@example.com> # Email linkDataview (Plugin)
Inline Queries
`= this.file.name`
`= date(today)`
`= this.tags`List Query
````markdown
LIST
FROM #tag
WHERE file.mtime > date(today) - dur(7 days)
SORT file.mtime DESC````
Table Query
````markdown
TABLE file.ctime AS "Created", status
FROM "Projects"
WHERE status != "archived"
SORT file.ctime DESC````
Task Query
````markdown
TASK
FROM "Daily Notes"
WHERE !completed
GROUP BY file.link````
Templater (Plugin)
Basic Template
<%*
const title = await tp.system.prompt("Title?");
-%>
# <% title %>
Created: <% tp.date.now("YYYY-MM-DD") %>Common Functions
<% tp.date.now("YYYY-MM-DD") %>
<% tp.file.title %>
<% tp.file.path() %>
<% tp.file.cursor() %>
<% tp.system.clipboard() %>Query Blocks (Core)
Embed Search Results
````markdown
tag:#importantpath:Projects````
Best Practices
Link Formatting
1. Use relative paths - Works across devices 2. Use aliases - Cleaner in preview 3. Avoid deep nesting - Hard to maintain
Frontmatter
1. Consistent property names - Case-sensitive 2. Quote strings with colons - YAML parsing 3. Use lists for multiple values - Better for queries
Organization
1. One topic per note - Atomic notes 2. Link liberally - Build knowledge graph 3. Use tags sparingly - Avoid over-categorization
Obsidian Bases Reference
Bases are YAML-based .base files that define dynamic views of vault notes.
Complete Schema
filters: # Global filters (all views)
and: []
or: []
not: []
formulas: # Computed properties
name: 'expression'
properties: # Display config
property_name:
displayName: "Display Name"
formula.name:
displayName: "Formula Name"
summaries: # Custom summary formulas
name: 'values.mean().round(3)'
views: # One or more views
- type: table | cards | list | map
name: "View Name"
limit: 10
groupBy:
property: property_name
direction: ASC | DESC
filters: # View-specific filters
and: []
order: # Properties to display
- file.name
- property_name
- formula.name
summaries: # Property -> summary mapping
property_name: AverageFilter Syntax
Operators
| Operator | Description |
|---|---|
==, != | Equality |
>, <, >=, <= | Comparison |
&&, `\ | \ |
Examples
# Single
filters: 'status == "done"'
# AND
filters:
and:
- 'status == "done"'
- 'priority > 3'
# OR
filters:
or:
- file.hasTag("book")
- file.hasTag("article")
# NOT
filters:
not:
- file.hasTag("archived")
# Nested
filters:
or:
- file.hasTag("tag")
- and:
- file.hasTag("book")
- file.hasLink("Textbook")
- not:
- file.hasTag("book")
- file.inFolder("Required Reading")Properties
Three Types
1. Note properties - From frontmatter: author or note.author 2. File properties - File metadata: file.name, file.mtime, etc. 3. Formula properties - Computed: formula.my_formula
File Properties
| Property | Type | Description |
|---|---|---|
file.name | String | File name |
file.basename | String | Name without extension |
file.path | String | Full path |
file.folder | String | Parent folder |
file.ext | String | Extension |
file.size | Number | Size in bytes |
file.ctime | Date | Created time |
file.mtime | Date | Modified time |
file.tags | List | All tags |
file.links | List | Internal links |
file.backlinks | List | Files linking to this |
file.embeds | List | Embeds in note |
file.properties | Object | All frontmatter |
The this Keyword
- In main content: refers to base file itself
- When embedded: refers to embedding file
- In sidebar: refers to active file
Formula Syntax
formulas:
total: "price * quantity"
status_icon: 'if(done, "done", "pending")'
formatted: 'if(price, price.toFixed(2) + " dollars")'
created: 'file.ctime.format("YYYY-MM-DD")'
days_old: '((now() - file.ctime) / 86400000).round(0)'Functions Reference
Global Functions
| Function | Description |
|---|---|
date(string) | Parse string to date |
duration(string) | Parse duration |
now() | Current date+time |
today() | Current date (00:00) |
if(cond, true, false?) | Conditional |
min(n1, n2, ...) | Minimum |
max(n1, n2, ...) | Maximum |
number(any) | Convert to number |
link(path, display?) | Create link |
list(element) | Wrap in list |
file(path) | Get file object |
image(path) | Create image |
icon(name) | Lucide icon |
html(string) | Render HTML |
escapeHTML(string) | Escape HTML |
Date Functions
Fields: .year, .month, .day, .hour, .minute, .second, .millisecond
| Function | Description |
|---|---|
date.format(pattern) | Format (Moment.js) |
date.relative() | Human-readable relative |
date.time() | Time as string |
date.date() | Remove time portion |
Date Arithmetic
"date + \"1M\"" # Add 1 month
"date - \"2h\"" # Subtract 2 hours
"now() + \"1 day\"" # Tomorrow
"today() + \"7d\"" # Week from today
"now() - file.ctime" # Milliseconds since creationDuration units: y/year/years, M/month/months, d/day/days, w/week/weeks, h/hour/hours, m/minute/minutes, s/second/seconds
String Functions
Field: .length
contains(), containsAll(), containsAny(), startsWith(), endsWith(), isEmpty(), lower(), title(), trim(), replace(), repeat(), reverse(), slice(), split()
Number Functions
abs(), ceil(), floor(), round(digits?), toFixed(precision), isEmpty()
List Functions
Field: .length
contains(), containsAll(), containsAny(), filter(expr), map(expr), reduce(expr, init), flat(), join(sep), reverse(), slice(), sort(), unique(), isEmpty()
Filter/map/reduce use: value, index, acc (reduce only)
File Functions
asLink(display?), hasLink(file), hasTag(...tags), hasProperty(name), inFolder(folder)
Link Functions
asFile(), linksTo(file)
View Types
Table
views:
- type: table
name: "Tasks"
order: [file.name, status, due_date]
summaries:
price: SumCards
views:
- type: cards
name: "Gallery"
order: [cover, file.name, description]List
views:
- type: list
name: "Simple"
order: [file.name, status]Map
Requires lat/lng properties and Maps plugin.
Default Summary Formulas
Number: Average, Min, Max, Sum, Range, Median, Stddev Date: Earliest, Latest, Range Boolean: Checked, Unchecked Any: Empty, Filled, Unique
Common Patterns
# By tag
filters:
and: [file.hasTag("project")]
# By folder
filters:
and: [file.inFolder("Notes")]
# By date range
filters:
and: ['file.mtime > now() - "7d"']
# By property
filters:
and: ['status == "active"', 'priority >= 3']Embedding
![[MyBase.base]]
![[MyBase.base#View Name]]YAML Quoting
- Single quotes for formulas with double quotes:
'if(done, "Yes", "No")' - Double quotes for simple strings:
"My View"
References
Integration Patterns Reference
Overview
An Obsidian vault is a codebase of markdown files. Claude Code navigates file structures and makes surgical edits natively.
Pattern Decision Tree
What integration level?
├── Minimal (just works)? -> Pattern A: Direct Access
├── Enhanced discovery? -> Pattern B: Manifest-Based (CLAUDE.md)
├── Real-time bidirectional? -> Pattern C: MCP Plugin
├── Self-evolving PKM? -> Pattern D: COG Pattern
└── Pre-configured structure? -> Pattern E: ClaudesidianPattern A: Direct Access (Zero Setup)
Claude Code reads/edits vault files directly. No configuration needed.
cd /path/to/vault && claude
# "Read my daily note from today"
# "Find all notes mentioning project X"
# "Add backlinks to people mentioned in this note"| Task | Capability |
|---|---|
| Read/edit notes | Direct file access |
| Add backlinks | Find references, insert wikilinks |
| Create notes | Write files with frontmatter |
| Search content | Grep across markdown |
| Refactor structure | Move files, update references |
Pattern B: Manifest-Based (CLAUDE.md)
Add CLAUDE.md at vault root to describe structure, conventions, and rules.
Key sections: Vault Overview, Folder Structure, Conventions (frontmatter, links, tags, dates), Important Files, When Creating/Editing Notes.
Pattern C: MCP Plugin
Real-time bidirectional via Model Context Protocol.
{
"mcpServers": {
"obsidian": {
"transport": "websocket",
"url": "ws://localhost:22360"
}
}
}Capabilities: read_note, write_note, search, list_notes, get_backlinks, get_outlinks, get_tags
Plugin: obsidian-claude-code-mcp (Community Plugins)
Pattern D: COG Self-Evolving
Git-based with auto-organization and self-healing.
vault/
├── .git/
├── CLAUDE.md
├── _meta/
│ ├── patterns.md # Learned patterns
│ ├── conventions.md # Auto-discovered rules
│ └── maintenance-log.md # Self-healing log
├── notes/
└── daily/Self-Healing Features
1. Auto cross-references: Updates links on note moves 2. Pattern learning: Discovers and applies conventions 3. Orphan detection: Identifies unlinked notes 4. Consistency checks: Validates frontmatter, tags
Git Hooks
# .git/hooks/post-commit
#!/bin/bash
claude --print "Check for broken links and orphan notes.
Fix issues and update _meta/maintenance-log.md."Pattern E: Claudesidian
Pre-configured vault structure optimized for AI interaction.
vault/
├── CLAUDE.md
├── Inbox/
├── Projects/
├── Knowledge/
├── Journal/
├── Templates/
└── _meta/
├── prompts/
├── contexts/
└── exports/Comparison
| Feature | Direct | Manifest | MCP | COG | Claudesidian |
|---|---|---|---|---|---|
| Setup | None | Minimal | Plugin | Git+hooks | Structure |
| Real-time | No | No | Yes | No | No |
| Semantic Search | Basic | Basic | Yes | Basic | Basic |
| Self-Healing | No | No | No | Yes | Partial |
| Vendor Lock-in | None | None | Low | None | Structure |
Common Workflows
Auto-Linking Entities
1. Read note content 2. Extract entity mentions (people, places, books) 3. Search vault for existing notes 4. Create new notes if needed 5. Insert [[wikilinks]] throughout
Knowledge Graph Maintenance
1. Identify orphan notes (no in/out links) 2. Analyze content for potential connections 3. Suggest or create links 4. Update MOCs
Research Synthesis
1. Search for relevant notes by topic 2. Extract key insights 3. Create structured summary note 4. Link back to source notes
Daily Note Enhancement
1. Read raw daily capture 2. Add proper frontmatter 3. Identify tasks -> add checkboxes 4. Identify mentions -> add wikilinks 5. Suggest tags based on content
Neovim Integration (obsidian.nvim)
For Neovim users, obsidian.nvim provides vault management inside the editor.
Minimal Setup (lazy.nvim)
return {
"obsidian-nvim/obsidian.nvim",
version = "*",
ft = "markdown",
opts = {
workspaces = {
{ name = "personal", path = "~/vaults/personal" },
},
},
}Key Commands
| Command | Description |
|---|---|
:Obsidian today | Open/create daily note |
:Obsidian new [TITLE] | Create new note |
:Obsidian search | Search vault |
:Obsidian quick_switch | Fuzzy find notes |
:Obsidian backlinks | Show backlinks |
:Obsidian template | Insert template |
Completion Triggers
[[- Wiki link completion[- Markdown link completion#- Tag completion
Template Variables
| Variable | Description |
|---|---|
{{title}} | Note title |
{{date}} | Current date |
{{time}} | Current time |
{{id}} | Note ID |
Requires: Neovim >= 0.10.0, ripgrep
Best Practices
1. Keep vault in version control (git rollback + change tracking) 2. Use consistent frontmatter across note types 3. Maintain CLAUDE.md manifest with conventions 4. Regular maintenance (orphan detection, broken link scan) 5. Review AI changes via git diff before committing 6. Document exceptions in manifest
References
Knowledge Capture Patterns
Templates and workflows for extracting knowledge from conversations into vault notes.
Capture Types
| Type | When to Use | Destination |
|---|---|---|
| ADR | Architecture/design decisions | 03 - Resources/decisions/ |
| Concept | New concept or mental model | 04 - Permanent/ |
| How-To | Step-by-step procedure | 03 - Resources/howtos/ |
| Meeting Note | Meeting outcomes and actions | 10 - 1-1/ or 01 - Projects/ |
| MOC Update | New topic index or update | 00 - Maps of Content/ |
| Daily Append | Quick insight to today's note | 06 - Daily/YYYY/MM/YYYYMMDD.md |
ADR Template (Architecture Decision Record)
---
created: YYYY-MM-DDTHH:mm
updated: YYYY-MM-DDTHH:mm
type: adr
status: accepted | proposed | deprecated | superseded
tags:
- decision
- topic/subtopic
---# ADR: [Decision Title]
## Status
Accepted
## Context
What is the issue we're deciding on? What forces are at play?
## Decision
What is the change that we're proposing and/or doing?
## Consequences
What becomes easier or harder as a result of this decision?
### Positive
- Benefit 1
- Benefit 2
### Negative
- Tradeoff 1
- Tradeoff 2
## Related
- [[Previous ADR]]
- [[Related Project]]Concept Template
---
created: YYYY-MM-DDTHH:mm
updated: YYYY-MM-DDTHH:mm
type: zettelkasten
tags:
- permanent
- concept
- topic
---# [Concept Name]
## Definition
One-paragraph explanation in your own words.
## Key Insight
**Core idea**: [Single sentence capturing the essence]
## How It Works
Explain the mechanism or model.
## Examples
- Example 1
- Example 2
## Connections
- [[Related Concept 1]] - how it relates
- [[Related Concept 2]] - how it relates
## Sources
- Where you learned thisHow-To Template
---
created: YYYY-MM-DDTHH:mm
updated: YYYY-MM-DDTHH:mm
type: howto
tags:
- howto
- tool/technology
---# How To: [Task Description]
## Prerequisites
- Requirement 1
- Requirement 2
## Steps
### 1. [First Step]
Details and commands.
### 2. [Second Step]
Details and commands.
### 3. [Third Step]
Details and commands.
## Verification
How to confirm it worked.
## Troubleshooting
Common issues and fixes.
## Related
- [[Related How-To]]
- [[Tool Documentation]]Meeting Note Template
---
created: YYYY-MM-DDTHH:mm
updated: YYYY-MM-DDTHH:mm
type: meeting
attendees:
- "[[Person 1]]"
- "[[Person 2]]"
tags:
- meeting
- project/name
---# Meeting: [Topic] - YYYY-MM-DD
## Attendees
- [[Person 1]]
- [[Person 2]]
## Agenda
1. Topic 1
2. Topic 2
## Notes
Key discussion points.
## Decisions
- Decision 1
- Decision 2
## Action Items
- [ ] @[[Person 1]] - Action by YYYY-MM-DD
- [ ] @[[Person 2]] - Action by YYYY-MM-DD
## Follow-up
Next meeting: [[YYYYMMDD]]MOC (Map of Content) Template
---
created: YYYY-MM-DDTHH:mm
updated: YYYY-MM-DDTHH:mm
type: moc
tags:
- moc
- topic
---# [Topic] MOC
## Overview
Brief description of this knowledge area.
## Core Concepts
- [[Concept 1]] - brief description
- [[Concept 2]] - brief description
## How-Tos
- [[How To: Task 1]]
- [[How To: Task 2]]
## Decisions
- [[ADR: Decision 1]]
- [[ADR: Decision 2]]
## Projects
- [[Project using this topic]]
## Resources
- [[External reference]]
- [[Book notes]]Capture Workflow
From Conversation to Note
1. Identify capture type - What kind of knowledge is this? (ADR, concept, howto, etc.) 2. Select template - Use appropriate template above 3. Extract key content - Pull relevant information from conversation 4. Add metadata - Frontmatter with timestamps, tags, type 5. Create wikilinks - Link to existing related notes in vault 6. Place in PARA folder - Put in correct location 7. Update MOCs - Add link to relevant Maps of Content if they exist
Auto-Detection Signals
| Signal in Conversation | Capture Type |
|---|---|
| "We decided to..." / "Let's go with..." | ADR |
| "What is X?" / "X works by..." | Concept |
| "How do I..." / "Steps to..." | How-To |
| "In the meeting..." / "We discussed..." | Meeting Note |
| "I want to track all notes about..." | MOC |
| Quick insight or todo | Daily Append |
Linking Strategy
When creating a captured note: 1. Search vault for existing notes on the same topic 2. Add [[wikilinks]] to those notes 3. Check if a relevant MOC exists and add entry 4. Add backlinks from related notes to new note 5. Tag with hierarchical tags matching vault conventions
Obsidian Flavored Markdown Reference
Obsidian uses CommonMark + GitHub Flavored Markdown + LaTeX + Obsidian extensions.
Basic Formatting
| Style | Syntax | Result |
|---|---|---|
| Bold | **text** | Bold |
| Italic | *text* | Italic |
| Bold+Italic | ***text*** | *Both* |
| Strikethrough | ~~text~~ | ~~Striked~~ |
| Highlight | ==text== | ==Highlighted== |
| Inline code | ` code ` | code |
Escape with backslash: \*, \_, \#, ` \ `, \|, \~`
Internal Links (Wikilinks)
[[Note Name]] # Link to note
[[Note Name|Display Text]] # Link with alias
[[Note Name#Heading]] # Link to heading
[[Note Name#^block-id]] # Link to block
[[#Heading in same note]] # Same-file heading link
[[##heading]] # Search headings in vault
[[^^block]] # Search blocks in vaultMarkdown-Style Links
[Display Text](Note%20Name.md) # Spaces must be URL-encoded
[Display Text](Note%20Name.md#Heading)
[Display Text](https://example.com)
[Note](obsidian://open?vault=VaultName&file=Note.md)Embeds (Transclusion)
![[Note Name]] # Embed entire note
![[Note Name#Heading]] # Embed section
![[Note Name#^block-id]] # Embed block
![[image.png]] # Embed image
![[image.png|300]] # Width only
![[image.png|300x200]] # Width x Height
![[audio.mp3]] # Embed audio
![[video.mp4]] # Embed video
![[document.pdf]] # Embed PDF
![[document.pdf#page=3]] # Embed PDF pageExternal Images

Embed Search Results
````markdown
tag:#project status:done````
Block References
This is a paragraph. ^block-id
# Reference from another note:
[[Note#^block-id]]
![[Note#^block-id]]For lists/quotes, add block ID on separate line after the block:
- Item 1
- Item 2
^list-idCallouts
> [!note] Title
> Content
> [!tip]+ Expandable (default open)
> Click to collapse
> [!info]- Collapsed (default closed)
> Click to expandNested Callouts
> [!question] Outer
> > [!note] Inner
> > Nested contentCallout Types
| Type | Aliases |
|---|---|
note | - |
abstract | summary, tldr |
info | - |
todo | - |
tip | hint, important |
success | check, done |
question | help, faq |
warning | caution, attention |
failure | fail, missing |
danger | error |
bug | - |
example | - |
quote | cite |
Custom Callouts (CSS)
.callout[data-callout="custom-type"] {
--callout-color: 255, 0, 0;
--callout-icon: lucide-alert-circle;
}Properties (Frontmatter)
---
title: My Note Title
date: 2024-01-15
tags:
- project
- important
aliases:
- "Alternate Name"
cssclasses:
- custom-class
status: in-progress
rating: 4.5
completed: false
due: 2024-02-01T14:30:00
---Property Types
| Type | Example |
|---|---|
| Text | title: My Title |
| Number | rating: 4.5 |
| Checkbox | completed: true |
| Date | date: 2024-01-15 |
| Date+Time | due: 2024-01-15T14:30:00 |
| List | tags: [one, two] or YAML list |
| Links | related: "[[Other Note]]" |
Default properties: tags, aliases, cssclasses
Tags
#tag
#nested/tag
#tag-with-dashes
#tag_with_underscoresTags can contain: letters (any language), numbers (not first), underscores, hyphens, forward slashes (nesting).
Lists
- [ ] Incomplete task
- [x] Completed task
- Unordered item
- Nested item
1. Ordered item
2. Second item
1. Nested orderedTables
| Left | Center | Right |
|:---------|:--------:|---------:|
| Cell | Cell | Cell |Escape pipes in tables: [[Link\|Display]]
Math (LaTeX)
Inline: $E = mc^2$
Block:
$$
\frac{-b \pm \sqrt{b^2 - 4ac}}{2a}
$$Common: $x^2$, $x_i$, $\frac{a}{b}$, $\sqrt{x}$, $\sum_{i=1}^{n}$, $\int_a^b$, $\alpha, \beta$
Mermaid Diagrams
````markdown
graph TD
A[Start] --> B{Decision}
B -->|Yes| C[Action 1]
B -->|No| D[Action 2]````
Sequence Diagrams
````markdown
sequenceDiagram
Alice->>Bob: Hello
Bob-->>Alice: Hi````
Code Blocks
````markdown
code here````
Nest by using more backticks for the outer block.
Footnotes
Text with footnote[^1].
[^1]: Footnote content.
Named footnote[^note].
[^note]: Named footnote content.
Inline footnote.^[This is inline.]Comments
%%This is hidden%%
%%
Multi-line
hidden comment
%%Horizontal Rules
---
***
___HTML Support
<details>
<summary>Click to expand</summary>
Hidden content here.
</details>
<kbd>Ctrl</kbd> + <kbd>C</kbd>References
Obsidian REST API & URI Scheme Reference
Local REST API Plugin
The Local REST API plugin provides HTTP endpoints for programmatic vault access.
Setup
1. Install "Local REST API" from Community Plugins 2. Enable the plugin 3. Configure API key in plugin settings 4. Default endpoint: https://127.0.0.1:27124
Authentication
curl -H "Authorization: Bearer YOUR_API_KEY" \
https://127.0.0.1:27124/vault/Environment variables: OBSIDIAN_API_KEY, OBSIDIAN_BASE_URL
Endpoints
# File Operations
GET /vault/ # List all files
GET /vault/{path} # Get file content
PUT /vault/{path} # Create/update file (Content-Type: text/markdown)
DELETE /vault/{path} # Delete file
PATCH /vault/{path} # Append/prepend to file
# Search
POST /search/simple/ # Simple text search
POST /search/ # Dataview query (DQL)
POST /search/jsonlogic/ # JsonLogic search
# Active File
GET /active/ # Get active file info
PUT /active/ # Set active file content
# Commands
POST /commands/{command-id} # Execute Obsidian command
GET /commands/ # List available commands
# Open
POST /open/{path} # Open file in ObsidianSearch Examples
# Simple text search
curl -X POST https://127.0.0.1:27124/search/simple/ \
-H "Authorization: Bearer $OBSIDIAN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"query": "search term"}'
# Dataview query (DQL)
curl -X POST https://127.0.0.1:27124/search/ \
-H "Authorization: Bearer $OBSIDIAN_API_KEY" \
-H "Content-Type: application/vnd.olrapi.dataview.dql+txt" \
-d 'TABLE status, tags FROM "01 - Projects" WHERE status != "completed"'
# JsonLogic query
curl -X POST https://127.0.0.1:27124/search/jsonlogic/ \
-H "Authorization: Bearer $OBSIDIAN_API_KEY" \
-H "Content-Type: application/vnd.olrapi.jsonlogic+json" \
-d '{"and": [{"glob": ["01 - Projects/**", {"var": "path"}]}, {"!==": [{"var": "frontmatter.status"}, "completed"]}]}'Python Client (httpx)
import httpx
import os
class ObsidianAPI:
def __init__(self):
self.base_url = os.getenv("OBSIDIAN_BASE_URL", "https://127.0.0.1:27124")
self.api_key = os.getenv("OBSIDIAN_API_KEY", "")
self.client = httpx.Client(
base_url=self.base_url,
headers={"Authorization": f"Bearer {self.api_key}"},
verify=False, # Self-signed cert
timeout=30.0,
)
def list_files(self, path: str = "") -> list:
r = self.client.get(f"/vault/{path}")
r.raise_for_status()
return r.json()["files"]
def read_file(self, path: str) -> str:
r = self.client.get(f"/vault/{path}", headers={"Accept": "text/markdown"})
r.raise_for_status()
return r.text
def write_file(self, path: str, content: str) -> bool:
r = self.client.put(
f"/vault/{path}",
content=content.encode("utf-8"),
headers={"Content-Type": "text/markdown"},
)
return r.status_code == 204
def search_simple(self, query: str) -> list:
r = self.client.post("/search/simple/", json={"query": query})
r.raise_for_status()
return r.json()
def search_dataview(self, dql: str) -> list:
r = self.client.post(
"/search/",
content=dql.encode("utf-8"),
headers={"Content-Type": "application/vnd.olrapi.dataview.dql+txt"},
)
r.raise_for_status()
return r.json()
def search_jsonlogic(self, logic: dict) -> list:
r = self.client.post(
"/search/jsonlogic/",
json=logic,
headers={"Content-Type": "application/vnd.olrapi.jsonlogic+json"},
)
r.raise_for_status()
return r.json()
def open_file(self, path: str) -> bool:
r = self.client.post(f"/open/{path}")
return r.status_code == 200URI Scheme
Native obsidian:// protocol for automation:
obsidian://open?vault=MyVault # Open vault
obsidian://open?vault=MyVault&file=Notes/MyNote # Open file
obsidian://new?vault=MyVault&name=NewNote&content=Hello # Create note
obsidian://search?vault=MyVault&query=keyword # Search
obsidian://daily?vault=MyVault # Open daily noteURI Parameters
| Parameter | Description |
|---|---|
vault | Vault name (required) |
file | File path without .md extension |
path | Full file path including folders |
name | Note name for creation |
content | Content to insert |
query | Search query |
heading | Navigate to heading |
block | Navigate to block reference |
Note: URL-encode spaces as %20, slashes as %2F, ampersands as %26.
Plugin Development API
Core API Classes
| Class | Purpose | Access |
|---|---|---|
App | Central application instance | this.app |
Vault | File system operations | this.app.vault |
Workspace | Pane and layout management | this.app.workspace |
MetadataCache | File metadata indexing | this.app.metadataCache |
FileManager | User-safe file operations | this.app.fileManager |
Plugin Template
import { Plugin, Notice, MarkdownView } from 'obsidian';
export default class MyPlugin extends Plugin {
async onload() {
this.addCommand({
id: 'my-command',
name: 'My Command',
callback: () => new Notice('Hello!'),
});
this.addCommand({
id: 'my-editor-command',
name: 'Insert Text',
editorCallback: (editor, view: MarkdownView) => {
editor.replaceSelection('Inserted text');
},
});
this.registerEvent(
this.app.workspace.on('file-open', (file) => {
if (file) console.log('Opened:', file.path);
})
);
}
onunload() {}
}Plugin Structure
my-plugin/
├── main.ts # Entry point
├── manifest.json # Metadata (id, name, version, minAppVersion)
├── package.json
├── styles.css
├── tsconfig.json
└── esbuild.config.mjsCLI Tools
# Go CLI (Yakitrak)
obsidian-cli open "Note Name"
obsidian-cli search "query"
obsidian-cli create "New Note"
obsidian-cli daily
obsidian-cli list
# Python CLI
obs vault list
obs vault create <name>
obs note search <query>References
Vault Organization Reference
PARA Folder Structure
| Folder | Purpose | Note Types |
|---|---|---|
00 - Inbox/ | Quick capture, unsorted notes | Fleeting thoughts |
00 - Maps of Content/ | Index notes linking topics | MOCs, dashboards |
01 - Projects/ | Active project documentation | Project notes |
02 - Areas/ | Ongoing responsibilities | Area overviews |
03 - Resources/ | Reference materials | Evergreen notes |
04 - Archive/ | Completed/inactive content | Archived projects |
04 - Permanent/ | Zettelkasten-style notes | Atomic ideas |
06 - Daily/ | Daily notes (YYYY/MM/YYYYMMDD.md) | Journal entries |
08 - books/ | Book notes and highlights | Reading notes |
10 - 1-1/ | One-on-one meeting notes | Meeting notes |
System Folders
| Folder | Purpose |
|---|---|
_bmad/ | BMAD Core Platform |
_assets/ | Vault assets and media |
99 - Meta/ | Templates and vault metadata |
attachments/ | Note attachments |
Clippings/ | Web clips and imports |
Excalidraw/ | Diagram files |
memory-bank/ | AI memory/context storage |
topics/ | Topic-based organization |
Frontmatter Schemas
Standard Note
---
created: YYYY-MM-DDTHH:mm
updated: YYYY-MM-DDTHH:mm
tags:
- category/subcategory
---Daily Note
---
created: YYYY-MM-DDTHH:mm
updated: YYYY-MM-DDTHH:mm
title: "YYYYMMDD"
type: daily-note
status: true
tags:
- daily
- YYYY
- YYYY-MM
date_formatted: YYYY-MM-DD
---Project Note
---
created: YYYY-MM-DDTHH:mm
updated: YYYY-MM-DDTHH:mm
type: project
status: active | paused | complete
priority: high | medium | low
tags:
- project/name
---Zettelkasten Note
---
created: YYYY-MM-DDTHH:mm
updated: YYYY-MM-DDTHH:mm
type: zettelkasten
tags:
- permanent
- topic
---Naming Conventions
- Regular notes: Descriptive titles (spaces allowed)
- Daily notes:
YYYYMMDD.mdin06 - Daily/YYYY/MM/ - Templates: Located in
99 - Meta/00 - Templates/ - BMAD files: kebab-case
Link Conventions
- Use
[[wikilinks]]for all internal links - Link to headings:
[[Note#Heading]] - Block references:
[[Note#^block-id]] - Embed notes:
![[Note]] - Embed images:
![[image.png]]
Dataview Queries
Quick Examples
LIST FROM "06 - Daily" WHERE file.cday = date(today) SORT file.ctime DESCTABLE status, tags FROM "01 - Projects" WHERE status != "completed"TABLE WITHOUT ID file.link AS "Note", file.mtime AS "Modified"
FROM "03 - Resources"
SORT file.mtime DESC
LIMIT 20TASK FROM "01 - Projects" WHERE !completedCommon Patterns
# Notes by tag
LIST FROM #project AND -#archived
# Recent notes in folder
TABLE file.mtime AS "Modified" FROM "02 - Areas" SORT file.mtime DESC LIMIT 10
# Notes linking to current file
LIST FROM [[]] SORT file.name ASC
# Orphan detection
LIST FROM "" WHERE length(file.inlinks) = 0 AND length(file.outlinks) = 0Templater Variables
<% tp.file.title %> # Current file name
<% tp.date.now("YYYY-MM-DD") %> # Current date
<% tp.file.cursor(1) %> # Cursor position
<% tp.system.prompt("Question") %> # User input promptInstalled Plugins
| Plugin | Purpose |
|---|---|
| Dataview | Query and display data from notes |
| Templater | Advanced templates with scripting |
| Auto Note Mover | Auto-organize notes by tags |
| Periodic Notes | Daily/weekly/monthly notes |
| Kanban | Kanban boards in markdown |
| Tag Wrangler | Bulk tag management |
| Advanced URI | Deep links to notes |
| Local REST API | External API access |
Lifecycle States
Notes can move through lifecycle states for processing:
inbox -> active -> processed -> archived- inbox: Raw capture, needs triage
- active: Being worked on
- processed: Reviewed, properly tagged and linked
- archived: No longer active, preserved for reference
Best Practices
1. Always include created and updated timestamps in frontmatter 2. Tag notes for discoverability 3. Link to related notes bidirectionally 4. Use callouts for important information 5. Include navigation links in daily notes (<< prev | today | next >>) 6. Place notes in correct PARA folder based on type 7. Use descriptive filenames (avoid special characters except hyphens)
#!/usr/bin/env python3
"""BaseBuilder - Generate Obsidian .base files programmatically.
Commands:
create - Build a .base file from parameters
validate - Validate existing .base YAML
preview - Dry-run showing generated YAML
Environment:
OBSIDIAN_VAULT - Vault path (default: auto-detect from cwd)
"""
from __future__ import annotations
import json
import os
import re
import sys
from pathlib import Path
try:
import click
except ImportError:
print("Missing dependency: pip install click", file=sys.stderr)
sys.exit(1)
try:
import yaml
except ImportError:
yaml = None
# ---------------------------------------------------------------------------
# Vault detection
# ---------------------------------------------------------------------------
def _detect_vault(vault_arg: str | None = None) -> Path:
if vault_arg:
return Path(vault_arg)
env = os.getenv("OBSIDIAN_VAULT")
if env:
return Path(env)
cwd = Path.cwd()
if (cwd / ".obsidian").is_dir():
return cwd
for parent in cwd.parents:
if (parent / ".obsidian").is_dir():
return parent
return cwd
# ---------------------------------------------------------------------------
# YAML generation (manual to avoid pyyaml formatting issues with quoted strings)
# ---------------------------------------------------------------------------
def _build_base_yaml(
name: str,
filter_expr: str | None,
view_type: str,
columns: list[str],
formulas: dict[str, str] | None = None,
group_by: str | None = None,
sort_dir: str = "ASC",
limit: int | None = None,
summaries: dict[str, str] | None = None,
) -> str:
lines = []
# Global filters
if filter_expr:
filters = _parse_filter_expr(filter_expr)
lines.append("filters:")
lines.append(" and:")
for f in filters:
lines.append(f" - '{f}'")
lines.append("")
# Formulas
if formulas:
lines.append("formulas:")
for fname, fexpr in formulas.items():
lines.append(f" {fname}: '{fexpr}'")
lines.append("")
# Properties display names for formulas
if formulas:
lines.append("properties:")
for fname in formulas:
display = fname.replace("_", " ").title()
lines.append(f" formula.{fname}:")
lines.append(f' displayName: "{display}"')
lines.append("")
# Views
lines.append("views:")
lines.append(f" - type: {view_type}")
lines.append(f' name: "{name}"')
if limit:
lines.append(f" limit: {limit}")
if group_by:
lines.append(" groupBy:")
lines.append(f" property: {group_by}")
lines.append(f" direction: {sort_dir}")
lines.append(" order:")
for col in columns:
lines.append(f" - {col}")
if summaries:
lines.append(" summaries:")
for prop, summary_type in summaries.items():
lines.append(f" {prop}: {summary_type}")
return "\n".join(lines) + "\n"
def _parse_filter_expr(expr: str) -> list[str]:
"""Split a filter expression by && into individual filter strings."""
parts = re.split(r"\s*&&\s*", expr)
return [p.strip() for p in parts if p.strip()]
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
@click.group()
def cli():
"""Generate Obsidian .base files."""
@cli.command()
@click.option("--name", required=True, help="View name")
@click.option("--filter", "filter_expr", help="Filter expression (use && for AND)")
@click.option("--view", "view_type", type=click.Choice(["table", "cards", "list", "map"]), default="table")
@click.option("--columns", required=True, help="Comma-separated column list")
@click.option("--formula", "formula_list", multiple=True, help="name=expression (repeatable)")
@click.option("--group-by", help="Property to group by")
@click.option("--sort", "sort_dir", type=click.Choice(["ASC", "DESC"]), default="ASC")
@click.option("--limit", type=int, help="Max results")
@click.option("--summary", "summary_list", multiple=True, help="property=SummaryType (repeatable)")
@click.option("--output", help="Output .base file path")
@click.option("--vault", help="Vault path")
def create(name, filter_expr, view_type, columns, formula_list, group_by, sort_dir, limit, summary_list, output, vault):
"""Create a .base file from parameters."""
col_list = [c.strip() for c in columns.split(",")]
formulas = {}
for f in formula_list:
if "=" not in f:
raise click.ClickException(f"Formula must be name=expression: {f}")
fname, fexpr = f.split("=", 1)
formulas[fname.strip()] = fexpr.strip()
summaries = {}
for s in summary_list:
if "=" not in s:
raise click.ClickException(f"Summary must be property=Type: {s}")
prop, stype = s.split("=", 1)
summaries[prop.strip()] = stype.strip()
base_yaml = _build_base_yaml(
name=name,
filter_expr=filter_expr,
view_type=view_type,
columns=col_list,
formulas=formulas or None,
group_by=group_by,
sort_dir=sort_dir,
limit=limit,
summaries=summaries or None,
)
if output:
v = _detect_vault(vault)
dest = v / output if not Path(output).is_absolute() else Path(output)
dest.parent.mkdir(parents=True, exist_ok=True)
dest.write_text(base_yaml, encoding="utf-8")
click.echo(f"Created: {dest}")
else:
click.echo(base_yaml)
@cli.command()
@click.option("--path", required=True, help="Path to .base file")
@click.option("--vault", help="Vault path")
def validate(path, vault):
"""Validate existing .base YAML."""
if yaml is None:
raise click.ClickException("pyyaml required for validation. Run: pip install pyyaml")
v = _detect_vault(vault)
base_path = v / path if not Path(path).is_absolute() else Path(path)
if not base_path.exists():
raise click.ClickException(f"File not found: {base_path}")
text = base_path.read_text(encoding="utf-8")
try:
data = yaml.safe_load(text)
except yaml.YAMLError as e:
raise click.ClickException(f"Invalid YAML: {e}")
if not isinstance(data, dict):
raise click.ClickException("Root must be a YAML mapping")
errors = []
warnings = []
# Check views
views = data.get("views", [])
if not views:
errors.append("No views defined")
for i, view in enumerate(views):
if not isinstance(view, dict):
errors.append(f"View {i} is not a mapping")
continue
vtype = view.get("type")
if vtype not in ("table", "cards", "list", "map"):
errors.append(f"View {i}: invalid type '{vtype}'")
if not view.get("name"):
warnings.append(f"View {i}: no name specified")
if not view.get("order"):
warnings.append(f"View {i}: no order (columns) specified")
# Check formulas reference valid syntax
formulas = data.get("formulas", {})
if formulas and not isinstance(formulas, dict):
errors.append("formulas must be a mapping")
# Check filters
filters = data.get("filters")
if filters and not isinstance(filters, (str, dict)):
errors.append("filters must be a string or mapping with and/or/not")
if errors:
click.echo("INVALID")
for e in errors:
click.echo(f" ERROR: {e}")
for w in warnings:
click.echo(f" WARN: {w}")
sys.exit(1)
else:
click.echo("VALID")
for w in warnings:
click.echo(f" WARN: {w}")
click.echo(f" Views: {len(views)}")
click.echo(f" Formulas: {len(formulas)}")
@cli.command()
@click.option("--filter", "filter_expr", help="Filter expression")
@click.option("--view", "view_type", type=click.Choice(["table", "cards", "list", "map"]), default="table")
@click.option("--columns", required=True, help="Comma-separated column list")
@click.option("--formula", "formula_list", multiple=True, help="name=expression (repeatable)")
def preview(filter_expr, view_type, columns, formula_list):
"""Preview generated YAML (dry run)."""
col_list = [c.strip() for c in columns.split(",")]
formulas = {}
for f in formula_list:
if "=" in f:
fname, fexpr = f.split("=", 1)
formulas[fname.strip()] = fexpr.strip()
base_yaml = _build_base_yaml(
name="Preview",
filter_expr=filter_expr,
view_type=view_type,
columns=col_list,
formulas=formulas or None,
)
click.echo(base_yaml)
if __name__ == "__main__":
cli()
#!/usr/bin/env python3
"""NoteCreator - Create notes with templates, frontmatter, and wikilinks.
Commands:
create - Create a new note with template and PARA placement
daily - Create or append to daily note
capture - Capture knowledge from conversation into vault
Environment:
OBSIDIAN_VAULT - Vault path (default: auto-detect from cwd)
"""
from __future__ import annotations
import json
import os
import re
import sys
from datetime import datetime, timedelta, timezone
from pathlib import Path
try:
import click
except ImportError:
print("Missing dependency: pip install click", file=sys.stderr)
sys.exit(1)
# ---------------------------------------------------------------------------
# Constants & vault detection
# ---------------------------------------------------------------------------
PARA_FOLDERS = {
"inbox": "00 - Inbox",
"moc": "00 - Maps of Content",
"projects": "01 - Projects",
"areas": "02 - Areas",
"resources": "03 - Resources",
"archive": "04 - Archive",
"permanent": "04 - Permanent",
"daily": "06 - Daily",
"books": "08 - books",
"meetings": "10 - 1-1",
}
CAPTURE_DESTINATIONS = {
"concept": "permanent",
"adr": "resources",
"howto": "resources",
"meeting": "meetings",
"moc": "moc",
"project": "projects",
"daily": "daily",
"generic": "inbox",
}
WIKILINK_RE = re.compile(r"\[\[([^\]|#]+?)(?:#[^\]|]*)?(?:\|[^\]]+)?\]\]")
def _detect_vault(vault_arg: str | None = None) -> Path:
if vault_arg:
return Path(vault_arg)
env = os.getenv("OBSIDIAN_VAULT")
if env:
return Path(env)
cwd = Path.cwd()
if (cwd / ".obsidian").is_dir():
return cwd
for parent in cwd.parents:
if (parent / ".obsidian").is_dir():
return parent
return cwd
def _now_str() -> str:
return datetime.now().strftime("%Y-%m-%dT%H:%M")
def _today() -> datetime:
return datetime.now()
# ---------------------------------------------------------------------------
# Templates
# ---------------------------------------------------------------------------
def _template_generic(title: str, tags: list[str]) -> str:
tag_yaml = "\n".join(f" - {t}" for t in tags) if tags else " - inbox"
return f"""---
created: {_now_str()}
updated: {_now_str()}
tags:
{tag_yaml}
---
# {title}
"""
def _template_concept(title: str, tags: list[str]) -> str:
tag_yaml = "\n".join(f" - {t}" for t in ["permanent", "concept"] + tags)
return f"""---
created: {_now_str()}
updated: {_now_str()}
type: zettelkasten
tags:
{tag_yaml}
---
# {title}
## Definition
One-paragraph explanation in your own words.
## Key Insight
**Core idea**:
## How It Works
## Examples
-
## Connections
-
## Sources
-
"""
def _template_adr(title: str, tags: list[str]) -> str:
tag_yaml = "\n".join(f" - {t}" for t in ["decision"] + tags)
return f"""---
created: {_now_str()}
updated: {_now_str()}
type: adr
status: accepted
tags:
{tag_yaml}
---
# ADR: {title}
## Status
Accepted
## Context
What is the issue we're deciding on?
## Decision
What is the change that we're proposing?
## Consequences
### Positive
-
### Negative
-
## Related
-
"""
def _template_howto(title: str, tags: list[str]) -> str:
tag_yaml = "\n".join(f" - {t}" for t in ["howto"] + tags)
return f"""---
created: {_now_str()}
updated: {_now_str()}
type: howto
tags:
{tag_yaml}
---
# How To: {title}
## Prerequisites
-
## Steps
### 1. First Step
### 2. Second Step
### 3. Third Step
## Verification
How to confirm it worked.
## Troubleshooting
Common issues and fixes.
## Related
-
"""
def _template_meeting(title: str, tags: list[str]) -> str:
tag_yaml = "\n".join(f" - {t}" for t in ["meeting"] + tags)
date = _today().strftime("%Y-%m-%d")
return f"""---
created: {_now_str()}
updated: {_now_str()}
type: meeting
attendees: []
tags:
{tag_yaml}
---
# Meeting: {title} - {date}
## Attendees
-
## Agenda
1.
## Notes
## Decisions
-
## Action Items
- [ ]
## Follow-up
"""
def _template_project(title: str, tags: list[str]) -> str:
tag_yaml = "\n".join(f" - {t}" for t in [f"project/{title.lower().replace(' ', '-')}"] + tags)
return f"""---
created: {_now_str()}
updated: {_now_str()}
type: project
status: active
priority: medium
tags:
{tag_yaml}
---
# {title}
## Overview
## Goals
-
## Tasks
- [ ]
## Notes
## Related
-
"""
def _template_moc(title: str, tags: list[str]) -> str:
tag_yaml = "\n".join(f" - {t}" for t in ["moc"] + tags)
return f"""---
created: {_now_str()}
updated: {_now_str()}
type: moc
tags:
{tag_yaml}
---
# {title} MOC
## Overview
## Core Concepts
-
## How-Tos
-
## Decisions
-
## Projects
-
## Resources
-
"""
def _template_daily(dt: datetime) -> str:
date_str = dt.strftime("%Y-%m-%d")
title = dt.strftime("%Y%m%d")
year = dt.strftime("%Y")
month = dt.strftime("%Y-%m")
prev_dt = dt - timedelta(days=1)
next_dt = dt + timedelta(days=1)
prev = prev_dt.strftime("%Y%m%d")
nxt = next_dt.strftime("%Y%m%d")
return f"""---
created: {dt.strftime("%Y-%m-%dT%H:%M")}
updated: {dt.strftime("%Y-%m-%dT%H:%M")}
title: "{title}"
type: daily-note
status: true
tags:
- daily
- {year}
- {month}
date_formatted: {date_str}
---
# Daily Note - {date_str}
### Tasks
- [ ]
### Journal
### Navigation
<< [[{prev}]] | **Today** | [[{nxt}]] >>
"""
TEMPLATES = {
"generic": _template_generic,
"concept": _template_concept,
"adr": _template_adr,
"howto": _template_howto,
"meeting": _template_meeting,
"project": _template_project,
"moc": _template_moc,
}
# ---------------------------------------------------------------------------
# Wikilink scanning
# ---------------------------------------------------------------------------
def _scan_related(vault: Path, title: str, content: str) -> list[str]:
"""Find existing notes that could be linked."""
words = set(title.lower().split())
related = []
for md in vault.rglob("*.md"):
rel = str(md.relative_to(vault))
if rel.startswith(".obsidian"):
continue
name = md.stem.lower()
if any(w in name for w in words if len(w) > 3):
related.append(md.stem)
return related[:10]
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
@click.group()
def cli():
"""Create Obsidian notes with templates and proper organization."""
@cli.command()
@click.option("--title", required=True, help="Note title")
@click.option("--template", "tmpl", type=click.Choice(list(TEMPLATES.keys())), default="generic")
@click.option("--folder", type=click.Choice(list(PARA_FOLDERS.keys())), default="inbox")
@click.option("--tags", help="Comma-separated tags")
@click.option("--content", help="Additional body content")
@click.option("--vault", help="Vault path")
@click.option("--dry-run", is_flag=True, help="Preview without creating")
def create(title, tmpl, folder, tags, content, vault, dry_run):
"""Create a new note with template and PARA placement."""
v = _detect_vault(vault)
tag_list = [t.strip() for t in tags.split(",")] if tags else []
template_fn = TEMPLATES[tmpl]
note_content = template_fn(title, tag_list)
if content:
note_content += f"\n{content}\n"
dest_dir = v / PARA_FOLDERS[folder]
if tmpl == "adr":
dest_dir = dest_dir / "decisions"
elif tmpl == "howto":
dest_dir = dest_dir / "howtos"
filename = f"{title}.md"
dest = dest_dir / filename
if dry_run:
click.echo(f"Would create: {dest.relative_to(v)}")
click.echo("---")
click.echo(note_content)
return
dest_dir.mkdir(parents=True, exist_ok=True)
if dest.exists():
raise click.ClickException(f"Note already exists: {dest.relative_to(v)}")
dest.write_text(note_content, encoding="utf-8")
click.echo(f"Created: {dest.relative_to(v)}")
related = _scan_related(v, title, note_content)
if related:
click.echo(f"Related notes: {', '.join(f'[[{r}]]' for r in related)}")
@cli.command()
@click.option("--date", "date_str", help="Date (YYYY-MM-DD), default today")
@click.option("--content", help="Initial content")
@click.option("--append", "append_text", help="Append to existing daily note")
@click.option("--vault", help="Vault path")
@click.option("--dry-run", is_flag=True)
def daily(date_str, content, append_text, vault, dry_run):
"""Create or append to daily note."""
v = _detect_vault(vault)
dt = datetime.strptime(date_str, "%Y-%m-%d") if date_str else _today()
year = dt.strftime("%Y")
month = dt.strftime("%m")
filename = dt.strftime("%Y%m%d.md")
dest_dir = v / "06 - Daily" / year / month
dest = dest_dir / filename
if append_text and dest.exists():
existing = dest.read_text(encoding="utf-8")
now = _now_str()
existing = re.sub(r"^updated:.*$", f"updated: {now}", existing, count=1, flags=re.MULTILINE)
new_content = f"{existing.rstrip()}\n\n{append_text}\n"
if dry_run:
click.echo(f"Would append to: {dest.relative_to(v)}")
click.echo(f"Content: {append_text}")
return
dest.write_text(new_content, encoding="utf-8")
click.echo(f"Appended to: {dest.relative_to(v)}")
return
if dest.exists() and not append_text:
click.echo(f"Daily note exists: {dest.relative_to(v)}")
return
note_content = _template_daily(dt)
if content:
note_content = note_content.replace("### Journal\n", f"### Journal\n{content}\n")
if dry_run:
click.echo(f"Would create: {dest.relative_to(v)}")
click.echo("---")
click.echo(note_content)
return
dest_dir.mkdir(parents=True, exist_ok=True)
dest.write_text(note_content, encoding="utf-8")
click.echo(f"Created: {dest.relative_to(v)}")
@cli.command()
@click.option("--type", "capture_type", type=click.Choice(list(CAPTURE_DESTINATIONS.keys())), required=True)
@click.option("--title", help="Note title (required for non-daily)")
@click.option("--content", required=True, help="Content to capture")
@click.option("--tags", help="Comma-separated tags")
@click.option("--vault", help="Vault path")
@click.option("--dry-run", is_flag=True)
def capture(capture_type, title, content, tags, vault, dry_run):
"""Capture knowledge from conversation into vault."""
v = _detect_vault(vault)
if capture_type == "daily":
# Append to today's daily note
dt = _today()
year = dt.strftime("%Y")
month = dt.strftime("%m")
filename = dt.strftime("%Y%m%d.md")
dest = v / "06 - Daily" / year / month / filename
if dest.exists():
existing = dest.read_text(encoding="utf-8")
now = _now_str()
existing = re.sub(r"^updated:.*$", f"updated: {now}", existing, count=1, flags=re.MULTILINE)
new_content = f"{existing.rstrip()}\n\n{content}\n"
if dry_run:
click.echo(f"Would append to daily: {dest.relative_to(v)}")
return
dest.write_text(new_content, encoding="utf-8")
click.echo(f"Appended to daily: {dest.relative_to(v)}")
else:
note = _template_daily(dt)
note = note.replace("### Journal\n", f"### Journal\n{content}\n")
if dry_run:
click.echo(f"Would create daily with content: {dest.relative_to(v)}")
return
dest.parent.mkdir(parents=True, exist_ok=True)
dest.write_text(note, encoding="utf-8")
click.echo(f"Created daily with content: {dest.relative_to(v)}")
return
if not title:
raise click.ClickException("--title required for non-daily captures")
folder = CAPTURE_DESTINATIONS[capture_type]
tag_list = [t.strip() for t in tags.split(",")] if tags else []
if capture_type in TEMPLATES:
template_fn = TEMPLATES[capture_type]
note_content = template_fn(title, tag_list)
# Inject content into appropriate section
if capture_type == "concept":
note_content = note_content.replace(
"One-paragraph explanation in your own words.",
content,
)
elif capture_type == "adr":
note_content = note_content.replace(
"What is the issue we're deciding on?",
content,
)
elif capture_type == "howto":
note_content = note_content.replace(
"### 1. First Step\n",
f"### 1. First Step\n{content}\n",
)
else:
note_content += f"\n{content}\n"
else:
note_content = _template_generic(title, tag_list)
note_content += f"\n{content}\n"
dest_dir = v / PARA_FOLDERS[folder]
if capture_type == "adr":
dest_dir = dest_dir / "decisions"
elif capture_type == "howto":
dest_dir = dest_dir / "howtos"
dest = dest_dir / f"{title}.md"
if dry_run:
click.echo(f"Would create: {dest.relative_to(v)}")
click.echo("---")
click.echo(note_content)
return
dest_dir.mkdir(parents=True, exist_ok=True)
if dest.exists():
raise click.ClickException(f"Note already exists: {dest.relative_to(v)}")
dest.write_text(note_content, encoding="utf-8")
click.echo(f"Captured: {dest.relative_to(v)}")
related = _scan_related(v, title, note_content)
if related:
click.echo(f"Related notes: {', '.join(f'[[{r}]]' for r in related)}")
if __name__ == "__main__":
cli()
#!/usr/bin/env python3
"""SearchVault - Search Obsidian vault via REST API or direct file access.
Commands:
status - Check REST API connectivity
auth - Verify API key
search - Search vault (--type dataview|content|jsonlogic)
Environment:
OBSIDIAN_API_KEY - REST API bearer token
OBSIDIAN_BASE_URL - API base URL (default: https://127.0.0.1:27124)
OBSIDIAN_VAULT - Vault path for direct file fallback
"""
from __future__ import annotations
import json
import os
import re
import sys
from pathlib import Path
try:
import click
except ImportError:
print("Missing dependency: pip install click", file=sys.stderr)
sys.exit(1)
try:
import httpx
except ImportError:
httpx = None
# ---------------------------------------------------------------------------
# REST API Client
# ---------------------------------------------------------------------------
API_BASE = os.getenv("OBSIDIAN_BASE_URL", "https://127.0.0.1:27124")
API_KEY = os.getenv("OBSIDIAN_API_KEY", "")
def _client() -> "httpx.Client":
if httpx is None:
raise click.ClickException("httpx not installed. Run: pip install httpx")
return httpx.Client(
base_url=API_BASE,
headers={"Authorization": f"Bearer {API_KEY}"},
verify=False,
timeout=30.0,
)
def _api_available() -> bool:
try:
c = _client()
r = c.get("/vault/")
c.close()
return r.status_code == 200
except Exception:
return False
# ---------------------------------------------------------------------------
# Direct file search fallback
# ---------------------------------------------------------------------------
def _detect_vault() -> Path:
"""Detect vault path from env or cwd."""
env = os.getenv("OBSIDIAN_VAULT")
if env:
return Path(env)
cwd = Path.cwd()
if (cwd / ".obsidian").is_dir():
return cwd
for parent in cwd.parents:
if (parent / ".obsidian").is_dir():
return parent
return cwd
def _direct_content_search(
query: str,
folder: str | None = None,
limit: int = 50,
) -> list[dict]:
vault = _detect_vault()
base = vault / folder if folder else vault
pattern = re.compile(re.escape(query), re.IGNORECASE)
results = []
for md in sorted(base.rglob("*.md")):
rel = str(md.relative_to(vault))
if rel.startswith(".obsidian"):
continue
try:
text = md.read_text(encoding="utf-8", errors="replace")
except OSError:
continue
matches = []
for i, line in enumerate(text.splitlines(), 1):
if pattern.search(line):
matches.append({"line": i, "text": line.strip()})
if matches:
results.append({"path": rel, "matches": matches})
if len(results) >= limit:
break
return results
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
@click.group()
def cli():
"""Search Obsidian vault via REST API or direct file access."""
@cli.command()
def status():
"""Check REST API connectivity."""
if httpx is None:
click.echo("httpx not installed - REST API unavailable")
click.echo(f"Direct file fallback: vault at {_detect_vault()}")
return
available = _api_available()
click.echo(f"REST API ({API_BASE}): {'connected' if available else 'unavailable'}")
if not available:
click.echo(f"Direct file fallback: vault at {_detect_vault()}")
@cli.command()
def auth():
"""Verify API key authentication."""
if httpx is None:
raise click.ClickException("httpx not installed. Run: pip install httpx")
try:
c = _client()
r = c.get("/vault/")
c.close()
if r.status_code == 200:
click.echo("Authenticated successfully")
elif r.status_code == 401:
click.echo("Authentication failed - check OBSIDIAN_API_KEY")
else:
click.echo(f"Unexpected status: {r.status_code}")
except Exception as e:
click.echo(f"Connection error: {e}")
@cli.command()
@click.argument("query", required=False)
@click.option("--type", "search_type", type=click.Choice(["dataview", "content", "jsonlogic"]), default="content")
@click.option("--query", "query_opt", help="Search query (alternative to positional arg)")
@click.option("--folder", help="Limit search to folder")
@click.option("--tags", help="Filter by tags (comma-separated)")
@click.option("--json", "as_json", is_flag=True, help="JSON output")
@click.option("--table", "as_table", is_flag=True, help="Table output")
@click.option("--limit", default=50, help="Max results")
def search(query, search_type, query_opt, folder, tags, as_json, as_table, limit):
"""Search vault. Positional QUERY or --query flag."""
q = query or query_opt
if not q:
raise click.ClickException("Provide a search query as argument or --query")
use_api = httpx is not None and _api_available()
if search_type == "dataview":
if not use_api:
raise click.ClickException("Dataview search requires Obsidian REST API running")
c = _client()
r = c.post(
"/search/",
content=q.encode("utf-8"),
headers={"Content-Type": "application/vnd.olrapi.dataview.dql+txt"},
)
c.close()
if r.status_code != 200:
raise click.ClickException(f"Dataview query failed ({r.status_code}): {r.text}")
results = r.json()
elif search_type == "jsonlogic":
if not use_api:
raise click.ClickException("JsonLogic search requires Obsidian REST API running")
logic = json.loads(q)
c = _client()
r = c.post(
"/search/jsonlogic/",
json=logic,
headers={"Content-Type": "application/vnd.olrapi.jsonlogic+json"},
)
c.close()
if r.status_code != 200:
raise click.ClickException(f"JsonLogic query failed ({r.status_code}): {r.text}")
results = r.json()
elif search_type == "content":
if use_api:
c = _client()
r = c.post("/search/simple/", json={"query": q})
c.close()
if r.status_code != 200:
raise click.ClickException(f"Search failed ({r.status_code}): {r.text}")
results = r.json()
if folder:
results = [r for r in results if r.get("filename", "").startswith(folder)]
results = results[:limit]
else:
results = _direct_content_search(q, folder=folder, limit=limit)
else:
raise click.ClickException(f"Unknown search type: {search_type}")
# Tag filtering (post-filter for content/jsonlogic)
if tags and isinstance(results, list):
tag_set = {t.strip().lstrip("#") for t in tags.split(",")}
filtered = []
for r in results:
note_tags = set()
for t in r.get("tags", []):
note_tags.add(t.lstrip("#"))
if tag_set & note_tags:
filtered.append(r)
results = filtered
# Output
if as_json:
click.echo(json.dumps(results, indent=2, default=str))
elif as_table and isinstance(results, list):
for item in results:
if isinstance(item, dict):
path = item.get("path") or item.get("filename", "?")
matches = item.get("matches", [])
click.echo(f"\n {path}")
for m in matches[:3]:
if isinstance(m, dict):
click.echo(f" L{m.get('line', '?')}: {m.get('text', '')[:100]}")
else:
if isinstance(results, list):
for item in results:
if isinstance(item, dict):
click.echo(item.get("path") or item.get("filename", json.dumps(item, default=str)))
else:
click.echo(str(item))
else:
click.echo(json.dumps(results, indent=2, default=str))
if __name__ == "__main__":
cli()
#!/usr/bin/env python3
"""VaultManager - Vault health checks, orphan detection, and lifecycle management.
Commands:
health - Full vault health report
orphans - Find notes with no inlinks or outlinks
broken-links - Find wikilinks pointing to non-existent notes
lifecycle - Change note lifecycle state
move - Move note between PARA folders
inbox - List inbox notes for triage
Environment:
OBSIDIAN_VAULT - Vault path (default: auto-detect from cwd)
"""
from __future__ import annotations
import json
import os
import re
import shutil
import sys
from collections import Counter, defaultdict
from datetime import datetime, timezone
from pathlib import Path
try:
import click
except ImportError:
print("Missing dependency: pip install click", file=sys.stderr)
sys.exit(1)
try:
import yaml
except ImportError:
yaml = None
# ---------------------------------------------------------------------------
# Vault detection
# ---------------------------------------------------------------------------
WIKILINK_RE = re.compile(r"\[\[([^\]|#]+?)(?:#[^\]|]*)?(?:\|[^\]]+)?\]\]")
FRONTMATTER_RE = re.compile(r"^---\n(.*?)\n---", re.DOTALL)
TAG_RE = re.compile(r"(?:^|\s)#([a-zA-Z][a-zA-Z0-9_/\-]*)", re.MULTILINE)
def _detect_vault(vault_arg: str | None = None) -> Path:
if vault_arg:
return Path(vault_arg)
env = os.getenv("OBSIDIAN_VAULT")
if env:
return Path(env)
cwd = Path.cwd()
if (cwd / ".obsidian").is_dir():
return cwd
for parent in cwd.parents:
if (parent / ".obsidian").is_dir():
return parent
return cwd
def _iter_notes(vault: Path):
for md in vault.rglob("*.md"):
rel = str(md.relative_to(vault))
if rel.startswith(".obsidian") or rel.startswith("."):
continue
yield md, rel
def _read_note(path: Path) -> str:
try:
return path.read_text(encoding="utf-8", errors="replace")
except OSError:
return ""
def _parse_frontmatter(text: str) -> dict:
m = FRONTMATTER_RE.match(text)
if not m:
return {}
if yaml:
try:
return yaml.safe_load(m.group(1)) or {}
except Exception:
return {}
return {"_raw": m.group(1)}
def _extract_wikilinks(text: str) -> list[str]:
return WIKILINK_RE.findall(text)
def _extract_tags(text: str) -> list[str]:
return TAG_RE.findall(text)
def _note_name(rel: str) -> str:
return Path(rel).stem
def _folder_name(rel: str) -> str:
parts = Path(rel).parts
return parts[0] if len(parts) > 1 else "/"
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
@click.group()
def cli():
"""Obsidian vault management and health checks."""
@cli.command()
@click.option("--vault", help="Vault path")
@click.option("--json", "as_json", is_flag=True, help="JSON output")
def health(vault, as_json):
"""Full vault health report."""
v = _detect_vault(vault)
folder_counts: Counter = Counter()
tag_counts: Counter = Counter()
total = 0
empty_notes = []
no_frontmatter = []
no_tags = []
for md, rel in _iter_notes(v):
total += 1
folder_counts[_folder_name(rel)] += 1
text = _read_note(md)
if len(text.strip()) < 10:
empty_notes.append(rel)
fm = _parse_frontmatter(text)
if not fm:
no_frontmatter.append(rel)
tags = fm.get("tags", []) or []
if isinstance(tags, list):
for t in tags:
tag_counts[str(t)] += 1
inline_tags = _extract_tags(text)
for t in inline_tags:
tag_counts[t] += 1
if not tags and not inline_tags:
no_tags.append(rel)
fm_coverage = ((total - len(no_frontmatter)) / total * 100) if total else 0
report = {
"total_notes": total,
"by_folder": dict(folder_counts.most_common(20)),
"frontmatter_coverage_pct": round(fm_coverage, 1),
"notes_without_tags": len(no_tags),
"empty_notes": len(empty_notes),
"top_tags": dict(tag_counts.most_common(15)),
}
if as_json:
click.echo(json.dumps(report, indent=2))
else:
click.echo("Vault Health Report")
click.echo("=" * 40)
click.echo(f"Total notes: {total:,}")
click.echo(f"\nBy folder:")
for folder, count in folder_counts.most_common(20):
click.echo(f" {folder:<30} {count:>5}")
click.echo(f"\nFrontmatter coverage: {fm_coverage:.1f}%")
click.echo(f"Notes without tags: {len(no_tags)}")
click.echo(f"Empty notes: {len(empty_notes)}")
if empty_notes:
for n in empty_notes[:5]:
click.echo(f" - {n}")
click.echo(f"\nTop tags:")
for tag, count in tag_counts.most_common(15):
click.echo(f" #{tag:<25} {count:>4}")
@cli.command()
@click.option("--vault", help="Vault path")
@click.option("--json", "as_json", is_flag=True)
def orphans(vault, as_json):
"""Find notes with no inlinks or outlinks."""
v = _detect_vault(vault)
outlinks: dict[str, set] = {}
inlinks: defaultdict[str, set] = defaultdict(set)
all_names: dict[str, str] = {}
for md, rel in _iter_notes(v):
name = _note_name(rel)
all_names[name.lower()] = rel
text = _read_note(md)
links = _extract_wikilinks(text)
outlinks[rel] = {l.strip() for l in links}
for link in links:
inlinks[link.strip().lower()].add(rel)
orphan_list = []
for md, rel in _iter_notes(v):
name = _note_name(rel)
has_outlinks = bool(outlinks.get(rel))
has_inlinks = bool(inlinks.get(name.lower()))
if not has_outlinks and not has_inlinks:
orphan_list.append(rel)
if as_json:
click.echo(json.dumps({"orphans": orphan_list, "count": len(orphan_list)}, indent=2))
else:
click.echo(f"Orphan notes (no inlinks or outlinks): {len(orphan_list)}")
for o in sorted(orphan_list):
click.echo(f" - {o}")
@cli.command("broken-links")
@click.option("--vault", help="Vault path")
@click.option("--json", "as_json", is_flag=True)
def broken_links(vault, as_json):
"""Find wikilinks pointing to non-existent notes."""
v = _detect_vault(vault)
all_names: set[str] = set()
for md, rel in _iter_notes(v):
all_names.add(_note_name(rel).lower())
broken = []
for md, rel in _iter_notes(v):
text = _read_note(md)
links = _extract_wikilinks(text)
for link in links:
target = link.strip().lower()
if target and target not in all_names:
broken.append({"source": rel, "target": link.strip()})
if as_json:
click.echo(json.dumps({"broken_links": broken, "count": len(broken)}, indent=2))
else:
click.echo(f"Broken wikilinks: {len(broken)}")
seen = set()
for b in broken:
key = f"{b['source']} -> {b['target']}"
if key not in seen:
seen.add(key)
click.echo(f" {b['source']}")
click.echo(f" -> [[{b['target']}]]")
@cli.command()
@click.option("--vault", help="Vault path")
@click.option("--path", "note_path", required=True, help="Note path relative to vault")
@click.option("--state", type=click.Choice(["active", "processed", "archived"]), required=True)
def lifecycle(vault, note_path, state):
"""Change note lifecycle state."""
v = _detect_vault(vault)
full = v / note_path
if not full.exists():
raise click.ClickException(f"Note not found: {note_path}")
text = _read_note(full)
now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M")
fm = FRONTMATTER_RE.match(text)
if fm:
fm_text = fm.group(1)
if re.search(r"^status:", fm_text, re.MULTILINE):
fm_text = re.sub(r"^status:.*$", f"status: {state}", fm_text, flags=re.MULTILINE)
else:
fm_text += f"\nstatus: {state}"
fm_text = re.sub(r"^updated:.*$", f"updated: {now}", fm_text, flags=re.MULTILINE)
new_text = f"---\n{fm_text}\n---{text[fm.end():]}"
else:
new_text = f"---\nstatus: {state}\nupdated: {now}\n---\n{text}"
full.write_text(new_text, encoding="utf-8")
click.echo(f"Updated {note_path} -> status: {state}")
@cli.command()
@click.option("--vault", help="Vault path")
@click.option("--path", "note_path", required=True, help="Note path relative to vault")
@click.option("--to", "dest", required=True, help="Destination folder")
def move(vault, note_path, dest):
"""Move note between PARA folders."""
v = _detect_vault(vault)
src = v / note_path
if not src.exists():
raise click.ClickException(f"Note not found: {note_path}")
dest_dir = v / dest
dest_dir.mkdir(parents=True, exist_ok=True)
dst = dest_dir / src.name
if dst.exists():
raise click.ClickException(f"Destination already exists: {dst.relative_to(v)}")
shutil.move(str(src), str(dst))
click.echo(f"Moved: {note_path} -> {dst.relative_to(v)}")
@cli.command()
@click.option("--vault", help="Vault path")
@click.option("--json", "as_json", is_flag=True)
def inbox(vault, as_json):
"""List notes in 00 - Inbox/ for triage."""
v = _detect_vault(vault)
inbox_dir = v / "00 - Inbox"
if not inbox_dir.is_dir():
click.echo("No inbox folder found (00 - Inbox/)")
return
notes = []
for md in sorted(inbox_dir.rglob("*.md")):
rel = str(md.relative_to(v))
text = _read_note(md)
fm = _parse_frontmatter(text)
preview = ""
lines = text.split("\n")
for line in lines:
stripped = line.strip()
if stripped and not stripped.startswith("---") and not stripped.startswith("#"):
preview = stripped[:100]
break
notes.append({
"path": rel,
"name": md.stem,
"has_frontmatter": bool(fm),
"tags": fm.get("tags", []),
"preview": preview,
"size": md.stat().st_size,
"modified": datetime.fromtimestamp(md.stat().st_mtime).isoformat(),
})
if as_json:
click.echo(json.dumps(notes, indent=2, default=str))
else:
click.echo(f"Inbox notes: {len(notes)}")
for n in notes:
fm_icon = "+" if n["has_frontmatter"] else "-"
click.echo(f" [{fm_icon}] {n['name']}")
if n["preview"]:
click.echo(f" {n['preview']}")
if __name__ == "__main__":
cli()
Workflow: Capture Knowledge
Extract insights from conversations and save them as vault notes.
Steps
1. Identify knowledge type from conversation signals:
- "We decided..." -> ADR
- "What is X?" / explanation -> Concept
- "How do I..." / steps -> How-To
- "In the meeting..." -> Meeting Note
- Quick insight -> Daily Append
2. Select template from reference/knowledge-capture.md 3. Extract content from conversation context 4. Create note via Tools/NoteCreator.py capture 5. Link to existing notes - search vault for related content 6. Update MOCs if relevant Map of Content exists
Tool Usage
# Capture a concept
python Tools/NoteCreator.py capture \
--type concept \
--title "Event Sourcing" \
--content "Event sourcing stores state as a sequence of events..." \
--tags "architecture,patterns"
# Capture an ADR
python Tools/NoteCreator.py capture \
--type adr \
--title "Use PostgreSQL for primary storage" \
--content "Context: We need a relational database..." \
--tags "decision,database"
# Capture a how-to
python Tools/NoteCreator.py capture \
--type howto \
--title "Deploy to Kubernetes with Helm" \
--content "Prerequisites: kubectl, helm..." \
--tags "howto,kubernetes,helm"
# Append to today's daily note
python Tools/NoteCreator.py capture \
--type daily \
--content "Learned about event sourcing patterns today"Capture Types
| Type | Template | Destination |
|---|---|---|
concept | Concept template | 04 - Permanent/ |
adr | ADR template | 03 - Resources/decisions/ |
howto | How-To template | 03 - Resources/howtos/ |
meeting | Meeting template | 10 - 1-1/ |
moc | MOC template | 00 - Maps of Content/ |
daily | Append to daily note | 06 - Daily/YYYY/MM/YYYYMMDD.md |
Auto-Detection
The workflow scans conversation for these signals:
| Signal | Capture Type |
|---|---|
| Decision language ("decided", "chose", "went with") | ADR |
| Explanation language ("X is", "works by", "defined as") | Concept |
| Procedural language ("steps to", "how to", "first... then") | How-To |
| Meeting context ("attendees", "agenda", "action items") | Meeting |
Context Files
reference/knowledge-capture.md- All templates and linking strategyreference/vault-organization.md- PARA placement rules
Workflow: Create Base
Create and edit Obsidian .base database view files.
Steps
1. Define purpose - What data to view? (tasks, projects, reading list, etc.) 2. Design filters - Which notes to include (tags, folders, properties) 3. Design formulas - Computed columns needed 4. Choose view type - table, cards, list, or map 5. Generate .base YAML via Tools/BaseBuilder.py create 6. Validate the generated YAML 7. Place file in appropriate folder
Tool Usage
# Create a project tracker base
python Tools/BaseBuilder.py create \
--name "Active Projects" \
--filter 'file.inFolder("01 - Projects") && status != "completed"' \
--view table \
--columns "file.name,status,priority,due" \
--output "01 - Projects/projects.base"
# Create with formulas
python Tools/BaseBuilder.py create \
--name "Task Dashboard" \
--filter 'file.hasTag("task")' \
--view table \
--formula 'days_until_due=if(due, ((date(due) - today()) / 86400000).round(0), "")' \
--formula 'priority_label=if(priority == 1, "High", if(priority == 2, "Medium", "Low"))' \
--columns "file.name,status,formula.priority_label,due,formula.days_until_due"
# Validate existing base file
python Tools/BaseBuilder.py validate \
--path "01 - Projects/projects.base"
# Preview (dry run)
python Tools/BaseBuilder.py preview \
--filter 'file.hasTag("book")' \
--view cards \
--columns "cover,file.name,author,status"Options
| Flag | Description |
|---|---|
--name | View name |
--filter | Filter expression (can use &&, `\ |
--view | table, cards, list, map |
--columns | Comma-separated property list |
--formula | name=expression (repeatable) |
--group-by | Property to group by |
--sort | Sort direction: ASC, DESC |
--limit | Max results |
--summary | property=SummaryType (repeatable) |
--output | Output file path |
Context Files
reference/bases.md- Complete .base schema, filters, formulas, functions
Workflow: Create Note
Create notes with proper frontmatter, PARA placement, and wikilinks.
Steps
1. Determine note type - project, zettelkasten, concept, howto, adr, meeting, moc, or generic 2. Select PARA folder based on type:
- Project ->
01 - Projects/ - Zettelkasten/Concept ->
04 - Permanent/ - How-To/Resource ->
03 - Resources/ - Meeting ->
10 - 1-1/or project folder - MOC ->
00 - Maps of Content/ - Generic ->
00 - Inbox/(for later triage)
3. Apply template from reference/knowledge-capture.md or reference/vault-organization.md 4. Generate frontmatter with created, updated, tags, type 5. Scan for related notes and insert [[wikilinks]] 6. Create file using Tools/NoteCreator.py create
Tool Usage
python Tools/NoteCreator.py create \
--title "Note Title" \
--template concept \
--folder permanent \
--tags "topic,subtopic" \
--content "Optional body content"Options
| Flag | Values | Default |
|---|---|---|
--template | concept, adr, howto, meeting, project, daily, moc, generic | generic |
--folder | inbox, projects, areas, resources, archive, permanent, moc, daily | inbox |
--tags | Comma-separated | From template |
--dry-run | Preview without creating | false |
--vault | Vault path | Auto-detect |
Context Files
reference/vault-organization.md- Folder structure, frontmatter schemasreference/knowledge-capture.md- Templates for each note typereference/markdown.md- Obsidian markdown syntax
Workflow: Daily Note
Create or enhance daily notes at 06 - Daily/YYYY/MM/YYYYMMDD.md.
Steps
Create Daily Note
1. Calculate date - today or specified date 2. Build path - 06 - Daily/YYYY/MM/YYYYMMDD.md 3. Generate frontmatter with daily note schema 4. Add navigation - prev/next links 5. Create file via Tools/NoteCreator.py daily
Enhance Existing Daily Note
1. Read existing note 2. Add structure - headings, task checkboxes 3. Add wikilinks - link mentioned people, projects, concepts 4. Suggest tags based on content 5. Update frontmatter - update updated timestamp
Tool Usage
# Create today's daily note
python Tools/NoteCreator.py daily
# Create for specific date
python Tools/NoteCreator.py daily --date 2026-02-09
# Append content to today's note
python Tools/NoteCreator.py daily --append "Met with team about API redesign"
# Create with initial content
python Tools/NoteCreator.py daily --content "## Focus\n- API redesign\n- Code review"Daily Note Format
---
created: 2026-02-09T09:00
updated: 2026-02-09T09:00
title: "20260209"
type: daily-note
status: true
tags:
- daily
- 2026
- 2026-02
date_formatted: 2026-02-09
---
# Daily Note - 2026-02-09
### Tasks
- [ ] Task 1
### Journal
### Navigation
<< [[20260208]] | **Today** | [[20260210]] >>Context Files
reference/vault-organization.md- Daily note frontmatter schema, naming conventionsreference/markdown.md- Wikilink and callout syntax
Workflow: Manage Vault
Vault health checks, orphan detection, broken link scanning, and lifecycle management.
Steps
1. Determine action - health check, orphan scan, broken links, lifecycle change, or move 2. Execute via Tools/VaultManager.py 3. Report results with actionable suggestions
Tool Usage
# Full health report
python Tools/VaultManager.py health --vault /path/to/vault
# Find orphan notes (no inlinks or outlinks)
python Tools/VaultManager.py orphans --vault /path/to/vault
# Find broken wikilinks
python Tools/VaultManager.py broken-links --vault /path/to/vault
# Change lifecycle state
python Tools/VaultManager.py lifecycle \
--path "01 - Projects/old-project.md" \
--state archived
# Move note between PARA folders
python Tools/VaultManager.py move \
--path "00 - Inbox/unsorted-note.md" \
--to "03 - Resources/"
# List inbox notes for triage
python Tools/VaultManager.py inbox --vault /path/to/vaultCommands
| Command | Description |
|---|---|
health | Count notes by folder, tag stats, find empty notes, frontmatter coverage |
orphans | Notes with no inlinks or outlinks |
broken-links | Wikilinks pointing to non-existent notes |
lifecycle | Change state: active, processed, archived |
move | Relocate note between PARA folders |
inbox | List notes in 00 - Inbox/ for triage |
Health Report Output
Vault Health Report
===================
Total notes: 1,247
By folder:
06 - Daily: 892
03 - Resources: 128
01 - Projects: 45
...
Frontmatter coverage: 89%
Notes without tags: 23
Empty notes: 5
Orphan notes: 31
Broken links: 12Context Files
reference/vault-organization.md- PARA structure, lifecycle states
Workflow: Process Inbox
Triage notes in 00 - Inbox/ into proper PARA folders.
Steps
1. List inbox - Get all notes in 00 - Inbox/ via Tools/VaultManager.py inbox 2. For each note: a. Read content and any existing frontmatter b. Determine note type (project, resource, permanent, etc.) c. Suggest PARA destination folder d. Add/fix frontmatter (created, updated, tags, type) e. Add wikilinks to related notes f. Move to destination folder 3. Report - Summary of actions taken
Tool Usage
# List inbox notes
python Tools/VaultManager.py inbox --vault /path/to/vault
# Move a processed note
python Tools/VaultManager.py move \
--path "00 - Inbox/kubernetes-notes.md" \
--to "03 - Resources/"
# Change lifecycle state after processing
python Tools/VaultManager.py lifecycle \
--path "03 - Resources/kubernetes-notes.md" \
--state processedPARA Destination Rules
| Content Signal | Destination |
|---|---|
| Active work with deadline | 01 - Projects/ |
| Ongoing responsibility | 02 - Areas/ |
| Reference material | 03 - Resources/ |
| Atomic insight/idea | 04 - Permanent/ |
| Completed/no longer relevant | 04 - Archive/ |
| Book notes | 08 - books/ |
Frontmatter Fixes
When processing inbox notes:
- Add
createdif missing (use file creation time) - Add
updatedwith current timestamp - Add
tagsbased on content analysis - Add
typebased on destination - Preserve any existing frontmatter fields
Context Files
reference/vault-organization.md- PARA folders, frontmatter schemasreference/knowledge-capture.md- Templates for different note types
Workflow: Sync Docs
Copy project documentation from external repositories into the vault.
Steps
1. Identify source - project repo path and docs to sync 2. Determine destination - typically 01 - Projects/[project-name]/ or 03 - Resources/ 3. Copy files preserving structure 4. Add frontmatter to each synced file 5. Update lifecycle - mark as active 6. Add tags - project name, source repo 7. Create/update MOC - link synced docs from project MOC
Tool Usage
# Sync docs from a project
python Tools/VaultManager.py move \
--path "/external/project/docs/" \
--to "01 - Projects/my-project/docs/" \
--sync
# Update lifecycle after sync
python Tools/VaultManager.py lifecycle \
--path "01 - Projects/my-project/docs/" \
--state activeSync Strategy
1. Copy, don't move - source files remain in original location 2. Add vault frontmatter - source path, sync date, tags 3. Convert links - external markdown links to wikilinks where possible 4. Preserve original - keep original formatting intact 5. Track sync date - record when last synced in frontmatter
Frontmatter for Synced Notes
---
created: YYYY-MM-DDTHH:mm
updated: YYYY-MM-DDTHH:mm
type: synced-doc
source: /path/to/original/file.md
synced_at: YYYY-MM-DDTHH:mm
tags:
- synced
- project/name
---Context Files
reference/vault-organization.md- Destination folder conventions