| name | obsidian-plugin-dev |
| description | Comprehensive skill for Obsidian plugin development with TypeScript. Covers plugin lifecycle, CodeMirror 6 editor extensions, React/Svelte integration, Vault API patterns, settings with migration pipelines, SecretStorage, CSS theming, CLI debugging workflow, testing, CI/CD, and community plugin submission. Trigger on: create obsidian plugin, obsidian plugin dev, obsidian API, obsidian editor extension, obsidian CM6, obsidian view, obsidian modal, obsidian settings, obsidian command, obsidian manifest, obsidian publish, obsidian submit plugin, obsidian plugin test, obsidian vite config, obsidian react, obsidian theme, obsidian CLI debug.
|
| version | 1.0.0 |
Obsidian Plugin Development
When This Skill Applies
Use this skill when the user is:
- Creating a new Obsidian plugin from scratch
- Implementing plugin features (commands, views, modals, settings, editor extensions)
- Debugging plugin issues or unexpected behavior
- Configuring build tools (Vite, esbuild, rollup)
- Writing tests for Obsidian plugins
- Setting up CI/CD and release workflows
- Preparing a plugin for community submission
- Working with CodeMirror 6 editor extensions
- Integrating React/Svelte/Vue into Obsidian views
Critical Rules (Always Follow)
| # | Rule | Why |
|---|
| 1 | Never use global app ā use this.app | Global app breaks in multi-window; submission rejected |
| 2 | Never use innerHTML/outerHTML ā use createEl(), createDiv(), setText() | XSS vulnerability; instant rejection |
| 3 | Use registerEvent() for all event subscriptions | Auto-cleanup on unload; prevents memory leaks |
| 4 | No default hotkeys ā let users configure | Hotkey conflicts with other plugins |
| 5 | Use requestUrl() over fetch() | Bypasses CORS; works on mobile |
| 6 | Use normalizePath() for user-provided paths | Handles cross-platform path differences |
| 7 | Prefer vault.process() over vault.modify() | Atomic operation; safe with concurrent edits |
| 8 | Use FileManager.processFrontMatter() for YAML | Never parse/serialize YAML manually |
| 9 | Use Sentence case for all UI text | Obsidian convention; submission requirement |
| 10 | Use setHeading() not <h1>/<h2> | Semantic; supports RTL; submission requirement |
| 11 | Import only what you use ā no unused classes | Cleaner code; easier audits; submission reviewers check this |
| 12 | Use checkCallback when command depends on context | callback = always available; checkCallback = conditionally shown; editorCallback = needs editor |
| 13 | Always provide .theme-dark / .theme-light CSS variants | Obsidian CSS vars auto-adapt, but explicit theme blocks ensure edge cases render correctly; submission reviewers check this |
| 14 | No regex lookbehind ā (?!...) OK, (?<=...) NOT OK | Breaks on iOS Safari < 16.4; submission rejected |
| 15 | All interactive elements keyboard accessible | Tab navigation + Enter/Space; submission requirement |
| 16 | ARIA labels on all icon-only buttons | Screen reader support; submission requirement |
| 17 | Touch targets ā„ 44Ć44px | Mobile usability; submission requirement |
| 18 | Use vault.configDir not .obsidian | Cross-platform compatibility; submission requirement |
| 19 | Use fileManager.trashFile() not vault.delete() | Respects user trash settings |
| 20 | Use AbstractInputSuggest not TextInputSuggest | Built-in API; Liam's copy-pasted implementation is banned |
| 21 | Create versions.json ā maps plugin version ā min Obsidian version | Submission bot checks for it; auto-reject if missing |
| 22 | Version your settings schema ā _settingsVersion field | Enables migration pipeline on upgrade; prevents data loss |
Quick Reference
Plugin Lifecycle
import { Plugin } from 'obsidian'
export default class MyPlugin extends Plugin {
async onload() {
await this.loadSettings()
this.addSettingTab(new MySettingTab(this.app, this))
this.addCommand({ id: 'my-command', name: 'My command', callback: () => {} })
this.registerView(MY_VIEW_TYPE, (leaf) => new MyView(leaf))
this.registerEditorExtension(myExtension)
this.registerEvent(this.app.vault.on('modify', (file) => {}))
this.registerDomEvent(document, 'click', (evt) => {})
this.registerInterval(window.setInterval(() => {}, 1000))
}
async onunload() {
}
}
Essential API Cheatsheet
| Need | API |
|---|
| Get active file | this.app.workspace.getActiveFile() |
| Read file | this.app.vault.cachedRead(file) |
| Modify file (background) | this.app.vault.process(file, (data) => data) |
| Modify file (editor) | editor.replaceSelection(), editor.getRange() |
| Create file | this.app.vault.create(path, content) |
| Delete file | this.app.fileManager.trashFile(file) |
| Rename file | this.app.fileManager.renameFile(file, newPath) |
| Read frontmatter | this.app.metadataCache.getFileCache(file)?.frontmatter |
| Write frontmatter | this.app.fileManager.processFrontMatter(file, (fm) => {}) |
| Show notification | new Notice('message', duration) |
| Open modal | new MyModal(this.app).open() |
| Get active editor | this.app.workspace.activeEditor?.editor |
| Platform check | Platform.isMacOS, Platform.isMobile, Platform.isDesktop |
| Network request | requestUrl({ url, method, headers, body }) |
| Persist data | this.loadData() / this.saveData(data) |
| Secure storage | this.app.secretStorage.setSecret(id, value) (v1.11.4+) |
| Detect theme | document.body.classList.contains('theme-dark') |
Command Callback Decision Tree
Does the command need an active editor?
āā YES ā editorCallback
ā (automatically hidden when no editor; gives you editor + view)
ā
āā NO ā Does it need any context to run? (active file, leaf, etc.)
āā YES ā checkCallback
ā (return true when available; run action on !checking)
ā
āā NO ā callback
(always visible, always runs)
Examples:
this.addCommand({
id: 'open-settings',
name: 'Open plugin settings',
callback: () => { this.openSettings() }
})
this.addCommand({
id: 'copy-stats',
name: 'Copy note statistics',
checkCallback: (checking) => {
const file = this.app.workspace.getActiveFile()
if (file) {
if (!checking) this.copyStats(file)
return true
}
return false
}
})
this.addCommand({
id: 'wrap-callout',
name: 'Wrap selection in callout',
editorCallback: (editor) => {
const sel = editor.getSelection()
editor.replaceSelection(`> [!note]\n> ${sel}`)
}
})
Import Hygiene
Only import what you actually use. Submission reviewers flag unused imports.
import { MarkdownView, Notice, Plugin, PluginSettingTab, Setting } from 'obsidian'
import { App, Editor, Modal, Notice, Plugin, PluginSettingTab, Setting } from 'obsidian'
Common Pitfalls
- Storing view references ā use
getLeavesOfType() on demand
- Passing plugin as Component ā use
this.addChild() instead
- Detaching leaves in onunload ā they reinitialize on update
- Not removing sample code ā
MyPlugin, SampleSettingTab must be renamed
- Using
vault.modify() on active file ā use Editor API instead
- Manual YAML parsing ā use
processFrontMatter() instead
fetch() for API calls ā use requestUrl() instead
- Hardcoded colors in CSS ā use
var(--text-normal), etc.
navigator.platform ā use Platform.isMacOS instead
var declarations ā use const/let instead
- Promise chains ā use
async/await instead
console.log in production ā remove or use console.debug with conditional
- Regex lookbehind
(?<=...) ā breaks on iOS Safari < 16.4; use alternative patterns
Object.assign(defaults, saved) ā mutates defaults; use Object.assign({}, defaults, saved)
- Hardcoded
.obsidian path ā use this.app.vault.configDir instead
- Shallow merge for nested settings ā use deep merge; shallow merge loses nested defaults
vault.delete() for removing files ā use fileManager.trashFile() to respect user settings
- Liam's
TextInputSuggest ā use built-in AbstractInputSuggest instead
- Missing
styles.css ā create empty file if no styles (submission bot checks for it)
- Missing
versions.json ā create with { "1.0.0": "1.0.0" } (submission bot checks for it)
- No settings version tracking ā add
_settingsVersion to settings interface for migration support
Detailed References
| Topic | File | When to Load |
|---|
| Lifecycle & Core API | reference/lifecycle.md | Always; building any plugin feature |
| ESLint Rules (28 rules) | reference/eslint-rules.md | ESLint setup, pre-submission audit, rule reference |
| Accessibility (MANDATORY) | reference/accessibility.md | Keyboard nav, ARIA labels, focus indicators, touch targets |
| CodeMirror 6 Editor Extensions | reference/editor-extensions.md | Editor decorations, syntax highlighting, live preview |
| React / Svelte / Vue Integration | reference/frameworks.md | Using React/Vue/Svelte in views or settings |
| Vault & File Operations | reference/vault-operations.md | File CRUD, frontmatter, events, caching |
| Settings & Data Migration | reference/settings-migration.md | Settings UI, load/save, deep merge, migration pipelines |
| Security & SecretStorage | reference/security.md | API keys, credentials, XSS prevention, network requests |
| CSS Styling | reference/css-accessibility.md | Theming, CSS variables, scoping, mobile styles |
| Dev Workflow & CLI | reference/dev-workflow.md | Build, hot-reload, CLI debugging, Obsidian CLI, ESLint config |
| Testing | reference/testing.md | Unit tests, mocking Obsidian API, Jest/Vitest |
| CI/CD & Release | reference/cicd-release.md | GitHub Actions, version bump, community submission |
Development Workflow
Quick Dev Loop (with Obsidian CLI)
npm run build && obsidian plugin:reload id=<plugin-id>
obsidian dev:errors
obsidian dev:dom selector=".my-plugin-view"
obsidian dev:screenshot
obsidian eval code="app.plugins.plugins"
Without Obsidian CLI
npm run build && cp main.js manifest.json styles.css /path/to/TestVault/.obsidian/plugins/<plugin-id>/
Pre-Submission Checklist
Before creating a release or submitting to community plugins, verify:
Submission Validation (Bot checks ā will auto-reject if incorrect)
Code Quality
Accessibility (MANDATORY)
ESLint & Release
Reference Source Tracking
| Reference File | Primary Sources | Last Verified |
|---|
lifecycle.md | obsidian API docs, gapmiss/obsidian-plugin-skill | 2026-03 |
eslint-rules.md | obsidianmd/eslint-plugin v0.1.9, gapmiss/obsidian-plugin-skill | 2026-03 |
accessibility.md | gapmiss/obsidian-plugin-skill, obsidian plugin guidelines | 2026-03 |
editor-extensions.md | CM6 docs, @codemirror/view source | 2026-03 |
frameworks.md | Leonezz/obsidian-plugin-dev-skill, React docs | 2026-03 |
vault-operations.md | obsidian API docs, official plugin guidelines | 2026-03 |
settings-migration.md | Leonezz/obsidian-plugin-dev-skill | 2026-03 |
security.md | gapmiss/obsidian-plugin-skill, obsidian developer policies | 2026-03 |
css-accessibility.md | davidvkimball/obsidian-dev-skills, obsidian sample theme | 2026-03 |
dev-workflow.md | adriangrantdotorg/Obsidian-Skills, obsidian CLI docs | 2026-03 |
testing.md | Leonezz/obsidian-plugin-dev-skill | 2026-03 |
cicd-release.md | Leonezz/obsidian-plugin-dev-skill, obsidian submission docs | 2026-03 |
To update references: check each source for new content, cross-reference with obsidian developer docs changelog.
Design Decisions
- SKILL.md stays under 500 lines ā quick reference + links to detailed docs
- Reference files are topic-based ā load only what you need
- Code examples are real ā from actual plugin patterns, not toy demos
- Do/Don't tables ā clear before/after comparisons