Claude-code-plugins-plus-skills obsidian-hello-world
install
source · Clone the upstream repo
git clone https://github.com/jeremylongshore/claude-code-plugins-plus-skills
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/jeremylongshore/claude-code-plugins-plus-skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/plugins/saas-packs/obsidian-pack/skills/obsidian-hello-world" ~/.claude/skills/jeremylongshore-claude-code-plugins-plus-skills-obsidian-hello-world && rm -rf "$T"
manifest:
plugins/saas-packs/obsidian-pack/skills/obsidian-hello-world/SKILL.mdsource content
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
setup (symlinked dev vault,obsidian-install-auth
working)npm run dev - Build pipeline producing
frommain.jssrc/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
,editorCallbackcheckCallback - 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
- Three command types:
Error Handling
| Error | Cause | Solution |
|---|---|---|
| Plugin not loading | Build errors or bad manifest | Check console (Ctrl+Shift+I) for red errors |
| Settings not saving | Missing on | Always in |
| Command greyed out | needs active editor | Open a markdown note, or use instead |
| Ribbon icon missing | Invalid icon name | Use Lucide icon names: , , |
| Status bar not updating | Event not registered | Wrap in for auto-cleanup |
| Settings reset on restart | Forgot call | returns null on first run — handles this |
Examples
Available Lucide Icon Names
Obsidian uses Lucide icons. Common examples:
,file-text
,folder
,search
,settingsstar
,heart
,bookmark
,tag
,linkexternal-link
,edit
,trash-2
,copy
,clipboardcheck
,dice
,bot
,sparkles
,wandcalendar
,bar-chart-2
,globe
,downloadupload
Command Types Summary
| Type | When Available | Use Case |
|---|---|---|
| Always | Non-editor commands (open modal, toggle feature) |
| When editor is active | Insert text, transform selection |
| Conditionally | Show/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
- Obsidian Plugin API
- Plugin Development Workflow
- Lucide Icons — icon names for
addRibbonIcon - Obsidian Hub — community knowledge base
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