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-dev3description: 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---56# Obsidian Plugin Development78## When This Skill Applies910Use this skill when the user is:1112- Creating a new Obsidian plugin from scratch13- Implementing plugin features (commands, views, modals, settings, editor extensions)14- Debugging plugin issues or unexpected behavior15- Configuring build tools (Vite, esbuild, rollup)16- Writing tests for Obsidian plugins17- Setting up CI/CD and release workflows18- Preparing a plugin for community submission19- Working with CodeMirror 6 editor extensions20- Integrating React/Svelte/Vue into Obsidian views2122## Critical Rules (Always Follow)2324| # | Rule | Why |25| --- | ------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------- |26| 1 | **Never use global `app`** — use `this.app` | Global `app` breaks in multi-window; submission rejected |27| 2 | **Never use `innerHTML`/`outerHTML`** — use `createEl()`, `createDiv()`, `setText()` | XSS vulnerability; instant rejection |28| 3 | **Use `registerEvent()`** for all event subscriptions | Auto-cleanup on unload; prevents memory leaks |29| 4 | **No default hotkeys** — let users configure | Hotkey conflicts with other plugins |30| 5 | **Use `requestUrl()` over `fetch()`** | Bypasses CORS; works on mobile |31| 6 | **Use `normalizePath()`** for user-provided paths | Handles cross-platform path differences |32| 7 | **Prefer `vault.process()`** over `vault.modify()` | Atomic operation; safe with concurrent edits |33| 8 | **Use `FileManager.processFrontMatter()`** for YAML | Never parse/serialize YAML manually |34| 9 | **Use Sentence case** for all UI text | Obsidian convention; submission requirement |35| 10 | **Use `setHeading()`** not `<h1>`/`<h2>` | Semantic; supports RTL; submission requirement |36| 11 | **Import only what you use** — no unused classes | Cleaner code; easier audits; submission reviewers check this |37| 12 | **Use `checkCallback` when command depends on context** | `callback` = always available; `checkCallback` = conditionally shown; `editorCallback` = needs editor |38| 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 |39| 14 | **No regex lookbehind** — `(?!...)` OK, `(?<=...)` NOT OK | Breaks on iOS Safari < 16.4; submission rejected |40| 15 | **All interactive elements keyboard accessible** | Tab navigation + Enter/Space; submission requirement |41| 16 | **ARIA labels on all icon-only buttons** | Screen reader support; submission requirement |42| 17 | **Touch targets ≥ 44×44px** | Mobile usability; submission requirement |43| 18 | **Use `vault.configDir` not `.obsidian`** | Cross-platform compatibility; submission requirement |44| 19 | **Use `fileManager.trashFile()` not `vault.delete()`** | Respects user trash settings |45| 20 | **Use `AbstractInputSuggest` not `TextInputSuggest`** | Built-in API; Liam's copy-pasted implementation is banned |46| 21 | **Create `versions.json`** — maps plugin version → min Obsidian version | Submission bot checks for it; auto-reject if missing |47| 22 | **Version your settings schema** — `_settingsVersion` field | Enables migration pipeline on upgrade; prevents data loss |4849## Quick Reference5051### Plugin Lifecycle5253```typescript54import { Plugin } from 'obsidian';5556export default class MyPlugin extends Plugin {57 async onload() {58 // 1. Load settings FIRST59 await this.loadSettings();60 // 2. Add settings tab61 this.addSettingTab(new MySettingTab(this.app, this));62 // 3. Register commands63 this.addCommand({ id: 'my-command', name: 'My command', callback: () => {} });64 // 4. Register views65 this.registerView(MY_VIEW_TYPE, leaf => new MyView(leaf));66 // 5. Register editor extensions67 this.registerEditorExtension(myExtension);68 // 6. Register events69 this.registerEvent(this.app.vault.on('modify', file => {}));70 this.registerDomEvent(document, 'click', evt => {});71 this.registerInterval(window.setInterval(() => {}, 1000));72 }7374 async onunload() {75 // Resources registered with register*() are auto-cleaned76 // Manual cleanup needed for: MutationObserver, React root, vault.on() in React77 }78}79```8081### Essential API Cheatsheet8283| Need | API |84| ------------------------ | ------------------------------------------------------------- |85| Get active file | `this.app.workspace.getActiveFile()` |86| Read file | `this.app.vault.cachedRead(file)` |87| Modify file (background) | `this.app.vault.process(file, (data) => data)` |88| Modify file (editor) | `editor.replaceSelection()`, `editor.getRange()` |89| Create file | `this.app.vault.create(path, content)` |90| Delete file | `this.app.fileManager.trashFile(file)` |91| Rename file | `this.app.fileManager.renameFile(file, newPath)` |92| Read frontmatter | `this.app.metadataCache.getFileCache(file)?.frontmatter` |93| Write frontmatter | `this.app.fileManager.processFrontMatter(file, (fm) => {})` |94| Show notification | `new Notice('message', duration)` |95| Open modal | `new MyModal(this.app).open()` |96| Get active editor | `this.app.workspace.activeEditor?.editor` |97| Platform check | `Platform.isMacOS`, `Platform.isMobile`, `Platform.isDesktop` |98| Network request | `requestUrl({ url, method, headers, body })` |99| Persist data | `this.loadData()` / `this.saveData(data)` |100| Secure storage | `this.app.secretStorage.setSecret(id, value)` (v1.11.4+) |101| Detect theme | `document.body.classList.contains('theme-dark')` |102103### Command Callback Decision Tree104105```106Does the command need an active editor?107├─ YES → editorCallback108│ (automatically hidden when no editor; gives you editor + view)109│110└─ NO → Does it need any context to run? (active file, leaf, etc.)111 ├─ YES → checkCallback112 │ (return true when available; run action on !checking)113 │114 └─ NO → callback115 (always visible, always runs)116```117118**Examples:**119120```typescript121// Always available — no conditions122this.addCommand({123 id: 'open-settings',124 name: 'Open plugin settings',125 callback: () => {126 this.openSettings();127 },128});129130// Needs active file — use checkCallback131this.addCommand({132 id: 'copy-stats',133 name: 'Copy note statistics',134 checkCallback: checking => {135 const file = this.app.workspace.getActiveFile();136 if (file) {137 if (!checking) this.copyStats(file);138 return true;139 }140 return false;141 },142});143144// Needs editor — use editorCallback145this.addCommand({146 id: 'wrap-callout',147 name: 'Wrap selection in callout',148 editorCallback: editor => {149 const sel = editor.getSelection();150 editor.replaceSelection(`> [!note]\n> ${sel}`);151 },152});153```154155### Import Hygiene156157Only import what you actually use. Submission reviewers flag unused imports.158159```typescript160// Good — only what's needed161import { MarkdownView, Notice, Plugin, PluginSettingTab, Setting } from 'obsidian';162163// Bad — unused imports164import { App, Editor, Modal, Notice, Plugin, PluginSettingTab, Setting } from 'obsidian';165// ^^^ ^^^^^^ ^^^^^ — never used166```167168## Common Pitfalls1691701. **Storing view references** → use `getLeavesOfType()` on demand1712. **Passing plugin as Component** → use `this.addChild()` instead1723. **Detaching leaves in onunload** → they reinitialize on update1734. **Not removing sample code** → `MyPlugin`, `SampleSettingTab` must be renamed1745. **Using `vault.modify()` on active file** → use Editor API instead1756. **Manual YAML parsing** → use `processFrontMatter()` instead1767. **`fetch()` for API calls** → use `requestUrl()` instead1778. **Hardcoded colors in CSS** → use `var(--text-normal)`, etc.1789. **`navigator.platform`** → use `Platform.isMacOS` instead17910. **`var` declarations** → use `const`/`let` instead18011. **Promise chains** → use `async/await` instead18112. **`console.log` in production** → remove or use `console.debug` with conditional18213. **Regex lookbehind `(?<=...)`** → breaks on iOS Safari < 16.4; use alternative patterns18314. **`Object.assign(defaults, saved)`** → mutates defaults; use `Object.assign({}, defaults, saved)`18415. **Hardcoded `.obsidian` path** → use `this.app.vault.configDir` instead18516. **Shallow merge for nested settings** → use deep merge; shallow merge loses nested defaults18617. **`vault.delete()` for removing files** → use `fileManager.trashFile()` to respect user settings18718. **Liam's `TextInputSuggest`** → use built-in `AbstractInputSuggest` instead18819. **Missing `styles.css`** → create empty file if no styles (submission bot checks for it)18920. **Missing `versions.json`** → create with `{ "1.0.0": "1.0.0" }` (submission bot checks for it)19021. **No settings version tracking** → add `_settingsVersion` to settings interface for migration support191192## Detailed References193194| Topic | File | When to Load |195| -------------------------------- | --------------------------------- | ------------------------------------------------------------- |196| Lifecycle & Core API | `reference/lifecycle.md` | Always; building any plugin feature |197| ESLint Rules (28 rules) | `reference/eslint-rules.md` | ESLint setup, pre-submission audit, rule reference |198| Accessibility (MANDATORY) | `reference/accessibility.md` | Keyboard nav, ARIA labels, focus indicators, touch targets |199| CodeMirror 6 Editor Extensions | `reference/editor-extensions.md` | Editor decorations, syntax highlighting, live preview |200| React / Svelte / Vue Integration | `reference/frameworks.md` | Using React/Vue/Svelte in views or settings |201| Vault & File Operations | `reference/vault-operations.md` | File CRUD, frontmatter, events, caching |202| Settings & Data Migration | `reference/settings-migration.md` | Settings UI, load/save, deep merge, migration pipelines |203| Security & SecretStorage | `reference/security.md` | API keys, credentials, XSS prevention, network requests |204| CSS Styling | `reference/css-accessibility.md` | Theming, CSS variables, scoping, mobile styles |205| Dev Workflow & CLI | `reference/dev-workflow.md` | Build, hot-reload, CLI debugging, Obsidian CLI, ESLint config |206| Testing | `reference/testing.md` | Unit tests, mocking Obsidian API, Jest/Vitest |207| CI/CD & Release | `reference/cicd-release.md` | GitHub Actions, version bump, community submission |208209## Development Workflow210211### Quick Dev Loop (with Obsidian CLI)212213```bash214# Build and hot-reload215npm run build && obsidian plugin:reload id=<plugin-id>216217# Check for errors218obsidian dev:errors219220# Inspect DOM221obsidian dev:dom selector=".my-plugin-view"222223# Take screenshot224obsidian dev:screenshot225226# Evaluate JS in Obsidian context227obsidian eval code="app.plugins.plugins"228```229230### Without Obsidian CLI231232```bash233# Build and copy to test vault234npm run build && cp main.js manifest.json styles.css /path/to/TestVault/.obsidian/plugins/<plugin-id>/235# Then reload in Obsidian: Ctrl+P → "Reload app without saving"236```237238## Pre-Submission Checklist239240Before creating a release or submitting to community plugins, verify:241242### Submission Validation (Bot checks — will auto-reject if incorrect)243244- [ ] `id` in manifest.json does not contain "obsidian"; doesn't end with "plugin"; lowercase only245- [ ] `name` does not contain "Obsidian"; doesn't end with "Plugin"; doesn't start with "Obsi" or end with "dian"246- [ ] `description` does not contain "Obsidian" or "This plugin"; must end with `.?!)` punctuation; max 250 chars247- [ ] `manifest.json` `id`, `name`, `description` match submission entry in `community-plugins.json`248- [ ] `LICENSE` file present; copyright holder ≠ "Dynalist Inc."; year is current249- [ ] `styles.css` exists (empty if no styles)250- [ ] `versions.json` exists with correct version mapping251- [ ] GitHub release has `main.js`, `manifest.json`, `styles.css` attached252253### Code Quality254255- [ ] All sample/template code removed (`MyPlugin`, `SampleSettingTab`, `SampleModal`)256- [ ] No `innerHTML`/`outerHTML` anywhere in code257- [ ] No default hotkeys set258- [ ] No `console.log` in production (remove or use conditional `console.debug`)259- [ ] No unused imports260- [ ] `setHeading()` used instead of `<h2>` in settings261- [ ] Sentence case for all UI text (run ESLint to verify)262- [ ] `this.app` used everywhere (not global `app`)263- [ ] All resources cleaned up in `onunload()`264- [ ] No `Object.assign(defaults, saved)` — use `Object.assign({}, defaults, saved)`265- [ ] Use `fileManager.trashFile()` not `vault.delete()`266- [ ] No regex lookbehind (`(?<=...)`) — breaks on iOS267- [ ] Use `vault.configDir` not hardcoded `.obsidian`268269### Accessibility (MANDATORY)270271- [ ] All interactive elements keyboard accessible (Tab, Enter, Space)272- [ ] ARIA labels on all icon-only buttons273- [ ] `:focus-visible` styled with Obsidian CSS variables274- [ ] Touch targets ≥ 44×44px275- [ ] Can use entire plugin without a mouse276277### ESLint & Release278279- [ ] ESLint passes with `eslint-plugin-obsidianmd` (`npx eslint .`)280- [ ] `manifest.json` version correct, `minAppVersion` set281- [ ] `isDesktopOnly: true` only if using Node/Electron APIs282283## Reference Source Tracking284285| Reference File | Primary Sources | Last Verified |286| ----------------------- | -------------------------------------------------------------- | ------------- |287| `lifecycle.md` | obsidian API docs, gapmiss/obsidian-plugin-skill | 2026-03 |288| `eslint-rules.md` | obsidianmd/eslint-plugin v0.1.9, gapmiss/obsidian-plugin-skill | 2026-03 |289| `accessibility.md` | gapmiss/obsidian-plugin-skill, obsidian plugin guidelines | 2026-03 |290| `editor-extensions.md` | CM6 docs, @codemirror/view source | 2026-03 |291| `frameworks.md` | Leonezz/obsidian-plugin-dev-skill, React docs | 2026-03 |292| `vault-operations.md` | obsidian API docs, official plugin guidelines | 2026-03 |293| `settings-migration.md` | Leonezz/obsidian-plugin-dev-skill | 2026-03 |294| `security.md` | gapmiss/obsidian-plugin-skill, obsidian developer policies | 2026-03 |295| `css-accessibility.md` | davidvkimball/obsidian-dev-skills, obsidian sample theme | 2026-03 |296| `dev-workflow.md` | adriangrantdotorg/Obsidian-Skills, obsidian CLI docs | 2026-03 |297| `testing.md` | Leonezz/obsidian-plugin-dev-skill | 2026-03 |298| `cicd-release.md` | Leonezz/obsidian-plugin-dev-skill, obsidian submission docs | 2026-03 |299300To update references: check each source for new content, cross-reference with obsidian developer docs changelog.301302## Design Decisions3033041. **SKILL.md stays under 500 lines** — quick reference + links to detailed docs3052. **Reference files are topic-based** — load only what you need3063. **Code examples are real** — from actual plugin patterns, not toy demos3074. **Do/Don't tables** — clear before/after comparisons