agentsclimarketplace

Obsidian hello world

Skill jeremylongshore/claude-code-plugins-plus-skills/plugins/saas-packs/obsidian-pack/skills/obsidian-hello-world

425 plugins, 2,810 skills, 200 agents for Claude Code. Open-source marketplace at tonsofskills.com with the ccpi CLI package manager.

Install
npx -y skills add jeremylongshore/claude-code-plugins-plus-skills --skill obsidian-hello-world

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

What its author says it does

Copied from the file, not written here

'Create a minimal working Obsidian plugin with commands, settings, modals, and ribbon icons.

The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.

SKILL.md

10.1 KB, as published. Nobody here has run it

Obsidian Hello World

Overview

Build a minimal working Obsidian plugin demonstrating the five core building blocks: commands (palette + editor + checkCallback), settings tab with typed config, ribbon icons, modals, and status bar. Every snippet uses real Obsidian API.

Prerequisites

  • Completed obsidian-install-auth setup (symlinked dev vault, npm run dev working)
  • Build pipeline producing main.js from src/main.ts

Instructions

Step 1: Define Typed Settings

// src/main.ts — top of file
import {
  App, Editor, MarkdownView, Modal, Notice,
  Plugin, PluginSettingTab, Setting, TFile
} from 'obsidian';

interface MyPluginSettings {
  greeting: string;
  showRibbon: boolean;
  dateFormat: string;
}

const DEFAULT_SETTINGS: MyPluginSettings = {
  greeting: 'Hello, Obsidian!',
  showRibbon: true,
  dateFormat: 'YYYY-MM-DD',
};

Step 2: Create the Plugin Class with Commands

export default class MyPlugin extends Plugin {
  settings: MyPluginSettings;

  async onload() {
    await this.loadSettings();

    // Ribbon icon — shows greeting as Notice
    if (this.settings.showRibbon) {
      this.addRibbonIcon('sparkles', 'My Plugin: Greet', () => {
        new Notice(this.settings.greeting);
      });
    }

    // Command: show greeting (available everywhere)
    this.addCommand({
      id: 'show-greeting',
      name: 'Show greeting',
      callback: () => new Notice(this.settings.greeting),
    });

    // Command: insert greeting at cursor (editor-only — greyed out when no editor is active)
    this.addCommand({
      id: 'insert-greeting',
      name: 'Insert greeting at cursor',
      editorCallback: (editor: Editor, view: MarkdownView) => {
        editor.replaceSelection(this.settings.greeting);
      },
    });

    // Command: word count with checkCallback (conditionally available)
    this.addCommand({
      id: 'count-words',
      name: 'Count words in current note',
      checkCallback: (checking: boolean) => {
        const view = this.app.workspace.getActiveViewOfType(MarkdownView);
        if (view) {
          if (!checking) {
            const text = view.editor.getValue();
            const count = text.split(/\s+/).filter(Boolean).length;
            new Notice(`Word count: ${count}`);
          }
          return true; // command is available
        }
        return false; // hide from palette when no editor
      },
    });

    // Command: open modal dialog
    this.addCommand({
      id: 'show-greeting-modal',
      name: 'Show greeting modal',
      callback: () => new GreetingModal(this.app, this.settings.greeting).open(),
    });

    // Command: insert today's date
    this.addCommand({
      id: 'insert-date',
      name: 'Insert today\'s date',
      editorCallback: (editor: Editor) => {
        const today = new Date().toISOString().slice(0, 10);
        editor.replaceSelection(today);
      },
    });

    // Status bar — persistent widget at bottom
    const statusEl = this.addStatusBarItem();
    statusEl.setText('Plugin loaded');

    // Update status bar when active file changes
    this.registerEvent(
      this.app.workspace.on('active-leaf-change', () => {
        const view = this.app.workspace.getActiveViewOfType(MarkdownView);
        if (view) {
          const count = view.editor.getValue().split(/\s+/).filter(Boolean).length;
          statusEl.setText(`Words: ${count}`);
        } else {
          statusEl.setText('No editor');
        }
      })
    );

    // Settings tab
    this.addSettingTab(new MySettingTab(this.app, this));
    console.log(`[${this.manifest.id}] loaded`);
  }

  onunload() {
    console.log(`[${this.manifest.id}] unloaded`);
  }

  async loadSettings() {
    this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
  }

  async saveSettings() {
    await this.saveData(this.settings);
  }
}

Step 3: Create Settings Tab

class MySettingTab extends PluginSettingTab {
  plugin: MyPlugin;

  constructor(app: App, plugin: MyPlugin) {
    super(app, plugin);
    this.plugin = plugin;
  }

