All skills
gapmiss avatar

/obsidian

@ff97cb0
by gapmissgapmiss/obsidian-plugin-skill188 stars
15

Comprehensive guidelines for Obsidian.md plugin development including ESLint rules from eslint-plugin-obsidianmd v0.4.2, 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.

Use this Skill: https://skilld.dev/gh/gapmiss/obsidian-plugin-skill/obsidian

This session only. Nothing lands on disk.

SKILL.md

β‰ˆ120 tokens always: the name and description. β‰ˆ4.4k when used: this file. β‰ˆ28k more on demand in 10 files.

Obsidian Plugin Development Guidelines

Follow these comprehensive guidelines derived from the official Obsidian ESLint plugin rules, submission requirements, and best practices.

Getting Started

Quick Start Tool

For new plugin projects, an interactive boilerplate generator is available:

  • Script: tools/create-plugin.js in the skill repository
  • Command: Invoke create-plugin using your agent's method (/create-plugin, $create-plugin, or @create-plugin)
  • Generates minimal, best-practice boilerplate with no sample code
  • Detects existing projects and only adds missing files

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.


Rules Reference (eslint-plugin-obsidianmd v0.4.2)

Submission & Naming

# 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

Memory & Lifecycle

# 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

Type Safety

# 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

UI/UX

# Rule βœ… Do ❌ Don't
11 UI text Sentence case β€” "Advanced settings"; use acronyms/brands options for proper names 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 |

Declarative Settings (1.13.0+)

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.

API Best Practices

# 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()
24a Response typing Parse response.text and validate/cast once at the API boundary Pass response.json (typed any) into plugin code
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

Popout Window Compatibility

# 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 window.setTimeout(), window.setInterval(), window.requestAnimationFrame(), etc. (exception to rule 29) Use bare setTimeout()/setInterval(), or activeWindow.setTimeout() β€” prefer-window-timers flags both
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.

Event Handling

# Rule βœ… Do ❌ Don't
31 Editor drop/paste Check evt.defaultPrevented and call evt.preventDefault() Handle editor-drop/paste without checking defaultPrevented

Styling

# 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

Security & Compatibility

# 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

Accessibility (MANDATORY)

# 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

Code Quality

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

Detailed Guidelines

For comprehensive information on specific topics, see the reference files:

Memory Management & Lifecycle

  • Using registerEvent(), addCommand(), registerDomEvent(), registerInterval()
  • registerDomEvent() vs manual addEventListener (and the activeDocument drift bug)
  • Avoiding view references in plugin
  • Not using plugin as component
  • Proper leaf cleanup

Type Safety

  • Using instanceof instead of type casting
  • Avoiding any type
  • Using const and let over var

UI/UX Standards

  • Sentence case enforcement (TypeScript, JSON locale, TS/JS locale modules)
  • recommendedWithLocalesEn config for locale file checks
  • Command naming conventions (no "command", no plugin name, no plugin ID)
  • Settings and configuration best practices
  • Declarative settings API (1.13+): migration paths, control types, pitfalls

File & Vault Operations

  • View access patterns
  • Editor vs Vault API
  • Atomic file operations
  • File management
  • Path handling

CSS Styling Best Practices

  • Avoiding inline styles
  • Using Obsidian CSS variables
  • Avoiding !important (use specificity or CSS variables)
  • Avoiding :has selector (toggle classes from TypeScript instead)
  • Scoping plugin styles
  • Theme support
  • Spacing and layout

Accessibility (A11y)

  • Keyboard navigation (MANDATORY)
  • ARIA labels and roles (MANDATORY)
  • Tooltips and accessibility
  • Focus management (MANDATORY)
  • Focus visible styles (MANDATORY)
  • Screen reader support (MANDATORY)
  • Mobile and touch accessibility (MANDATORY)
  • Accessibility checklist

Code Quality & Best Practices

  • Removing sample code
  • Security best practices
  • Platform compatibility
  • API usage best practices
  • Async/await patterns
  • DOM helpers
  • Deprecated/replaceable packages (e.g., builtin-modules β†’ node:module)

Plugin Submission Requirements

  • Repository structure
  • Submission process
  • Semantic versioning
  • Testing checklist
  • Additional resources and important notes

