agentleFS
gapmissgapmiss/obsidian-plugin-skill/.agents/skills/obsidian/SKILL.md

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

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-aiSkill

    taskflow-inbox-triage

    Also in 6 projects that use this

  • molt-botSkill

    discord-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-botSkill

    openclaw-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

  • clawdbotSkill

    openclaw-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

  • clawdbotSkill

    plugin-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

  • emanueleieloSkill

    sonos

    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

    1. moltbot/clawdbot★ 394ksame name
    2. steipete/warelay★ 394ksame name
    3. steipete/clawdis★ 394ksame name
    4. moltbot/moltbot★ 394ksame name
    5. clawdbot/clawdbot★ 394ksame name
    6. openclaw/openclaw★ 394ksame name
    7. molt-bot/clawdbot★ 391ksame name
    8. NousResearch/hermes-agent★ 262ksame name
    9. spinabot/brigade★ 11ksame name
    10. 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
  1. Distinctive:

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

    line 11
  2. Very specific:

    All four settings-tab rules ship as warn in recommended.

    line 67
  3. Distinctive:

    Not using plugin as component

    line 158
  4. Distinctive:

    Before submitting a plugin, follow this sequence:

    line 233
  5. Distinctive:

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

    line 243
  6. Very specific:

    Use typescript-eslint/recommendedTypeChecked for type-aware checks

    line 254
  7. Distinctive:

    Add GitHub artifact attestation to releases

    line 256
  8. Distinctive:

    Use this checklist for code review and implementation:

    line 264
  9. Distinctive:

    Command naming: No redundant words?

    line 269
  10. Distinctive:

    Mobile compatibility: No iOS-incompatible features?

    line 271
  11. Distinctive:

    When helping with Obsidian plugin development, proactively apply these rules and suggest improvements based on these guidelines.

    line 368

“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.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

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.5.1)

Submission & Naming

#Rule✅ Do❌ Don't
1Plugin IDOmit "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
2Plugin nameOmit "Obsidian" and "Plugin"Include "Obsidian" or "Plugin" anywhere in the name
3Plugin nameAvoid parts of "Obsidian"Use "obsi", "sidian", or a leading "ob"
4DescriptionSay what it does; don't repeat or start with the nameUse "Obsidian", "This plugin", "A plugin that", or start with the name
5Description10–250 chars, ending with ., !, or ?End with ) or no punctuation

Memory & Lifecycle

#Rule✅ Do❌ Don't
6Event cleanupUse registerEvent() for automatic cleanupRegister events without cleanup
6aDOM eventsUse registerDomEvent() on the plugin or owning componentPair addEventListener with manual removeEventListener cleanup
7View referencesReturn views/components directlyStore view references in plugin properties or pass plugin as component to MarkdownRenderer
8Leaf detachmentLet Obsidian handle leaf cleanupCall detachLeavesOfType() in onunload

Type Safety

#Rule✅ Do❌ Don't
9TFile/TFolderUse instanceof for type checkingCast to TFile/TFolder; use any; use var
10DOM instanceofUse .instanceOf(T) for DOM Nodes/UIEvents; e.targetNode?.instanceOf(T) for event targetsUse instanceof for cross-window DOM checks, including e.target instanceof HTMLElement

UI/UX

#Rule✅ Do❌ Don't
11UI textSentence case — "Advanced settings"; use acronyms/brands options for proper namesTitle Case — "Advanced Settings"
12JSON localeSentence case in JSON locale files (recommendedWithLocalesEn)Title case in locale JSON
13TS/JS localeSentence case in TS/JS locale modulesTitle case in locale modules
14Command namesOmit "command" as a redundant label; "Run command" or copy-command-id (what the command acts on) is fineAdd "command" as a label — "Command: open note", "Open settings command"
15Command IDsOmit plugin ID/name from command IDs/namesDuplicate plugin ID in command IDs
16HotkeysNo default hotkeysSet default hotkeys
17Settings headingsUse .setHeading()Create manual HTML headings; use "General", "settings", or plugin name in headings