  display(): void {
    const { containerEl } = this;
    containerEl.empty();

    new Setting(containerEl)
      .setName('Greeting message')
      .setDesc('Text shown by the greet command and ribbon icon.')
      .addText(text => text
        .setPlaceholder('Hello, Obsidian!')
        .setValue(this.plugin.settings.greeting)
        .onChange(async (value) => {
          this.plugin.settings.greeting = value;
          await this.plugin.saveSettings();
        }));

    new Setting(containerEl)
      .setName('Show ribbon icon')
      .setDesc('Toggle the sparkles icon in the left ribbon. Reload plugin to apply.')
      .addToggle(toggle => toggle
        .setValue(this.plugin.settings.showRibbon)
        .onChange(async (value) => {
          this.plugin.settings.showRibbon = value;
          await this.plugin.saveSettings();
        }));

    new Setting(containerEl)
      .setName('Date format')
      .setDesc('Format for the Insert Date command.')
      .addDropdown(dropdown => dropdown
        .addOption('YYYY-MM-DD', '2026-03-22')
        .addOption('MM/DD/YYYY', '03/22/2026')
        .addOption('DD.MM.YYYY', '22.03.2026')
        .setValue(this.plugin.settings.dateFormat)
        .onChange(async (value) => {
          this.plugin.settings.dateFormat = value;
          await this.plugin.saveSettings();
        }));
  }
}

Step 4: Create a Modal

class GreetingModal extends Modal {
  message: string;

  constructor(app: App, message: string) {
    super(app);
    this.message = message;
  }

  onOpen() {
    const { contentEl } = this;
    contentEl.createEl('h2', { text: this.message });
    contentEl.createEl('p', { text: 'This is a modal dialog from your plugin.' });

    // Add a button that does something
    const btn = contentEl.createEl('button', { text: 'Count vault files' });
    btn.addEventListener('click', () => {
      const count = this.app.vault.getMarkdownFiles().length;
      contentEl.createEl('p', { text: `Your vault has ${count} markdown files.` });
    });
  }

  onClose() {
    this.contentEl.empty();
  }
}

Step 5: Build and Test

set -euo pipefail
npm run build

# In Obsidian:
# 1. Settings > Community plugins > Enable your plugin
# 2. Click the sparkles icon in the ribbon
# 3. Ctrl+P > "Show greeting"
# 4. Ctrl+P > "Count words in current note" (open a .md file first)
# 5. Ctrl+P > "Show greeting modal"
# 6. Settings > My Plugin > change the greeting
# 7. Check the status bar at bottom for word count

Step 6: Listen to Vault Events

// Add to onload() — react to file changes
this.registerEvent(
  this.app.workspace.on('file-open', (file: TFile | null) => {
    if (file) {
      console.log(`[${this.manifest.id}] Opened: ${file.path}`);
    }
  })
);

// Track file modifications (debounce for production — see obsidian-rate-limits)
this.registerEvent(
  this.app.vault.on('create', (file) => {
    if (file instanceof TFile) {
      new Notice(`New file: ${file.basename}`);
    }
  })
);

Output

  • Working plugin with:
    • Three command types: callback, editorCallback, checkCallback
    • Settings tab with text, toggle, and dropdown controls
    • Ribbon icon with click handler
    • Modal dialog with interactive button
    • Status bar widget with live word count
    • Event listeners for file-open and file-create

Error Handling

ErrorCauseSolution
Plugin not loadingBuild errors or bad manifestCheck console (Ctrl+Shift+I) for red errors
Settings not savingMissing await on saveDataAlways await this.saveSettings() in onChange
Command greyed outeditorCallback needs active editorOpen a markdown note, or use callback instead
Ribbon icon missingInvalid icon nameUse Lucide icon names: sparkles, file-text, search
Status bar not updatingEvent not registeredWrap in this.registerEvent() for auto-cleanup
Settings reset on restartForgot saveData callloadData returns null on first run — Object.assign handles this

Examples

Available Lucide Icon Names

Obsidian uses Lucide icons. Common examples:

  • file-text, folder, search, settings, star
  • heart, bookmark, tag, link, external-link
  • edit, trash-2, copy, clipboard, check
  • dice, bot, sparkles, wand, calendar
  • bar-chart-2, globe, download, upload

Command Types Summary

TypeWhen AvailableUse Case
callbackAlwaysNon-editor commands (open modal, toggle feature)
editorCallbackWhen editor is activeInsert text, transform selection
checkCallbackConditionallyShow/hide based on context

Register a Hotkey-Ready Command

// Users assign hotkeys in Settings > Hotkeys
this.addCommand({
  id: 'toggle-feature',
  name: 'Toggle my feature',
  callback: () => this.toggleFeature(),
});

Resources

Next Steps

  • Set up hot-reload development: obsidian-local-dev-loop
  • Build advanced UI (views, fuzzy search, context menus): obsidian-core-workflow-b
  • Apply production patterns: obsidian-sdk-patterns

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.