| name | obsidian-plugin-dev |
| description | Comprehensive patterns and practices for building Obsidian community plugins with TypeScript — lifecycle, React integration, vault operations, settings migration, secure storage, testing, CI/CD, and release workflows. |
| version | 1.0.0 |
| tags | ["obsidian","plugin","typescript","react","electron"] |
Obsidian Plugin Development
Patterns and practices for building Obsidian community plugins with TypeScript, covering the full development lifecycle from plugin initialization to community release.
When This Skill Applies
Use this skill when:
- Creating a new Obsidian plugin
- Adding features to an existing Obsidian plugin
- Integrating React into an Obsidian plugin
- Working with Obsidian vault events, file operations, or settings
- Setting up CI/CD and release workflows for Obsidian plugins
- Debugging memory leaks, theme issues, or settings migration problems
Quick Reference
Plugin Lifecycle
class MyPlugin extends Plugin {
async onload() {
await this.loadSettings()
this.addRibbonIcon(...)
this.addSettingTab(...)
this.registerView(VIEW_TYPE, factory)
this.setupObservers()
}
onunload() {
this.observer.disconnect()
}
}
Settings must load before any UI. All observers must disconnect in onunload(). Use registerEvent(), registerDomEvent(), registerInterval() for automatic cleanup.
Essential API Cheatsheet
| Need | Use |
|---|
| Read file content | vault.cachedRead(file) |
| File metadata | metadataCache.getFileCache(file) |
| All markdown files | vault.getMarkdownFiles() |
| Modify file atomically | vault.process(file, (content) => newContent) |
| Modify frontmatter | fileManager.processFrontMatter(file, (fm) => { ... }) |
| Delete file safely | fileManager.trashFile(file) |
| User feedback | new Notice("message") |
| Open file | workspace.openLinkText(path, sourcePath, newLeaf) |
| Get active file | workspace.getActiveFile() |
| Theme detection | MutationObserver on document.body class theme-dark |
| Debounce | import { debounce } from "obsidian" |
| OS detection | import { Platform } from "obsidian" (not navigator) |
| HTTP requests | import { requestUrl } from "obsidian" (not fetch()) |
| Normalize paths | import { normalizePath } from "obsidian" |
| Config directory | vault.configDir (not hardcoded .obsidian) |
Common Pitfalls
- Shallow settings merge loses nested defaults — always deep merge.
Object.assign with settings doesn't handle new nested fields on upgrade.
- Not cleaning up vault event refs, observers, or React roots → memory leaks.
- Importing UI components in tests → jsdom
MessageChannel error. Isolate pure logic.
- API keys in
data.json are plaintext — use SecretStorage when available.
containerEl.children[1] is the correct React mount point, not containerEl.
- Vault events fire rapidly — always debounce reload logic.
- Migration order matters — run shape migrations before deep merge, not after.
innerHTML / outerHTML — use Obsidian DOM helpers (createDiv, createEl) for XSS safety.
- Regex lookbehind — avoid, breaks on iOS < 16.4.
- Global
app object — always use this.app from your plugin instance.
- Hardcoded
.obsidian — use vault.configDir.
Detailed Reference
For in-depth guidance on specific topics, see:
- Plugin Lifecycle & React Integration — onload/onunload ordering, React mount/unmount, error boundaries, PluginSettingTab pattern
- Vault Events & File Operations — event subscriptions, debouncing, file caching, markdown parsing/serialization
- Settings & Migration — deep merge, migration pipelines, version tracking, two-layer architecture
- SecretStorage — feature detection, extract/resolve pattern, one-time migration
- Theme & Event Bus — MutationObserver theme detection, typed pub/sub event bus
- Rendering Markdown in Views — link parsing, wikilinks, segment-based rendering
- Build & Testing — Vite CJS setup, mocking Obsidian API, jsdom pitfalls
- CI/CD & Release — GitHub Actions, version bumping, community submission
- Beta Releases with BRAT — BRAT integration, beta workflows, version matching
Do's and Don'ts
Do
- Use
registerEvent(), registerDomEvent(), registerInterval() for automatic cleanup
- Use
instanceof for type checking (file instanceof TFile), not type casting
- Use
vault.process() for background file modifications (atomic)
- Use
requestUrl() instead of fetch() (bypasses CORS)
- Use
normalizePath() for user-provided paths (cross-platform)
- Use
Platform API for OS detection, not navigator
- Use Obsidian CSS variables for all styling (respects themes)
- Use sentence case in all UI text ("Advanced settings" not "Advanced Settings")
- Deep merge settings with defaults on load
- Debounce vault event handlers
- Unmount React roots on view close
Don't
- Don't store view references in plugin properties (memory leak)
- Don't use global
app object (use this.app)
- Don't use
Vault.modify() for active file edits (use Editor API)
- Don't hardcode
.obsidian path (use vault.configDir)
- Don't use
innerHTML/outerHTML (XSS risk, use DOM helpers)
- Don't use regex lookbehind (iOS < 16.4 incompatibility)
- Don't set default hotkeys (causes conflicts)
- Don't include "command" in command names/IDs (redundant)
- Don't use
Object.assign for nested settings (use deep merge)
- Don't skip
offref() cleanup for vault events