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() {
// 1. Load settings FIRST
await this.loadSettings();
// 2. Add settings tab
this.addSettingTab(new MySettingTab(this.app, this));
// 3. Register commands
this.addCommand({ id: 'my-command', name: 'My command', callback: () => {} });
// 4. Register views
this.registerView(MY_VIEW_TYPE, leaf => new MyView(leaf));
// 5. Register editor extensions
this.registerEditorExtension(myExtension);
// 6. Register events
this.registerEvent(this.app.vault.on('modify', file => {}));
this.registerDomEvent(document, 'click', evt => {});
this.registerInterval(window.setInterval(() => {}, 1000));
}
async onunload() {
// Resources registered with register*() are auto-cleaned
// Manual cleanup needed for: MutationObserver, React root, vault.on() in React
}
}
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:
// Always available — no conditions
this.addCommand({
id: 'open-settings',
name: 'Open plugin settings',
callback: () => {
this.openSettings();
},
});
// Needs active file — use checkCallback
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;
},
});
// Needs editor — use editorCallback
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.
// Good — only what's needed
import { MarkdownView, Notice, Plugin, PluginSettingTab, Setting } from 'obsidian';
// Bad — unused imports
import { App, Editor, Modal, Notice, Plugin, PluginSettingTab, Setting } from 'obsidian';
// ^^^ ^^^^^^ ^^^^^ — never used
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)
# Build and hot-reload
npm run build && obsidian plugin:reload id=<plugin-id>
# Check for errors
obsidian dev:errors
# Inspect DOM
obsidian dev:dom selector=".my-plugin-view"
# Take screenshot
obsidian dev:screenshot
# Evaluate JS in Obsidian context
obsidian eval code="app.plugins.plugins"
Without Obsidian CLI
# Build and copy to test vault
npm run build && cp main.js manifest.json styles.css /path/to/TestVault/.obsidian/plugins/<plugin-id>/
# Then reload in Obsidian: Ctrl+P → "Reload app without saving"
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
1---2name: obsidian-plugin-dev-33description: 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.4---5# Obsidian Plugin Development67## When This Skill Applies89Use this skill when the user is:1011- Creating a new Obsidian plugin from scratch12- Implementing plugin features (commands, views, modals, settings, editor extensions)13- Debugging plugin issues or unexpected behavior14- Configuring build tools (Vite, esbuild, rollup)15- Writing tests for Obsidian plugins16- Setting up CI/CD and release workflows17- Preparing a plugin for community submission18- Working with CodeMirror 6 editor extensions19- Integrating React/Svelte/Vue into Obsidian views2021## Critical Rules (Always Follow)2223| # | Rule | Why |24| --- | ------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------- |25| 1 | **Never use global `app`** — use `this.app` | Global `app` breaks in multi-window; submission rejected |26| 2 | **Never use `innerHTML`/`outerHTML`** — use `createEl()`, `createDiv()`, `setText()` | XSS vulnerability; instant rejection |27| 3 | **Use `registerEvent()`** for all event subscriptions | Auto-cleanup on unload; prevents memory leaks |28| 4 | **No default hotkeys** — let users configure | Hotkey conflicts with other plugins |29| 5 | **Use `requestUrl()` over `fetch()`** | Bypasses CORS; works on mobile |30| 6 | **Use `normalizePath()`** for user-provided paths | Handles cross-platform path differences |31| 7 | **Prefer `vault.process()`** over `vault.modify()` | Atomic operation; safe with concurrent edits |32| 8 | **Use `FileManager.processFrontMatter()`** for YAML | Never parse/serialize YAML manually |33| 9 | **Use Sentence case** for all UI text | Obsidian convention; submission requirement |34| 10 | **Use `setHeading()`** not `<h1>`/`<h2>` | Semantic; supports RTL; submission requirement |35| 11 | **Import only what you use** — no unused classes | Cleaner code; easier audits; submission reviewers check this |36| 12 | **Use `checkCallback` when command depends on context** | `callback` = always available; `checkCallback` = conditionally shown; `editorCallback` = needs editor |37| 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 |38| 14 | **No regex lookbehind** — `(?!...)` OK, `(?<=...)` NOT OK | Breaks on iOS Safari < 16.4; submission rejected |39| 15 | **All interactive elements keyboard accessible** | Tab navigation + Enter/Space; submission requirement |40| 16 | **ARIA labels on all icon-only buttons** | Screen reader support; submission requirement |41| 17 | **Touch targets ≥ 44×44px** | Mobile usability; submission requirement |42| 18 | **Use `vault.configDir` not `.obsidian`** | Cross-platform compatibility; submission requirement |43| 19 | **Use `fileManager.trashFile()` not `vault.delete()`** | Respects user trash settings |44| 20 | **Use `AbstractInputSuggest` not `TextInputSuggest`** | Built-in API; Liam's copy-pasted implementation is banned |45| 21 | **Create `versions.json`** — maps plugin version → min Obsidian version | Submission bot checks for it; auto-reject if missing |46| 22 | **Version your settings schema** — `_settingsVersion` field | Enables migration pipeline on upgrade; prevents data loss |4748## Quick Reference4950### Plugin Lifecycle5152```typescript53import { Plugin } from 'obsidian';5455export default class MyPlugin extends Plugin {56 async onload() {57 // 1. Load settings FIRST58 await this.loadSettings();59 // 2. Add settings tab60 this.addSettingTab(new MySettingTab(this.app, this));61 // 3. Register commands62 this.addCommand({ id: 'my-command', name: 'My command', callback: () => {} });63 // 4. Register views64 this.registerView(MY_VIEW_TYPE, leaf => new MyView(leaf));65 // 5. Register editor extensions66 this.registerEditorExtension(myExtension);67 // 6. Register events68 this.registerEvent(this.app.vault.on('modify', file => {}));69 this.registerDomEvent(document, 'click', evt => {});70 this.registerInterval(window.setInterval(() => {}, 1000));71 }7273 async onunload() {74 // Resources registered with register*() are auto-cleaned75 // Manual cleanup needed for: MutationObserver, React root, vault.on() in React76 }77}78```7980### Essential API Cheatsheet8182| Need | API |83| ------------------------ | ------------------------------------------------------------- |84| Get active file | `this.app.workspace.getActiveFile()` |85| Read file | `this.app.vault.cachedRead(file)` |86| Modify file (background) | `this.app.vault.process(file, (data) => data)` |87| Modify file (editor) | `editor.replaceSelection()`, `editor.getRange()` |88| Create file | `this.app.vault.create(path, content)` |89| Delete file | `this.app.fileManager.trashFile(file)` |90| Rename file | `this.app.fileManager.renameFile(file, newPath)` |91| Read frontmatter | `this.app.metadataCache.getFileCache(file)?.frontmatter` |92| Write frontmatter | `this.app.fileManager.processFrontMatter(file, (fm) => {})` |93| Show notification | `new Notice('message', duration)` |94| Open modal | `new MyModal(this.app).open()` |95| Get active editor | `this.app.workspace.activeEditor?.editor` |96| Platform check | `Platform.isMacOS`, `Platform.isMobile`, `Platform.isDesktop` |97| Network request | `requestUrl({ url, method, headers, body })` |98| Persist data | `this.loadData()` / `this.saveData(data)` |99| Secure storage | `this.app.secretStorage.setSecret(id, value)` (v1.11.4+) |100| Detect theme | `document.body.classList.contains('theme-dark')` |101102### Command Callback Decision Tree103104```105Does the command need an active editor?106├─ YES → editorCallback107│ (automatically hidden when no editor; gives you editor + view)108│109└─ NO → Does it need any context to run? (active file, leaf, etc.)110 ├─ YES → checkCallback111 │ (return true when available; run action on !checking)112 │113 └─ NO → callback114 (always visible, always runs)115```116117**Examples:**118119```typescript120// Always available — no conditions121this.addCommand({122 id: 'open-settings',123 name: 'Open plugin settings',124 callback: () => {125 this.openSettings();126 },127});128129// Needs active file — use checkCallback130this.addCommand({131 id: 'copy-stats',132 name: 'Copy note statistics',133 checkCallback: checking => {134 const file = this.app.workspace.getActiveFile();135 if (file) {136 if (!checking) this.copyStats(file);137 return true;138 }139 return false;140 },141});142143// Needs editor — use editorCallback144this.addCommand({145 id: 'wrap-callout',146 name: 'Wrap selection in callout',147 editorCallback: editor => {148 const sel = editor.getSelection();149 editor.replaceSelection(`> [!note]\n> ${sel}`);150 },151});152```153154### Import Hygiene155156Only import what you actually use. Submission reviewers flag unused imports.157158```typescript159// Good — only what's needed160import { MarkdownView, Notice, Plugin, PluginSettingTab, Setting } from 'obsidian';161162// Bad — unused imports163import { App, Editor, Modal, Notice, Plugin, PluginSettingTab, Setting } from 'obsidian';164// ^^^ ^^^^^^ ^^^^^ — never used165```166167## Common Pitfalls1681691. **Storing view references** → use `getLeavesOfType()` on demand1702. **Passing plugin as Component** → use `this.addChild()` instead1713. **Detaching leaves in onunload** → they reinitialize on update1724. **Not removing sample code** → `MyPlugin`, `SampleSettingTab` must be renamed1735. **Using `vault.modify()` on active file** → use Editor API instead1746. **Manual YAML parsing** → use `processFrontMatter()` instead1757. **`fetch()` for API calls** → use `requestUrl()` instead1768. **Hardcoded colors in CSS** → use `var(--text-normal)`, etc.1779. **`navigator.platform`** → use `Platform.isMacOS` instead17810. **`var` declarations** → use `const`/`let` instead17911. **Promise chains** → use `async/await` instead18012. **`console.log` in production** → remove or use `console.debug` with conditional18113. **Regex lookbehind `(?<=...)`** → breaks on iOS Safari < 16.4; use alternative patterns18214. **`Object.assign(defaults, saved)`** → mutates defaults; use `Object.assign({}, defaults, saved)`18315. **Hardcoded `.obsidian` path** → use `this.app.vault.configDir` instead18416. **Shallow merge for nested settings** → use deep merge; shallow merge loses nested defaults18517. **`vault.delete()` for removing files** → use `fileManager.trashFile()` to respect user settings18618. **Liam's `TextInputSuggest`** → use built-in `AbstractInputSuggest` instead18719. **Missing `styles.css`** → create empty file if no styles (submission bot checks for it)18820. **Missing `versions.json`** → create with `{ "1.0.0": "1.0.0" }` (submission bot checks for it)18921. **No settings version tracking** → add `_settingsVersion` to settings interface for migration support190191## Detailed References192193| Topic | File | When to Load |194| -------------------------------- | --------------------------------- | ------------------------------------------------------------- |195| Lifecycle & Core API | `reference/lifecycle.md` | Always; building any plugin feature |196| ESLint Rules (28 rules) | `reference/eslint-rules.md` | ESLint setup, pre-submission audit, rule reference |197| Accessibility (MANDATORY) | `reference/accessibility.md` | Keyboard nav, ARIA labels, focus indicators, touch targets |198| CodeMirror 6 Editor Extensions | `reference/editor-extensions.md` | Editor decorations, syntax highlighting, live preview |199| React / Svelte / Vue Integration | `reference/frameworks.md` | Using React/Vue/Svelte in views or settings |200| Vault & File Operations | `reference/vault-operations.md` | File CRUD, frontmatter, events, caching |201| Settings & Data Migration | `reference/settings-migration.md` | Settings UI, load/save, deep merge, migration pipelines |202| Security & SecretStorage | `reference/security.md` | API keys, credentials, XSS prevention, network requests |203| CSS Styling | `reference/css-accessibility.md` | Theming, CSS variables, scoping, mobile styles |204| Dev Workflow & CLI | `reference/dev-workflow.md` | Build, hot-reload, CLI debugging, Obsidian CLI, ESLint config |205| Testing | `reference/testing.md` | Unit tests, mocking Obsidian API, Jest/Vitest |206| CI/CD & Release | `reference/cicd-release.md` | GitHub Actions, version bump, community submission |207208## Development Workflow209210### Quick Dev Loop (with Obsidian CLI)211212```bash213# Build and hot-reload214npm run build && obsidian plugin:reload id=<plugin-id>215216# Check for errors217obsidian dev:errors218219# Inspect DOM220obsidian dev:dom selector=".my-plugin-view"221222# Take screenshot223obsidian dev:screenshot224225# Evaluate JS in Obsidian context226obsidian eval code="app.plugins.plugins"227```228229### Without Obsidian CLI230231```bash232# Build and copy to test vault233npm run build && cp main.js manifest.json styles.css /path/to/TestVault/.obsidian/plugins/<plugin-id>/234# Then reload in Obsidian: Ctrl+P → "Reload app without saving"235```236237## Pre-Submission Checklist238239Before creating a release or submitting to community plugins, verify:240241### Submission Validation (Bot checks — will auto-reject if incorrect)242243- [ ] `id` in manifest.json does not contain "obsidian"; doesn't end with "plugin"; lowercase only244- [ ] `name` does not contain "Obsidian"; doesn't end with "Plugin"; doesn't start with "Obsi" or end with "dian"245- [ ] `description` does not contain "Obsidian" or "This plugin"; must end with `.?!)` punctuation; max 250 chars246- [ ] `manifest.json` `id`, `name`, `description` match submission entry in `community-plugins.json`247- [ ] `LICENSE` file present; copyright holder ≠ "Dynalist Inc."; year is current248- [ ] `styles.css` exists (empty if no styles)249- [ ] `versions.json` exists with correct version mapping250- [ ] GitHub release has `main.js`, `manifest.json`, `styles.css` attached251252### Code Quality253254- [ ] All sample/template code removed (`MyPlugin`, `SampleSettingTab`, `SampleModal`)255- [ ] No `innerHTML`/`outerHTML` anywhere in code256- [ ] No default hotkeys set257- [ ] No `console.log` in production (remove or use conditional `console.debug`)258- [ ] No unused imports259- [ ] `setHeading()` used instead of `<h2>` in settings260- [ ] Sentence case for all UI text (run ESLint to verify)261- [ ] `this.app` used everywhere (not global `app`)262- [ ] All resources cleaned up in `onunload()`263- [ ] No `Object.assign(defaults, saved)` — use `Object.assign({}, defaults, saved)`264- [ ] Use `fileManager.trashFile()` not `vault.delete()`265- [ ] No regex lookbehind (`(?<=...)`) — breaks on iOS266- [ ] Use `vault.configDir` not hardcoded `.obsidian`267268### Accessibility (MANDATORY)269270- [ ] All interactive elements keyboard accessible (Tab, Enter, Space)271- [ ] ARIA labels on all icon-only buttons272- [ ] `:focus-visible` styled with Obsidian CSS variables273- [ ] Touch targets ≥ 44×44px274- [ ] Can use entire plugin without a mouse275276### ESLint & Release277278- [ ] ESLint passes with `eslint-plugin-obsidianmd` (`npx eslint .`)279- [ ] `manifest.json` version correct, `minAppVersion` set280- [ ] `isDesktopOnly: true` only if using Node/Electron APIs281282## Reference Source Tracking283284| Reference File | Primary Sources | Last Verified |285| ----------------------- | -------------------------------------------------------------- | ------------- |286| `lifecycle.md` | obsidian API docs, gapmiss/obsidian-plugin-skill | 2026-03 |287| `eslint-rules.md` | obsidianmd/eslint-plugin v0.1.9, gapmiss/obsidian-plugin-skill | 2026-03 |288| `accessibility.md` | gapmiss/obsidian-plugin-skill, obsidian plugin guidelines | 2026-03 |289| `editor-extensions.md` | CM6 docs, @codemirror/view source | 2026-03 |290| `frameworks.md` | Leonezz/obsidian-plugin-dev-skill, React docs | 2026-03 |291| `vault-operations.md` | obsidian API docs, official plugin guidelines | 2026-03 |292| `settings-migration.md` | Leonezz/obsidian-plugin-dev-skill | 2026-03 |293| `security.md` | gapmiss/obsidian-plugin-skill, obsidian developer policies | 2026-03 |294| `css-accessibility.md` | davidvkimball/obsidian-dev-skills, obsidian sample theme | 2026-03 |295| `dev-workflow.md` | adriangrantdotorg/Obsidian-Skills, obsidian CLI docs | 2026-03 |296| `testing.md` | Leonezz/obsidian-plugin-dev-skill | 2026-03 |297| `cicd-release.md` | Leonezz/obsidian-plugin-dev-skill, obsidian submission docs | 2026-03 |298299To update references: check each source for new content, cross-reference with obsidian developer docs changelog.300301## Design Decisions3023031. **SKILL.md stays under 500 lines** — quick reference + links to detailed docs3042. **Reference files are topic-based** — load only what you need3053. **Code examples are real** — from actual plugin patterns, not toy demos3064. **Do/Don't tables** — clear before/after comparisons