obsidian-sdk-patterns
Implement idiomatic, robust patterns for settings migration, event cleanup, and vault operations in Obsidian plugins.
Install
mkdir -p .claude/skills/obsidian-sdk-patterns && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/1597" && unzip -o skill.zip -d .claude/skills/obsidian-sdk-patterns && rm skill.zipInstalls to .claude/skills/obsidian-sdk-patterns
Activation
This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.
Production-ready Obsidian plugin patterns: typed settings with migration,Key capabilities
- →Implement typed settings with versioned migration to handle field changes between releases.
- →Perform safe vault operations (read, write, append, delete) that check for file existence.
- →Manage events with automatic cleanup using `this.registerEvent()` to prevent memory leaks.
- →Utilize the metadata cache for efficient file property access.
- →Debounce file modification handlers to prevent UI jank from rapid changes.
- →Ensure parent folders exist before creating new files.
How it works
This skill provides copy-pasteable code patterns for Obsidian plugin development, addressing common issues like lost settings, null-reference crashes, and memory leaks. It includes solutions for typed settings, safe vault operations, and event management.
Inputs & outputs
When to use obsidian-sdk-patterns
- →Implementing versioned settings migration
- →Preventing memory leaks in plugins
- →Writing safe vault file handlers
- →Standardizing plugin coding patterns
About this skill
Obsidian SDK Patterns
Overview
Six production patterns that prevent the most common Obsidian plugin bugs: lost settings on upgrade, null-reference crashes on deleted files, memory leaks from unregistered events, stale metadata, and UI jank from rapid file changes. Each pattern is self-contained and copy-pasteable.
Prerequisites
- A working Obsidian plugin (see
obsidian-core-workflow-a) - TypeScript strict mode enabled (
"strictNullChecks": truein tsconfig) - Familiarity with
Plugin.onload()/onunload()lifecycle
Instructions
Step 1: Typed settings with versioned migration
Settings break when you add or rename fields between releases. Version the settings object and migrate on load so existing users keep their data.
// src/settings.ts
interface PluginSettingsV1 {
apiKey: string;
interval: number;
}
interface PluginSettingsV2 {
version: 2;
apiKey: string;
syncInterval: number; // renamed from "interval"
excludedFolders: string[]; // new field
theme: "default" | "minimal";
}
// Current version is always the latest
type PluginSettings = PluginSettingsV2;
const DEFAULTS: PluginSettings = {
version: 2,
apiKey: "",
syncInterval: 300,
excludedFolders: [],
theme: "default",
};
export async function loadSettings(plugin: Plugin): Promise<PluginSettings> {
const raw = (await plugin.loadData()) as any;
if (!raw) return { ...DEFAULTS };
// Migrate v1 -> v2
if (!raw.version || raw.version < 2) {
raw.version = 2;
if (raw.interval !== undefined) {
raw.syncInterval = raw.interval;
delete raw.interval;
}
raw.excludedFolders = raw.excludedFolders ?? [];
raw.theme = raw.theme ?? "default";
await plugin.saveData(raw);
}
// Merge with defaults to pick up any newly added fields
return { ...DEFAULTS, ...raw };
}
Why: Object.assign({}, DEFAULTS, raw) handles new fields added in patch
releases. The explicit migration block handles renames and type changes between
major versions.
Step 2: Safe vault operations (check-before-act)
The Vault API throws if you create a file that exists or read one that was deleted between your check and your call. Wrap every operation.
// src/vault-helpers.ts
import { App, TFile, TFolder, TAbstractFile, normalizePath } from "obsidian";
export class VaultHelper {
constructor(private app: App) {}
/** Read file content, return null if file doesn't exist */
async safeRead(path: string): Promise<string | null> {
const file = this.app.vault.getAbstractFileByPath(normalizePath(path));
if (!(file instanceof TFile)) return null;
return this.app.vault.read(file);
}
/** Create or overwrite a file. Creates parent folders as needed. */
async safeWrite(path: string, content: string): Promise<TFile> {
const normalized = normalizePath(path);
await this.ensureParentFolder(normalized);
const existing = this.app.vault.getAbstractFileByPath(normalized);
if (existing instanceof TFile) {
await this.app.vault.modify(existing, content);
return existing;
}
return this.app.vault.create(normalized, content);
}
/** Append content to a file. Creates the file if it doesn't exist. */
async safeAppend(path: string, content: string): Promise<void> {
const normalized = normalizePath(path);
const existing = this.app.vault.getAbstractFileByPath(normalized);
if (existing instanceof TFile) {
const current = await this.app.vault.read(existing);
await this.app.vault.modify(existing, current + content);
} else {
await this.ensureParentFolder(normalized);
await this.app.vault.create(normalized, content);
}
}
/** Delete a file if it exists, moving to trash by default. */
async safeDelete(path: string, useTrash = true): Promise<boolean> {
const file = this.app.vault.getAbstractFileByPath(normalizePath(path));
if (!(file instanceof TFile)) return false;
if (useTrash) {
await this.app.vault.trash(file, false);
} else {
await this.app.vault.delete(file);
}
return true;
}
/** Ensure a folder (and all parents) exist. */
private async ensureParentFolder(filePath: string): Promise<void> {
const parts = filePath.split("/");
parts.pop(); // remove filename
let current = "";
for (const part of parts) {
current = current ? `${current}/${part}` : part;
const existing = this.app.vault.getAbstractFileByPath(current);
if (!existing) {
await this.app.vault.createFolder(current);
}
}
}
}
Step 3: Event management with automatic cleanup
Every this.registerEvent(...) call in onload() is automatically cleaned up
when the plugin unloads. Never use raw addEventListener or app.vault.on()
without registering -- those leak.
export default class MyPlugin extends Plugin {
async onload() {
// File events -- auto-cleaned on unload
this.registerEvent(
this.app.vault.on("create", (file) => {
if (file instanceof TFile) this.onFileCreated(file);
})
);
this.registerEvent(
this.app.vault.on("delete", (file) => {
if (file instanceof TFile) this.onFileDeleted(file);
})
);
this.registerEvent(
this.app.vault.on("rename", (file, oldPath) => {
if (file instanceof TFile) this.onFileRenamed(file, oldPath);
})
);
// Workspace events
this.registerEvent(
this.app.workspace.on("active-leaf-change", (leaf) => {
this.onActiveLeafChange(leaf);
})
);
this.registerEvent(
this.app.workspace.on("layout-change", () => {
this.onLayoutChange();
})
);
// Periodic tasks -- also auto-cleaned
this.registerInterval(
window.setInterval(() => this.periodicSync(), 60_000)
);
// DOM events -- use registerDomEvent for auto-cleanup
this.registerDomEvent(document, "keydown", (evt: KeyboardEvent) => {
if (evt.key === "F5") this.refreshData();
});
}
// No cleanup code needed in onunload() -- all registered events
// are automatically removed by the Plugin base class.
}
Anti-pattern to avoid:
// BAD: leaks on plugin unload
this.app.vault.on("modify", handler);
document.addEventListener("click", handler);
// GOOD: auto-cleaned
this.registerEvent(this.app.vault.on("modify", handler));
this.registerDomEvent(document, "click", handler);
Step 4: Workspace layout manipulation
Open files in specific panes, split views, and restore layout state.
import { MarkdownView, WorkspaceLeaf } from "obsidian";
export class WorkspaceHelper {
constructor(private app: App) {}
/** Open a file in a new tab */
async openInNewTab(path: string): Promise<void> {
const file = this.app.vault.getAbstractFileByPath(path);
if (!(file instanceof TFile)) return;
const leaf = this.app.workspace.getLeaf("tab");
await leaf.openFile(file);
}
/** Open a file in a vertical split to the right */
async openInSplit(path: string): Promise<void> {
const file = this.app.vault.getAbstractFileByPath(path);
if (!(file instanceof TFile)) return;
const leaf = this.app.workspace.getLeaf("split", "vertical");
await leaf.openFile(file);
}
/** Get the currently active markdown file (or null) */
getActiveFile(): TFile | null {
const view = this.app.workspace.getActiveViewOfType(MarkdownView);
return view?.file ?? null;
}
/** Iterate all open markdown leaves */
forEachOpenNote(callback: (file: TFile, leaf: WorkspaceLeaf) => void): void {
this.app.workspace.iterateAllLeaves((leaf) => {
if (leaf.view instanceof MarkdownView && leaf.view.file) {
callback(leaf.view.file, leaf);
}
});
}
/** Pin/unpin the active tab */
togglePin(): void {
const leaf = this.app.workspace.getLeaf();
if (leaf) {
const pinned = (leaf as any).pinned;
(leaf as any).setPinned(!pinned);
}
}
}
Step 5: Metadata cache for fast queries
metadataCache is Obsidian's pre-parsed index of all vault files. It avoids
reading file content for frontmatter, tags, links, and headings.
import { App, TFile, CachedMetadata } from "obsidian";
export class MetadataHelper {
constructor(private app: App) {}
/** Get parsed metadata for a file (frontmatter, tags, links, headings) */
getCache(file: TFile): CachedMetadata | null {
return this.app.metadataCache.getFileCache(file);
}
/** Get frontmatter value, returns undefined if missing */
getFrontmatterValue(file: TFile, key: string): any | undefined {
const cache = this.getCache(file);
return cache?.frontmatter?.[key];
}
/** Find all files with a specific tag */
filesWithTag(tag: string): TFile[] {
const normalized = tag.startsWith("#") ? tag : `#${tag}`;
return this.app.vault.getMarkdownFiles().filter((file) => {
const cache = this.getCache(file);
// Tags in body
const bodyTags = cache?.tags?.map((t) => t.tag) ?? [];
// Tags in frontmatter
const fmTags = (cache?.frontmatter?.tags ?? []).map((t: string) =>
t.startsWith("#") ? t : `#${t}`
);
return [...bodyTags, ...fmTags].includes(normalized);
});
}
/** Get all outgoing links from a file */
outgoingLinks(file: TFile): string[] {
const cache = this.getCache(file);
const links = cache?.links?.map((l) => l.link) ?? [];
const embeds = cache?.embeds?.map((e) => e.link) ?? [];
return [...new Set([...links, ...embeds])];
}
/** Get files that link to this file (backlinks) */
backlinks(file: TFile): TFile[] {
const resolved = this.app.metadataCache.resolvedLinks;
const results: TFile[] = [];
for (const [sourcePath, targets] of Object.entries(resolved)) {
if (file.path in targets) {
const source = this.app.vault.getAbstractFileByPath(sourcePath);
if (source instanceof TFile) results.push(s
---
*Content truncated.*
Prerequisites
How it compares
This skill offers specific, reusable code patterns for common Obsidian plugin development challenges, providing concrete solutions beyond general best practices.
Compared to similar skills
obsidian-sdk-patterns side by side with the closest alternatives in the catalog.
| Skill | Installs | Updated | Safety | Difficulty |
|---|---|---|---|---|
| obsidian-sdk-patterns (this skill) | 3 | 27d | No flags | Advanced |
| typescript-review | 39 | 2mo | No flags | Intermediate |
| typescript | 28 | 2mo | No flags | Beginner |
| react-patterns | 9 | 6mo | No flags | Intermediate |
Try saying
Example prompts that trigger this skill in your AI assistant.
More by jeremylongshore
View all by jeremylongshore →You might also like
typescript-review
metabase
Review TypeScript and JavaScript code changes for compliance with Metabase coding standards, style violations, and code quality issues. Use when reviewing pull requests or diffs containing TypeScript/JavaScript code.
typescript
lobehub
TypeScript code style and optimization guidelines. Use when writing TypeScript code (.ts, .tsx, .mts files), reviewing code quality, or implementing type-safe patterns. Triggers on TypeScript development, type safety questions, or code style discussions.
react-patterns
davila7
Modern React patterns and principles. Hooks, composition, performance, TypeScript best practices.
antfu
antfu
Anthony Fu's opinionated tooling and conventions for JavaScript/TypeScript projects. Use when setting up new projects, configuring ESLint/Prettier alternatives, monorepos, library publishing, or when the user mentions Anthony Fu's preferences.
ast-grep-find
parcadei
AST-based code search and refactoring via ast-grep MCP
typescript-skills
llama-farm
Shared TypeScript best practices for Designer and Electron subsystems.