All skills
gapmiss avatar

/obsidian

@ff97cb0

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.

referencememory-management.md

≈1.6k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Memory Management & Lifecycle

Proper memory management is critical in Obsidian plugins to prevent memory leaks and ensure smooth performance.

Use registerEvent() and addCommand() for Cleanup

Rule: Official guidelines

✅ CORRECT:

async onload() {
  // These are automatically cleaned up on unload
  this.registerEvent(
    this.app.workspace.on('file-open', (file) => {
      // Handle file open
    })
  );

  this.addCommand({
    id: 'my-command',
    name: 'My command',
    callback: () => { }
  });

  // For DOM events, use registerDomEvent
  this.registerDomEvent(document, 'click', (evt) => {
    // Handle click
  });

  // For intervals, use registerInterval
  this.registerInterval(
    window.setInterval(() => {
      // Do something periodically
    }, 5000)
  );
}

onunload() {
  // No manual cleanup needed!
  // Obsidian handles it automatically
}

Rationale: Use registerEvent(), addCommand(), registerDomEvent(), and registerInterval() for automatic cleanup when the plugin unloads. This prevents memory leaks.


Use registerDomEvent() Instead of Manual addEventListener

Rule: Best practice (NOT caught by the linter)

The linter enforces activeDocument over document and registerEvent() for Obsidian events, but it does not flag manual addEventListener calls paired with manual cleanup. That pattern hides a subtle leak: activeDocument and activeWindow are dynamic getters that follow window focus, so calling them at setup and again at cleanup can return different documents (e.g., focus moved to a popout or the settings window in between).

❌ INCORRECT (leaks when focus changes between setup and cleanup):

export default class MyPlugin extends Plugin {
  private onClick = (evt: MouseEvent) => { /* ... */ };

  onload() {
    // Registers on whichever document is focused NOW
    activeDocument.addEventListener('click', this.onClick);
  }

  onunload() {
    // May be a DIFFERENT document — the original listener is never removed
    activeDocument.removeEventListener('click', this.onClick);
  }
}

✅ CORRECT:

onload() {
  // Target is captured once at registration; removal is automatic on unload
  this.registerDomEvent(activeDocument, 'click', (evt) => {
    // Handle click
  });
}

If manual management is truly unavoidable, capture the document once and use the same reference for both calls:

const doc = activeDocument;
doc.addEventListener('click', this.onClick);
// later, in the cleanup path:
doc.removeEventListener('click', this.onClick);

Scope Listeners to the Owning Component

registerDomEvent() is a Component method, so views and modals have it too. Register on the component whose lifecycle matches the listener — not always the plugin:

export class MyView extends ItemView {
  async onOpen() {
    // Cleaned up when the view unloads, not when the plugin unloads
    this.registerDomEvent(this.containerEl.ownerDocument, 'mousemove', (evt) => {
      // ...
    });
  }
}

Helper classes that can't extend Component should receive the owning view/component and call its registerDomEvent() instead of managing cleanup themselves.

Covering Popout Windows

A listener registered on one document never fires in other windows. For app-wide listeners, register on each window as it opens:

this.registerDomEvent(document, 'click', this.onClick);  // main window
this.registerEvent(
  this.app.workspace.on('window-open', (workspaceWindow) => {
    this.registerDomEvent(workspaceWindow.doc, 'click', this.onClick);
  })
);

When plain addEventListener is fine: listeners on short-lived elements your own code creates (e.g., a button inside a settings tab) are garbage-collected with the element. The managed pattern is mandatory for long-lived targets — document, window, workspace containers — and anything that outlives the registering code.

Rationale: registerDomEvent() captures the event target at registration time and removes the listener automatically when the owning component unloads. This eliminates both the manual-cleanup burden and the activeDocument drift bug.


Don't Store View References in Plugin

Rule: obsidianmd/no-view-references-in-plugin

❌ INCORRECT:

this.registerView(VIEW_TYPE, (leaf) => {
  this.view = new MyCustomView(leaf);  // Memory leak!
  return this.view;
});

✅ CORRECT:

this.registerView(VIEW_TYPE, (leaf) => {
  return new MyCustomView(leaf);  // Create and return directly
});

Rationale: Storing view instances as plugin properties prevents proper cleanup and causes memory leaks.


Don't Use Plugin as Component

Rule: obsidianmd/no-plugin-as-component

❌ INCORRECT:

// Passing plugin instance
MarkdownRenderer.render(app, markdown, el, sourcePath, this);

// Inline new Component()
MarkdownRenderer.render(app, markdown, el, sourcePath, new Component());

✅ CORRECT:

const component = new Component();
MarkdownRenderer.render(app, markdown, el, sourcePath, component);
// Later: component.unload() when done

Rationale: Plugin lifecycle is too long, causing memory leaks. Components must be stored to call unload().


Don't Detach Leaves in onunload

Rule: obsidianmd/detach-leaves (auto-fixable)

❌ INCORRECT:

onunload() {
  this.app.workspace.detachLeavesOfType(VIEW_TYPE);
}

✅ CORRECT:

onunload() {
  // Let Obsidian handle leaf cleanup automatically
}

Rationale: Obsidian handles leaf cleanup automatically. Manual detachment can cause issues.


Use getActiveLeavesOfType() Instead of Storing Views

Rule: Official guidelines (relates to no-view-references-in-plugin)

❌ INCORRECT:

// Don't store view references
this.customViews = [];

✅ CORRECT:

// Get views when needed
const views = this.app.workspace.getLeavesOfType(VIEW_TYPE)
  .map(leaf => leaf.view as MyCustomView);

Rationale: Don't store references to custom views. Use getLeavesOfType() or getActiveLeavesOfType() to access them when needed.

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