Community Plugin Scanner

  • What the scanner runs (ESLint rule sets + checks beyond ESLint)
  • Scorecard system (Health, Review, Disclosures, improvement tips)
  • Version-stamped β€” the single file to update as the scanner evolves

ESLint Setup Guide

  • Complete ESLint config for community scanner compliance
  • Why typescript-eslint recommendedTypeChecked is required
  • Common violations and fixes (floating promises, require imports, etc.)
  • Popout window compatibility rules

Plugin Submission Validation Workflow

Before submitting a plugin, follow this sequence:

  1. Run ESLint β€” npx eslint . using eslint-plugin-obsidianmd; fix all errors AND warnings (warnings affect your Scorecard)
  2. Validate manifest β€” Confirm id, name, description, version, and minAppVersion meet naming and formatting rules (rules 1–5)
  3. Check LICENSE β€” Copyright holder must not be "Dynalist Inc." and the year must be current
  4. Test on mobile β€” Verify no regex lookbehind, no fetch(), and touch targets β‰₯ 44Γ—44px (skip only if plugin is declared desktop-only)
  5. Keyboard accessibility audit β€” Tab through all interactive elements; confirm focus indicators and ARIA labels are present
  6. Create GitHub Release β€” Tag must match manifest.json version; attach main.js, manifest.json, and styles.css (optional)
  7. Submit via community.obsidian.md β€” Sign in, link GitHub account, navigate to Plugins β†’ New plugin, enter repository URL, review Developer policies, and submit

If ESLint reports new errors after fixing, re-run from step 1.


Scorecard System

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:

  • Aim for 90%+ overall score
  • Fix ALL ESLint warnings, not just errors β€” warnings are publicly visible
  • Use typescript-eslint/recommendedTypeChecked for type-aware checks
  • Add GitHub artifact attestation to releases

See Community Plugin Scanner for full details on scanner checks, Health metrics, Review checks, common warnings, and improvement tips.


When Reviewing/Writing Code

Use this checklist for code review and implementation:

  1. Memory management: Are components and views properly managed?
  2. Type safety: Using instanceof instead of casts?
  3. UI text: Is everything in sentence case?
  4. Command naming: No redundant words?
  5. File operations: Using preferred APIs?
  6. Mobile compatibility: No iOS-incompatible features?
  7. Sample code: Removed all boilerplate?
  8. Manifest: Correct version, valid structure?
  9. Accessibility: Keyboard navigation, ARIA labels, focus indicators?
  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?

Common Patterns

Proper Command Registration

// βœ… CORRECT
this.addCommand({
  id: 'insert-timestamp',
  name: 'Insert timestamp',
  editorCallback: (editor: Editor, view: MarkdownView) => {
    editor.replaceSelection(new Date().toISOString());
  }
});

Safe Type Narrowing

// βœ… 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);
}

Keyboard Accessible Button

// βœ… 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();
  }
});

Declarative Settings (1.13+)

// βœ… CORRECT β€” getSettingDefinitions() replaces display() on 1.13+
getSettingDefinitions() {
  return [
    {
      name: 'Mode',
      control: {
        type: 'dropdown',
        key: 'mode',
        defaultValue: 'fast',
        options: { fast: 'Fast', thorough: 'Thorough' },
      },
    },
  ];
}

Themed CSS

/* βœ… 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.

Source: SKILL.md on GitHub

No alerts4d5 checks Β· Risk SAFE
  • Gen Agent Trust Hub5d

    The skill is a comprehensive, high-quality documentation resource for Obsidian.md plugin development. It provides correct security guidance (such as XSS prevention), memory management practices, and accessibility standards. No malicious code, data exfiltration, or obfuscation was detected.

  • Socket4d

    No alerts

  • Snyk5d

    Risk: LOW Β· No issues

  • Runlayer7mo

    9 files scanned Β· No issues

  • ZeroLeaks5mo

    Score: 93/100 Β· 2 sections analyzed

Signed by skilld at ff97cb0. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub last week.

Activeupdated last week
metadata
{
  "version": "1.11.1"
}

README badge

README badge for gapmiss/obsidian-plugin-skill