obsidian
Skill196 starsChanged 8 days agoMITView on GitHub
Comprehensive guidelines for Obsidian.md plugin development including ESLint rules from eslint-plugin-obsidianmd v0.5.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.
96 other projects use a version of this.
Adopted by
Most recently taken up by
- pietz/skillstheir own version, first seen Nov 8, 2026
- Lukk17/agent-standardstheir own version, first seen Nov 7, 2026
- delorenj/skillstheir own version, first seen Nov 7, 2026
- QVerisAI/QVerisBottheir own version, first seen Nov 1, 2026
- tikhomirov/skillstheir own version, first seen Nov 1, 2026
- davisbuilds/dojotheir own version, first seen Nov 1, 2026
Used with
Projects with it also use taskflow-inbox-triage, discord-e2e and 10 more.
Other files people use alongside this one.
Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
AtomicBot-aiSkilltaskflow-inbox-triage
Also in 6 projects that use this
molt-botSkilldiscord-e2e
Test Discord with Convex-leased QA bots: check credentials without a Gateway, exercise owned messages, or prove OpenClaw replies, files, threads, reactions, streaming, and typing. Distinguish API and Gateway evidence from manual user interactions.
Also in 10 projects that use this
molt-botSkillopenclaw-repair-sweep
Run scoped OpenClaw issue/PR repair campaigns: coordinate workers, prove root causes, and land or close verified work under the requested authority.
Also in 10 projects that use this
clawdbotSkillopenclaw-performance
Performance work in OpenClaw: a slow or heavy flow (latency, CPU, memory, SQL, RPC, renders), writing or reviewing a perf PR, or adding a cache. Count the work, delete waste first, cache last.
Also in 7 projects that use this
clawdbotSkillplugin-sdk-impact
Count Plugin SDK consumers across live ClawHub releases and bundled OpenClaw plugins before deprecation, removal, or behavior changes; supports public release-date filters.
Also in 7 projects that use this
emanueleieloSkillsonos
Control Sonos speakers: discover, play/pause, volume, grouping, favorites, and queue management via sonos CLI.
Also in 5 projects that use this
- +2 more from molt-bot
Other versions96 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 96
- moltbot/clawdbot★ 394ksame name
- steipete/warelay★ 394ksame name
- steipete/clawdis★ 394ksame name
- moltbot/moltbot★ 394ksame name
- clawdbot/clawdbot★ 394ksame name
- openclaw/openclaw★ 394ksame name
- molt-bot/clawdbot★ 391ksame name
- NousResearch/hermes-agent★ 262ksame name
- spinabot/brigade★ 11ksame name
- steipete/agent-scripts★ 7.4ksame name
86 more not listed
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
The file
We split this skill into the rules it gives an agent
11 rules. 0 are common, 9 are distinctive, and 2 are very specific to this project.
0
Common
Most of the 91 edited versions we read keep these.
9
Distinctive
This author's own choices. Worth a look.
2
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 11
Follow these comprehensive guidelines derived from the official Obsidian ESLint plugin rules, submission requirements, and best practices.
- Very specific: line 67
All four settings-tab rules ship as warn in recommended.
- Distinctive: line 158
Not using plugin as component
- Distinctive: line 233
Before submitting a plugin, follow this sequence:
- Distinctive: line 243
If ESLint reports new errors after fixing, re-run from step 1.
- Very specific: line 254
Use typescript-eslint/recommendedTypeChecked for type-aware checks
- Distinctive: line 256
Add GitHub artifact attestation to releases
- Distinctive: line 264
Use this checklist for code review and implementation:
- Distinctive: line 269
Command naming: No redundant words?
- Distinctive: line 271
Mobile compatibility: No iOS-incompatible features?
- Distinctive: line 368
When helping with Obsidian plugin development, proactively apply these rules and suggest improvements based on these guidelines.
“Common” is measured: kept by at least 6 in 10 of the 91 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
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.jsin the skill repository - Command: Invoke
create-pluginusing 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.5.1)
Submission & Naming
| # | Rule | ✅ Do | ❌ Don't |
| 1 | Plugin ID | Omit "obsidian"; don't end with "plugin" (checked only with validate-manifest's checkId: true — turn it on before first submission) | Include "obsidian" or end with "plugin"; rename a published plugin's id |
| 2 | Plugin name | Omit "Obsidian" and "Plugin" | Include "Obsidian" or "Plugin" anywhere in the name |
| 3 | Plugin name | Avoid parts of "Obsidian" | Use "obsi", "sidian", or a leading "ob" |
| 4 | Description | Say what it does; don't repeat or start with the name | Use "Obsidian", "This plugin", "A plugin that", or start with the name |
| 5 | Description | 10–250 chars, ending with ., !, or ? | End with ) or no 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; e.targetNode?.instanceOf(T) for event targets | Use instanceof for cross-window DOM checks, including e.target instanceof HTMLElement |
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 |
| 14 | Command names | Omit "command" as a redundant label; "Run command" or copy-command-id (what the command acts on) is fine | Add "command" as a label — "Command: open note", "Open settings command" |
| 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 |
Note (v0.5.0):
ui/sentence-caseis off inrecommended(and the scanner doesn't run it) — sentence case is still the convention, so opt in with"obsidianmd/ui/sentence-case": ["warn", { enforceCamelCaseLower: true }].recommendedWithLocalesEnchecks English locale files (rules 12–13).
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 PluginSettingTabonly.extends obsidian.PluginSettingTabis 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() (1.6.6+), which follows the user's "Deleted files" setting | Use Vault.trash() or Vault.delete() with a fixed destination |
| 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 |
| 26a | Confirm dialogs | Use built-in ConfirmationModal (1.13.0+); use FileManager.promptForDeletion() to confirm and delete files | Hand-build a Modal subclass with its own confirm/cancel button row |
| 27 | API compatibility | Check minAppVersion for API availability (e.g., getSettingDefinitions() 1.13.0, setCssProps() 1.1.7, instanceOf() 0.15.3) | 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 | Window-wide state | Add and remove <body>/<html>/<head> classes, CSS variables, and styles in each window's document (window-open + open windows) | document.body.addClass() (main window only) or activeDocument.body.addClass() (whichever window has focus) |
| 29b | 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(), etc. (exception to rule 29); activeWindow.requestAnimationFrame() is fine for a popout's frames | 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.5.1):
prefer-active-docwarns inrecommended. It reports the globaldocumentand offers an editor suggestion, not an autofix. Changes todocument.body/documentElement/headget their own message and no suggestion (rule 29a).documentin a listener on the main window'sdocument/window, as the fallback inactiveDocument ?? document, and lookups of main-window-only UI (status bar, ribbons) are not reported. Test, tooling, and web worker files are exempt.
Note (v1.13.0): Settings now open in a new window.
activeDocumentfrom settings callbacks points to the settings window, not the main vault. Usethis.app.workspace.containerEl.ownerDocumentto target main workspace UI.
Note:
activeDocument/activeWindoware dynamic getters that track the focused window. A listener added viaactiveDocument.addEventListener()at setup cannot reliably be removed viaactiveDocument.removeEventListener()at cleanup. PreferregisterDomEvent()(rule 6a), which captures the target at registration.
Event Handling
| # | Rule | ✅ Do | ❌ Don't |
| 31 | Editor drop/paste | When the handler writes to the editor: return early on evt.defaultPrevented, then call evt.preventDefault() | Write to the editor without both checks; call preventDefault() in a handler that only observes (cancels Obsidian's insertion) |
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; in popouts, create on a parent element (containerEl.createEl('p')) or detached with activeDocument.createElement() | Use global document.createElement(); use activeWindow.createEl() — Window doesn't declare the helpers, so it fails tsc |
| 36 | Node.js modules | Load Node.js modules with import()/require() inside a Platform.isDesktop guard (no-nodejs-modules) | Static import of a Node built-in in a plugin that isn't isDesktopOnly |
| 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 | Replace the sample's "Dynalist Inc." holder with your name (validate-license) | Leave the sample plugin's Copyright (C) 2020-2025 by Dynalist Inc. line |
| Async | Use async/await; void fire-and-forget promises in void callbacks (obsidianmd/no-misused-promises) | Use Promise chains; disable the old @typescript-eslint/no-misused-promises name |
| 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 manualaddEventListener(and theactiveDocumentdrift bug)- Avoiding view references in plugin
- Not using plugin as component
- Proper leaf cleanup
Type Safety
- Using
instanceofinstead of type casting - Avoiding
anytype - Using
constandletovervar
UI/UX Standards
- Sentence case enforcement (TypeScript, JSON locale, TS/JS locale modules)
recommendedWithLocalesEnconfig 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
- Confirmation dialogs:
ConfirmationModal(1.13+), awaiting the user's choice,promptForDeletion()
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
:hasselector (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
- Upgrading from v0.5.0 (new findings, fewer false positives) and from v0.4.x (breaking changes)
- Scoping overrides with
files; test, tooling, and web worker file exemptions - Why
typescript-eslintrecommendedTypeChecked 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:
- Run ESLint —
npx eslint .from the project root usingeslint-plugin-obsidianmd(eslint srcskipsmanifest.json,LICENSE, andpackage.json); fix all errors AND warnings (warnings affect your Scorecard) - Validate manifest —
validate-manifestchecksnameanddescription(rules 2–5), andid(rule 1) only withcheckId: true; confirmversionandminAppVersionyourself - Check LICENSE —
validate-licensereports the sample's "Dynalist Inc." holder; put your own name there - Test on mobile — Verify no regex lookbehind, no
fetch(), and touch targets ≥ 44×44px (skip only if plugin is declared desktop-only) - Keyboard accessibility audit — Tab through all interactive elements; confirm focus indicators and ARIA labels are present
- Create GitHub Release — Tag must match
manifest.jsonversion; attachmain.js,manifest.json, andstyles.css(optional) - 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/recommendedTypeCheckedfor type-aware checks - New submissions get the scanner's strictest layer:
no-static-styles-assignment,no-unsupported-api,sample-names, and other rules are hard errors, and everyeslint-disableneeds a-- description - 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:
- Memory management: Are components and views properly managed?
- Type safety: Using
instanceofinstead of casts? - UI text: Is everything in sentence case?
- Command naming: No redundant words?
- File operations: Using preferred APIs?
- Mobile compatibility: No iOS-incompatible features?
- Sample code: Removed all boilerplate?
- Manifest: Correct version, valid structure?
- Accessibility: Keyboard navigation, ARIA labels, focus indicators?
- Testing: Can you use the plugin without a mouse?
- Touch targets: Are all interactive elements at least 44×44px?
- Focus styles: Using
:focus-visibleand proper CSS variables? - Settings: Using declarative
getSettingDefinitions()on 1.13+? All saved data co-located inplugin.settings? - Confirm dialogs: Using
ConfirmationModalorpromptForDeletion()instead of a custom confirm modal?
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.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

