plugins/obsidian-development/skills/obsidian-plugin-development/SKILL.md
Ensures compliance with Obsidian's automated plugin review (community.obsidian.md), eslint-plugin-obsidianmd rules, and official Obsidian plugin guidelines. TRIGGER WHEN: writing, reviewing, or fixing Obsidian community plugin code DO NOT TRIGGER WHEN: the task is outside the specific scope of this component.
npx skillsauth add acaprino/anvil-toolset obsidian-plugin-developmentInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Write Obsidian plugin code that passes Obsidian's automated plugin review on first submission. Since May 2026, plugins are submitted and reviewed through the Community hub at community.obsidian.md (the old PR workflow to obsidianmd/obsidian-releases, gated by ObsidianReviewBot, is retired); every GitHub release is scanned automatically. All rules below are enforced via eslint-plugin-obsidianmd and @typescript-eslint. Violations labeled "Required" block approval, and a failing version of an already-listed plugin is removed from directory search within 24 hours.
Every user-visible string: sentence case only.
// NO
'Block Settings'
'Add Block'
'Recent Files'
// YES
'Block settings'
'Add block'
'Recent files'
Applies to: Setting.setName(), Setting.setDesc(), createEl() text, button labels, modal titles, notices, menu items, tooltips. Proper nouns and acronyms (e.g. "API", "GitHub", "Obsidian") keep their casing.
Never assign element.style.* directly. Use CSS classes.
// NO
el.style.display = 'flex';
el.style.transform = 'scale(0.9)';
el.style.opacity = '0';
// YES -- use CSS classes
el.addClass('hp-flex-container');
el.toggleClass('hp-scaled', true);
el.toggleClass('hp-hidden', true);
// For dynamic CSS custom properties, use setCssProps or setCssStyles:
el.setCssStyles({ '--my-var': value });
Flagged properties include: display, transform, opacity, width, height, margin, padding, cursor, fontSize, fontFamily, flexDirection, alignItems, flexShrink, borderRadius, backdropFilter, background, borderWidth, borderStyle, transition, gridTemplateRows, transformOrigin, and all others.
Don't as Type when it doesn't change the type.
// NO -- assertion is redundant with ?? fallback
draft.url as string ?? ''
draft.showDate as boolean ?? true
// YES
String(draft.url ?? '')
Boolean(draft.showDate ?? true)
// or just
(draft.url ?? '') as string // assertion AFTER coalescing
Every Promise must be: awaited, .catch()ed, .then() with rejection handler, or voided.
// NO
someAsyncFn();
this.app.vault.read(file).then(text => { ... });
// YES
await someAsyncFn();
void someAsyncFn();
this.app.vault.read(file).then(text => { ... }, err => console.error(err));
this.app.vault.read(file).then(text => { ... }).catch(console.error);
Remove async from methods that don't use await.
// NO
async onOpen() { this.render(); }
// YES
onOpen() { this.render(); }
Don't return a Promise in callbacks expecting void.
// NO -- event callback expects void
this.registerEvent(this.app.vault.on('modify', async (file) => {
await this.reload();
}));
// YES
this.registerEvent(this.app.vault.on('modify', (file) => {
void this.reload();
}));
Ensure values won't stringify as [object Object].
// NO -- if draft is Record<string,unknown>, draft.mode could be an object
`Value: ${draft.mode ?? 'default'}`
// YES
`Value: ${String(draft.mode ?? 'default')}`
Don't create HTML headings. Use Setting.setHeading().
// NO
contentEl.createEl('h2', { text: 'My settings' });
// YES
new Setting(contentEl).setName('My settings').setHeading();
Obsidian handles leaf cleanup. Detaching resets user's layout.
// NO
onunload() {
this.app.workspace.detachLeavesOfType(VIEW_TYPE);
}
// YES
onunload() {
// Obsidian cleans up leaves automatically
}
Use instanceof instead of type casting.
// NO
const file = abstractFile as TFile;
// YES
if (abstractFile instanceof TFile) { ... }
Don't create <style> or <link> elements dynamically.
Don't pass this (plugin) to MarkdownRenderer.render(). Use a Component instance.
// NO
MarkdownRenderer.render(this.app, md, el, '', this);
// YES -- use a Component subclass or this view/block
MarkdownRenderer.render(this.app, md, el, '', this.component);
Don't store view references in plugin properties (memory leak).
Don't hardcode .obsidian. Use this.app.vault.configDir.
Use Platform API, not navigator.userAgent.
// NO
if (navigator.userAgent.includes('Mac')) { ... }
// YES
import { Platform } from 'obsidian';
if (Platform.isMacOS) { ... }
Lookbehinds break on some iOS versions. Avoid unless isDesktopOnly: true.
FileManager.trashFile() instead of Vault.trash()/Vault.delete()getAbstractFileByPath()normalizePath() for user-provided pathsRemove MyPlugin, SampleModal, template code from obsidian-sample-plugin.
Don't use Object.assign(this.settings, data) to mutate defaults.
manifest.json must have valid structureLICENSE must have correct copyright holder and current year. ? ! )Use the popout-safe globals. Bare document and global timers target the main window only and break in popout windows.
// NO
document.body.appendChild(el);
const t = setTimeout(cb, 500);
clearTimeout(t);
globalThis.myFlag = true;
// YES
activeDocument.body.appendChild(el);
const t = activeWindow.setTimeout(cb, 500);
activeWindow.clearTimeout(t);
Rules: obsidianmd/prefer-active-doc (warn), obsidianmd/prefer-window-timers (error, also covers setInterval and requestAnimationFrame), obsidianmd/no-global-this (error).
Every API the code calls must exist in the Obsidian version declared as minAppVersion. obsidianmd/no-unsupported-api cross-checks usage against the manifest.
// NO -- revealLeaf needs v1.7.2 but manifest says minAppVersion 1.5.0
await this.app.workspace.revealLeaf(leaf);
// YES -- raise minAppVersion to 1.7.2, or gate the call behind a version check
// NO
document.createElement('iframe');
document.createDocumentFragment();
el.createEl('span', { text: file.path });
// YES
createEl('iframe');
createFragment();
el.createSpan({ text: file.path });
Rule obsidianmd/prefer-create-el. The review platform enforces it, but the rule is not yet in the npm release, so local ESLint misses it: check these patterns manually.
Every eslint-disable directive needs a description, and some rules cannot be disabled at all (obsidianmd/no-static-styles-assignment, obsidianmd/ui/sentence-case).
// NO
// eslint-disable-next-line obsidianmd/prefer-active-doc
// YES
// eslint-disable-next-line obsidianmd/prefer-active-doc -- main-window-only startup code
Enforced platform-side via @eslint-community/eslint-comments/require-description and no-restricted-disable; add the same plugin locally to catch them before release.
AbstractInputSuggest instead of copied TextInputSuggestlocalStorage or sessionStorage. Use Plugin.loadData()/saveData() for plugin data (stored in the plugin's data.json, synced with the vault). For device-specific per-vault values (window positions, device caches), use App.loadLocalStorage()/App.saveLocalStorage(), which scope keys to the current vault. Raw localStorage is shared across all vaults on the device and never synced; sessionStorage does not survive a restart.| Practice | Details |
|----------|---------|
| No innerHTML/outerHTML | Use createEl, setText, sanitizeHTMLToDom |
| Use requestUrl() | Instead of fetch() for network requests |
| CSS variables for theming | --background-secondary, --text-muted, etc. |
| Scope CSS | All plugin CSS scoped to plugin containers |
| Accessibility | aria-label on icon buttons, keyboard nav, focus indicators |
| Touch targets | Min 44x44px on mobile |
| Auto-cleanup | registerEvent(), registerInterval(), register() |
| No production logging | No console.log in onload()/onunload() |
See references/obsidian-api-reference.md in this skill directory for a condensed TypeScript API reference covering all key classes: Plugin, App, Vault, Workspace, MetadataCache, FileManager, Component, View, Modal, Setting, Menu, MarkdownRenderer, Platform, DOM helpers, and more.
For the full type definitions, read node_modules/obsidian/obsidian.d.ts in the project.
npm install --save-dev eslint-plugin-obsidianmd @eslint-community/eslint-plugin-eslint-comments
Configure ESLint with the obsidianmd recommended config plus the eslint-comments rules (require-description, no-restricted-disable). Lint src/ AND package.json (the dependency rules only fire when package.json is linted). Run before publishing a release: the platform scans every GitHub release automatically.
| Mistake | Fix |
|---------|-----|
| Title Case in UI text | Sentence case everything |
| el.style.display = 'none' | el.addClass('hp-hidden') |
| as string ?? '' | String(x ?? '') |
| createEl('h2', ...) in modal | new Setting(el).setName(...).setHeading() |
| Async onOpen without await | Remove async keyword |
| Unhandled promise | Add void, await, or .catch() |
| detachLeavesOfType in onunload | Remove -- Obsidian handles it |
| abstractFile as TFile | if (x instanceof TFile) |
| setTimeout(cb, ms) | activeWindow.setTimeout(cb, ms) |
| document.body | activeDocument.body |
| el.createEl('span', {...}) | el.createSpan({...}) |
| Bare // eslint-disable-next-line | Append -- reason description |
| New API with old minAppVersion | Raise minAppVersion to the API's version |
| localStorage.setItem('key', v) | this.app.saveLocalStorage('key', v) or this.saveData(...) |
tools
Master memory forensics techniques including memory acquisition, process analysis, and artifact extraction using Volatility and related tools. Use when analyzing memory dumps, investigating incidents, or performing malware analysis from RAM captures.
development
Master binary analysis patterns including disassembly, decompilation, control flow analysis, and code pattern recognition. Use when analyzing executables, understanding compiled code, or performing static analysis on binaries.
development
Idiomatic Kotlin implementation patterns: coroutines and structured concurrency, Flow / StateFlow / SharedFlow, Kotlin Multiplatform (KMP) shared-code architecture, Jetpack Compose UI, Ktor server with JWT auth and Exposed, and type-safe DSL design (lambdas with receivers, delegated properties, inline reified, value classes). TRIGGER WHEN: building, writing, or reviewing Kotlin code using coroutines / Flow / suspend functions, expect/actual, Compose composables / ViewModels, Ktor routing, sealed-class state modeling, scope functions, or DSL builders. DO NOT TRIGGER WHEN: libGDX game work (use libgdx-development), Android Java without Kotlin, or pure JVM tuning unrelated to Kotlin language features.
tools
Strategic website planning skill that conducts structured client discovery, produces professional deliverables (website brief, sitemap, design direction, content strategy), and orchestrates frontend-design, frontend-layout, seo-specialist, and content-marketer agents automatically. TRIGGER WHEN: planning a new website or redesign before any code is written. DO NOT TRIGGER WHEN: the task is outside the specific scope of this component.