Note (v0.5.0): ui/sentence-case is off in recommended (and the scanner doesn't run it) — sentence case is still the convention, so opt in with "obsidianmd/ui/sentence-case": ["warn", { enforceCamelCaseLower: true }]. recommendedWithLocalesEn checks 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
17asettings-tab/require-displayKeep display() when minAppVersion < 1.13.0Ship declarative-only settings that render nothing on older Obsidian
17bsettings-tab/prefer-setting-definitionsImplement getSettingDefinitions() on every PluginSettingTabRely on display() alone — settings won't appear in 1.13+ global search
17csettings-tab/prefer-update-over-displayCall this.update() to re-render declarative settingsCall this.display() — it's bypassed when definitions are non-empty
17dsettings-tab/no-deprecated-displayDelete display() once minAppVersion >= 1.13.0 and definitions existLeave a dead display() behind (auto-fixable)
—Settings dataKeep all persisted data inside plugin.settingsStore 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
18Active file editsUse Editor APIUse Vault.modify() for active file edits
19Background file modsUse Vault.process()Use Vault.modify() for background modifications
20File deletionUse FileManager.trashFile() (1.6.6+), which follows the user's "Deleted files" settingUse Vault.trash() or Vault.delete() with a fixed destination
21File lookupUse Vault.getAbstractFileByPath()Iterate all files with Vault.getFiles().find()
22User pathsUse normalizePath()Hardcode .obsidian path; use raw user paths
23OS detectionUse Platform APIUse navigator.platform/userAgent
24Network requestsUse requestUrl()Use fetch()
24aResponse typingParse response.text and validate/cast once at the API boundaryPass response.json (typed any) into plugin code
25LoggingMinimize console logging; none in onload/onunload in productionUse console.log in onload/onunload
26Input suggestUse built-in AbstractInputSuggestCopy Liam's TextInputSuggest implementation
26aConfirm dialogsUse built-in ConfirmationModal (1.13.0+); use FileManager.promptForDeletion() to confirm and delete filesHand-build a Modal subclass with its own confirm/cancel button row
27API compatibilityCheck 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
28Language detectionUse Obsidian's getLanguage()Use localStorage.getItem('language') or i18next-browser-languagedetector

Popout Window Compatibility

#Rule✅ Do❌ Don't
29Document/WindowUse activeDocument and activeWindowUse global document and window
29aWindow-wide stateAdd 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)
29bGetter captureCapture activeDocument in a variable when the same document is needed laterCall activeDocument at setup and again at cleanup — it follows focus and may return different documents
30TimersUse window.setTimeout(), window.setInterval(), etc. (exception to rule 29); activeWindow.requestAnimationFrame() is fine for a popout's framesUse bare setTimeout()/setInterval(), or activeWindow.setTimeout() — prefer-window-timers flags both
31Main workspace UIUse this.app.workspace.containerEl.ownerDocument from settingsUse activeDocument to update main workspace from settings window

Note (v0.5.1): prefer-active-doc warns in recommended. It reports the global document and offers an editor suggestion, not an autofix. Changes to document.body/documentElement/head get their own message and no suggestion (rule 29a). document in a listener on the main window's document/window, as the fallback in activeDocument ?? 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. 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
31Editor drop/pasteWhen 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
32CSS variablesUse Obsidian CSS variables for all stylingHardcode colors, sizes, or spacing
33CSS scopeScope CSS to plugin containersUse broad CSS selectors
34Style elementsUse styles.css file (no-forbidden-elements)Create <link> or <style> elements; assign styles via JavaScript
34a!importantIncrease selector specificity or use CSS variablesUse !important — overrides user themes/snippets
34b:has selectorToggle classes from TypeScript when conditions changeUse :has — causes broad selector invalidation and performance issues

Security & Compatibility

#Rule✅ Do❌ Don't
35DOM creationUse 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
36Node.js modulesLoad 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
37iOS compatAvoid regex lookbehind (iOS < 16.4 incompatibility)Use regex lookbehind

Accessibility (MANDATORY)

#Rule✅ Do❌ Don't
38Keyboard accessMake all interactive elements keyboard accessible; Tab through all elementsCreate inaccessible interactive elements
39ARIA labelsProvide ARIA labels for icon buttons; use data-tooltip-position for tooltipsUse icon buttons without ARIA labels
40Focus indicatorsUse :focus-visible with Obsidian CSS variables; touch targets ≥ 44×44pxRemove focus indicators; make touch targets < 44×44px

Code Quality

Rule✅ Do❌ Don't
Sample codeRemove all sample/template codeKeep class names like MyPlugin, SampleModal
Object.assignObject.assign({}, defaults, overrides) (object-assign)Object.assign(defaultsVar, other) — mutates defaults
LICENSEReplace the sample's "Dynalist Inc." holder with your name (validate-license)Leave the sample plugin's Copyright (C) 2020-2025 by Dynalist Inc. line
AsyncUse 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 packagesReplace 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
  • 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 :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
  • 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-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 . from the project root using eslint-plugin-obsidianmd (eslint src skips manifest.json, LICENSE, and package.json); fix all errors AND warnings (warnings affect your Scorecard)
  2. Validate manifest — validate-manifest checks name and description (rules 2–5), and id (rule 1) only with checkId: true; confirm version and minAppVersion yourself
  3. Check LICENSE — validate-license reports the sample's "Dynalist Inc." holder; put your own name there
  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
  • New submissions get the scanner's strictest layer: no-static-styles-assignment, no-unsupported-api, sample-names, and other rules are hard errors, and every eslint-disable needs 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:

  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?
  14. Confirm dialogs: Using ConfirmationModal or promptForDeletion() 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.

Download now (ZIP)

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
What did you use it for? e.g. Reviewing pull requests in a Rails app

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.