Comprehensive guidelines for Obsidian.md plugin development including ESLint rules from eslint-plugin-obsidianmd v0.4.1, TypeScript best practices, memory management, API usage (requestUrl vs fetch), UI/UX standards, popout window compatibility, community.obsidian.md submission process, and Scorecard optimization. Use when working with Obsidian plugins, main.ts files, manifest.json, Plugin class, MarkdownView, TFile, vault operations, or any Obsidian API development.
npx skills add https://github.com/gapmiss/obsidian-plugin-skill --skill obsidian
Follow these comprehensive guidelines derived from the official Obsidian ESLint plugin rules, submission requirements, and best practices.
For new plugin projects, an interactive boilerplate generator is available:
tools/create-plugin.js in the skill repositorycreate-plugin using your agent's method (/create-plugin, $create-plugin, or @create-plugin)Recommend the boilerplate generator when users ask how to create a new plugin, want to start a new project, or need help setting up the basic structure.
| # | Rule | ✅ Do | ❌ Don't |
|---|------|--------|----------|
| 1 | Plugin ID | Omit "obsidian"; don't end with "plugin" | Include "obsidian" or end with "plugin" |
| 2 | Plugin name | Omit "Obsidian"; don't end with "Plugin" | Include "Obsidian" or end with "Plugin" |
| 3 | Plugin name | Don't start with "Obsi" or end with "dian" | Start with "Obsi" or end with "dian" |
| 4 | Description | Omit "Obsidian", "This plugin", etc. | Use "Obsidian" or "This plugin" |
| 5 | Description | End with .?!) punctuation | Leave description without terminal punctuation |
| # | Rule | ✅ Do | ❌ Don't |
|---|------|--------|----------|
| 6 | Event cleanup | Use registerEvent() for automatic cleanup | Register events without cleanup |
| 6a | DOM events | Use registerDomEvent() on the plugin or owning component | Pair addEventListener with manual removeEventListener cleanup |
| 7 | View references | Return views/components directly | Store view references in plugin properties or pass plugin as component to MarkdownRenderer |
| 8 | Leaf detachment | Let Obsidian handle leaf cleanup | Call detachLeavesOfType() in onunload |
| # | Rule | ✅ Do | ❌ Don't |
|---|------|--------|----------|
| 9 | TFile/TFolder | Use instanceof for type checking | Cast to TFile/TFolder; use any; use var |
| 10 | DOM instanceof | Use .instanceOf(T) for DOM Nodes/UIEvents | Use instanceof for cross-window DOM checks |
| # | Rule | ✅ Do | ❌ Don't |
|---|------|--------|----------|
| 11 | UI text | Sentence case — "Advanced settings" | Title Case — "Advanced Settings" |
| 12 | JSON locale | Sentence case in JSON locale files (recommendedWithLocalesEn) | Title case in locale JSON |
| 13 | TS/JS locale | Sentence case in TS/JS locale modules | Title case in locale modules |
> Note (v0.4.0): ui/sentence-case is now enabled (warn) and enforced on inline UI strings — it was disabled in v0.3.0. Use the recommendedWithLocalesEn config to also check English locale files (rules 12–13).
| 14 | Command names | Omit "command" in command names/IDs | Include "command" in names/IDs |
| 15 | Command IDs | Omit plugin ID/name from command IDs/names | Duplicate plugin ID in command IDs |
| 16 | Hotkeys | No default hotkeys | Set default hotkeys |
| 17 | Settings headings | Use .setHeading() | Create manual HTML headings; use "General", "settings", or plugin name in headings |
All four settings-tab rules ship as warn in recommended. Rules 17a/17c/17d read minAppVersion from manifest.json; 17b is not version-gated.
| # | Rule | ✅ Do | ❌ Don't |
|---|------|--------|----------|
| 17a | settings-tab/require-display | Keep display() when minAppVersion < 1.13.0 | Ship declarative-only settings that render nothing on older Obsidian |
| 17b | settings-tab/prefer-setting-definitions | Implement getSettingDefinitions() on every PluginSettingTab | Rely on display() alone — settings won't appear in 1.13+ global search |
| 17c | settings-tab/prefer-update-over-display | Call this.update() to re-render declarative settings | Call this.display() — it's bypassed when definitions are non-empty |
| 17d | settings-tab/no-deprecated-display | Delete display() once minAppVersion >= 1.13.0 and definitions exist | Leave a dead display() behind (auto-fixable) |
| — | Settings data | Keep all persisted data inside plugin.settings | Store sibling keys via saveData() — auto-persist clobbers them |
> Detection caveat: these rules match a bare extends PluginSettingTab only. extends obsidian.PluginSettingTab is out of scope and won't be flagged — but the underlying guidance still applies.
| # | Rule | ✅ Do | ❌ Don't |
|---|------|--------|----------|
| 18 | Active file edits | Use Editor API | Use Vault.modify() for active file edits |
| 19 | Background file mods | Use Vault.process() | Use Vault.modify() for background modifications |
| 20 | File deletion | Use FileManager.trashFile() | Use Vault.trash() or Vault.delete() directly |
| 21 | File lookup | Use Vault.getAbstractFileByPath() | Iterate all files with Vault.getFiles().find() |
| 22 | User paths | Use normalizePath() | Hardcode .obsidian path; use raw user paths |
| 23 | OS detection | Use Platform API | Use navigator.platform/userAgent |
| 24 | Network requests | Use requestUrl() | Use fetch() |
| 25 | Logging | Minimize console logging; none in onload/onunload in production | Use console.log in onload/onunload |
| 26 | Input suggest | Use built-in AbstractInputSuggest | Copy Liam's TextInputSuggest implementation |
| 27 | API compatibility | Check minAppVersion for API availability (e.g., getSettingDefinitions() requires 1.13.0) | Use APIs not available in declared minAppVersion |
| 28 | Language detection | Use Obsidian's getLanguage() | Use localStorage.getItem('language') or i18next-browser-languagedetector |
| # | Rule | ✅ Do | ❌ Don't |
|---|------|--------|----------|
| 29 | Document/Window | Use activeDocument and activeWindow | Use global document and window |
| 29a | Getter capture | Capture activeDocument in a variable when the same document is needed later | Call activeDocument at setup and again at cleanup — it follows focus and may return different documents |
| 30 | Timers | Use activeWindow.setTimeout(), setInterval(), etc. | Use bare setTimeout(), setInterval() |
| 31 | Main workspace UI | Use this.app.workspace.containerEl.ownerDocument from settings | Use activeDocument to update main workspace from settings window |
> Note (v0.4.0): prefer-active-doc remains disabled by default — the only Obsidian rule shipped as off. Enable it manually for popout window support.
> Note (v1.13.0): Settings now open in a new window. activeDocument from settings callbacks points to the settings window, not the main vault. Use this.app.workspace.containerEl.ownerDocument to target main workspace UI.
> Note: activeDocument/activeWindow are dynamic getters that track the focused window. A listener added via activeDocument.addEventListener() at setup cannot reliably be removed via activeDocument.removeEventListener() at cleanup. Prefer registerDomEvent() (rule 6a), which captures the target at registration.
| # | Rule | ✅ Do | ❌ Don't |
|---|------|--------|----------|
| 31 | Editor drop/paste | Check evt.defaultPrevented and call evt.preventDefault() | Handle editor-drop/paste without checking defaultPrevented |
| # | Rule | ✅ Do | ❌ Don't |
|---|------|--------|----------|
| 32 | CSS variables | Use Obsidian CSS variables for all styling | Hardcode colors, sizes, or spacing |
| 33 | CSS scope | Scope CSS to plugin containers | Use broad CSS selectors |
| 34 | Style elements | Use styles.css file (no-forbidden-elements) | Create <link> or <style> elements; assign styles via JavaScript |
| 34a | !important | Increase selector specificity or use CSS variables | Use !important — overrides user themes/snippets |
| 34b | :has selector | Toggle classes from TypeScript when conditions change | Use :has — causes broad selector invalidation and performance issues |
| # | Rule | ✅ Do | ❌ Don't |
|---|------|--------|----------|
| 35 | DOM creation | Use Obsidian DOM helpers (createEl(), createDiv(), createSpan(), createSvg(), createFragment()) via prefer-create-el; linter autofixes activeDocument.createElement() → activeWindow.createEl() (v0.4.1) | Use document.createElement(), document.createDocumentFragment(), etc. |
| 36 | Node.js modules | Guard Node.js imports with Platform.isDesktop check (no-nodejs-modules) | Import Node.js modules without platform guard |
| 37 | iOS compat | Avoid regex lookbehind (iOS < 16.4 incompatibility) | Use regex lookbehind |
| # | Rule | ✅ Do | ❌ Don't |
|---|------|--------|----------|
| 38 | Keyboard access | Make all interactive elements keyboard accessible; Tab through all elements | Create inaccessible interactive elements |
| 39 | ARIA labels | Provide ARIA labels for icon buttons; use data-tooltip-position for tooltips | Use icon buttons without ARIA labels |
| 40 | Focus indicators | Use :focus-visible with Obsidian CSS variables; touch targets ≥ 44×44px | Remove focus indicators; make touch targets < 44×44px |
| Rule | ✅ Do | ❌ Don't |
|------|--------|----------|
| Sample code | Remove all sample/template code | Keep class names like MyPlugin, SampleModal |
| Object.assign | Object.assign({}, defaults, overrides) (object-assign) | Object.assign(defaultsVar, other) — mutates defaults |
| LICENSE | Copyright holder must not be "Dynalist Inc."; year must be current (validate-license) | Leave "Dynalist Inc." as holder or use an outdated year |
| Async | Use async/await | Use Promise chains |
| Deprecated packages | Replace flagged npm packages with Node.js built-ins (e.g., builtin-modules → import { builtinModules } from "node:module") | Use packages the scanner flags as replaceable |
For comprehensive information on specific topics, see the reference files:
registerEvent(), addCommand(), registerDomEvent(), registerInterval()registerDomEvent() vs manual addEventListener (and the activeDocument drift bug)instanceof instead of type castingany typeconst and let over varrecommendedWithLocalesEn config for locale file checks!important (use specificity or CSS variables):has selector (toggle classes from TypeScript instead)builtin-modules → node:module)typescript-eslint recommendedTypeChecked is requiredBefore submitting a plugin, follow this sequence:
npx eslint . using eslint-plugin-obsidianmd; fix all errors AND warnings (warnings affect your Scorecard)id, name, description, version, and minAppVersion meet naming and formatting rules (rules 1–5)fetch(), and touch targets ≥ 44×44px (skip only if plugin is declared desktop-only)manifest.json version; attach main.js, manifest.json, and styles.css (optional)If ESLint reports new errors after fixing, re-run from step 1.
Published plugins receive a Scorecard visible on community.obsidian.md. The Scorecard affects user trust and discoverability — a poor score deters users from installing.
Key points:
typescript-eslint/recommendedTypeChecked for type-aware checksSee Community Plugin Scanner for full details on scanner checks, Health metrics, Review checks, common warnings, and improvement tips.
Use this checklist for code review and implementation:
instanceof instead of casts?10. Testing: Can you use the plugin without a mouse?
11. Touch targets: Are all interactive elements at least 44×44px?
12. Focus styles: Using :focus-visible and proper CSS variables?
13. Settings: Using declarative getSettingDefinitions() on 1.13+? All saved data co-located in plugin.settings?
// ✅ CORRECT
this.addCommand({
id: 'insert-timestamp',
name: 'Insert timestamp',
editorCallback: (editor: Editor, view: MarkdownView) => {
editor.replaceSelection(new Date().toISOString());
}
});
// ✅ CORRECT
const file = this.app.vault.getAbstractFileByPath(path);
if (file instanceof TFile) {
// TypeScript now knows it's a TFile
await this.app.vault.read(file);
}
// ✅ CORRECT
const button = containerEl.createEl('button', {
attr: {
'aria-label': 'Open settings',
'data-tooltip-position': 'top'
}
});
button.setText('⚙️');
button.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
performAction();
}
});
// ✅ CORRECT — getSettingDefinitions() replaces display() on 1.13+
getSettingDefinitions() {
return [
{
name: 'Mode',
control: {
type: 'dropdown',
key: 'mode',
defaultValue: 'fast',
options: { fast: 'Fast', thorough: 'Thorough' },
},
},
];
}
/* ✅ CORRECT */
.my-plugin-modal {
background: var(--modal-background);
color: var(--text-normal);
padding: var(--size-4-4);
border-radius: var(--radius-m);
font-size: var(--font-ui-medium);
}
.my-plugin-button:focus-visible {
outline: 2px solid var(--interactive-accent);
outline-offset: 2px;
}
When helping with Obsidian plugin development, proactively apply these rules and suggest improvements based on these guidelines. Refer to the detailed reference files for comprehensive information on specific topics.
Interact with Obsidian vaults using the Obsidian CLI to read, create, search, and manage notes, tasks, properties, and more. Also supports plugin and theme development with commands to reload plugins, run JavaScript, capture errors, take screenshots, and inspect the DOM. Use when the user asks to interact with their Obsidian vault, manage notes, search vault content, perform vault operations from the command line, or develop and debug Obsidian plugins and themes.
Comprehensive project architecture blueprint generator that analyzes codebases to create detailed architectural documentation. Automatically detects technology stacks and architectural patterns, generates visual diagrams, documents implementation patterns, and provides extensible blueprints for maintaining architectural consistency and guiding new development.
Review the changes since a fixed point (commit, branch, tag, or merge-base) along two axes — Standards (does the code follow this repo's documented coding standards?) and Spec (does the code match what the originating issue/PRD asked for?). Runs both reviews in parallel sub-agents and reports them side by side. Use when the user wants to review a branch, a PR, work-in-progress changes, or asks to "review since X".
Master API documentation with OpenAPI 3.1, AI-powered tools, and modern developer experience practices. Create interactive docs, generate SDKs, and build comprehensive developer portals.
Creates comprehensive API changelogs documenting breaking changes, deprecations, and migration strategies for API consumers. Use when managing API versions, communicating breaking changes, or creating upgrade guides.
Master API documentation with OpenAPI 3.1, AI-powered tools, and modern developer experience practices. Create interactive docs, generate SDKs, and build comprehensive developer portals. Use PROACTIVELY for API documentation or developer portal creation.
Analyze fundamental data primitives, type systems, and state management patterns in a codebase. Use when (1) evaluating typing strategies (Pydantic vs TypedDict vs loose dicts), (2) assessing immutability and mutation patterns, (3) understanding serialization approaches, (4) documenting state shape and lifecycle, or (5) comparing data modeling approaches across frameworks.
Write minimal, evergreen code comments that explain complex logic without documenting obvious behavior or temporary changes. Use this skill when adding comments to PHP files, TypeScript/JavaScript files, or any code files, when documenting complex algorithms or business logic, when adding PHPDoc blocks or JSDoc comments, when writing self-documenting code with clear naming, or when reviewing existing comments for relevance and necessity. Focus on keeping code self-explanatory through clear structure and naming rather than relying heavily on comments.
Take gapmiss/obsidian from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.
The instructions reference npx.
Without those the skill loads but fails at the first command.