Claude-code-plugins-plus obsidian-core-workflow-a
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-core-workflow-a" ~/.claude/skills/jeremylongshore-claude-code-plugins-plus-obsidian-core-workflow-a && rm -rf "$T"
manifest:
plugins/saas-packs/obsidian-pack/skills/obsidian-core-workflow-a/SKILL.mdsource content
Obsidian Core Workflow A: Create a Plugin from Scratch
Overview
Build a complete Obsidian plugin from an empty directory. By the end you will have a working plugin with a ribbon icon, command palette entries, a settings tab, and a production esbuild build. Every file is shown in full -- no stubs.
Prerequisites
- Node.js 18+ installed
- Obsidian desktop app installed
- A vault to test in (create a fresh vault at
if needed)~/ObsidianDev
Instructions
Step 1: Scaffold the project
set -euo pipefail PLUGIN_NAME="my-obsidian-plugin" mkdir -p "$PLUGIN_NAME/src" cd "$PLUGIN_NAME" # Initialize Node project npm init -y # Install Obsidian types and build tool npm install --save-dev obsidian@latest typescript@latest esbuild@latest \ @types/node@latest tslib@latest # TypeScript config cat > tsconfig.json << 'TSEOF' { "compilerOptions": { "baseUrl": ".", "inlineSourceMap": true, "inlineSources": true, "module": "ESNext", "target": "ES2018", "allowJs": true, "noImplicitAny": true, "moduleResolution": "node", "importHelpers": true, "isolatedModules": true, "strictNullChecks": true, "lib": ["DOM", "ES2018", "ES2021.String"] }, "include": ["src/**/*.ts"] } TSEOF echo "Scaffolding complete."
Step 2: Create manifest.json
Every Obsidian plugin needs a
manifest.json at the project root. This is what
Obsidian reads to register the plugin.
{ "id": "my-obsidian-plugin", "name": "My Obsidian Plugin", "version": "1.0.0", "minAppVersion": "1.0.0", "description": "A starter Obsidian plugin.", "author": "Your Name", "isDesktopOnly": false }
Step 3: Write the esbuild config
// esbuild.config.mjs import esbuild from "esbuild"; import process from "process"; const prod = process.argv[2] === "production"; const context = await esbuild.context({ entryPoints: ["src/main.ts"], bundle: true, external: [ "obsidian", "electron", "@codemirror/autocomplete", "@codemirror/collab", "@codemirror/commands", "@codemirror/language", "@codemirror/lint", "@codemirror/search", "@codemirror/state", "@codemirror/view", "@lezer/common", "@lezer/highlight", "@lezer/lr", ], format: "cjs", target: "es2018", logLevel: "info", sourcemap: prod ? false : "inline", treeShaking: true, outfile: "main.js", }); if (prod) { await context.rebuild(); process.exit(0); } else { await context.watch(); }
Step 4: Write main.ts -- the full plugin
This single file contains the Plugin subclass, a settings interface with defaults, a settings tab, and three commands.
// src/main.ts import { App, Editor, MarkdownView, Notice, Plugin, PluginSettingTab, Setting, } from "obsidian"; // ── Settings ──────────────────────────────────────────────────────── interface MyPluginSettings { greeting: string; showRibbon: boolean; } const DEFAULT_SETTINGS: MyPluginSettings = { greeting: "Hello from My Plugin!", showRibbon: true, }; // ── Plugin ────────────────────────────────────────────────────────── export default class MyPlugin extends Plugin { settings: MyPluginSettings; async onload() { await this.loadSettings(); // Ribbon icon -- shows a Notice when clicked if (this.settings.showRibbon) { this.addRibbonIcon("sparkles", "My Plugin: Greet", () => { new Notice(this.settings.greeting); }); } // Command: show greeting as Notice this.addCommand({ id: "show-greeting", name: "Show greeting", callback: () => { new Notice(this.settings.greeting); }, }); // Command: insert greeting at cursor (only available in editor) this.addCommand({ id: "insert-greeting", name: "Insert greeting at cursor", editorCallback: (editor: Editor, view: MarkdownView) => { editor.replaceSelection(this.settings.greeting); }, }); // Command: count words in current note this.addCommand({ id: "count-words", name: "Count words in current note", editorCallback: (editor: Editor) => { const text = editor.getValue(); const count = text.split(/\s+/).filter(Boolean).length; new Notice(`Word count: ${count}`); }, }); // Status bar item const statusEl = this.addStatusBarItem(); statusEl.setText("Plugin loaded"); // Settings tab this.addSettingTab(new MyPluginSettingTab(this.app, this)); console.log("MyPlugin loaded"); } onunload() { console.log("MyPlugin unloaded"); } async loadSettings() { this.settings = Object.assign( {}, DEFAULT_SETTINGS, await this.loadData() ); } async saveSettings() { await this.saveData(this.settings); } } // ── Settings Tab ──────────────────────────────────────────────────── class MyPluginSettingTab 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 from My Plugin!") .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.") .addToggle((toggle) => toggle .setValue(this.plugin.settings.showRibbon) .onChange(async (value) => { this.plugin.settings.showRibbon = value; await this.plugin.saveSettings(); new Notice("Reload plugin to apply ribbon change."); }) ); } }
Step 5: Add npm scripts and build
Add these scripts to
package.json:
{ "scripts": { "dev": "node esbuild.config.mjs", "build": "node esbuild.config.mjs production" } }
Build the plugin:
set -euo pipefail npm run build # Output: main.js at project root ls -la main.js manifest.json
Step 6: Install into your vault and test
set -euo pipefail VAULT="$HOME/ObsidianDev" PLUGIN_ID="my-obsidian-plugin" # Create plugin directory in vault mkdir -p "$VAULT/.obsidian/plugins/$PLUGIN_ID" # Copy build artifacts cp main.js manifest.json "$VAULT/.obsidian/plugins/$PLUGIN_ID/" echo "Plugin installed. Open Obsidian, enable it in Settings > Community plugins."
In Obsidian:
- Settings > Community plugins > Enable community plugins
- Find "My Obsidian Plugin" in the list, toggle it on
- Click the sparkles icon in the left ribbon
- Open command palette (Ctrl/Cmd+P), search "Show greeting"
- Open Settings > My Obsidian Plugin to change the greeting text
Output
A complete plugin directory containing:
-- plugin metadata Obsidian readsmanifest.json
-- Plugin subclass with commands, ribbon icon, settings tabsrc/main.ts
-- bundler with watch mode supportesbuild.config.mjs
-- production build outputmain.js
+package.json
-- standard Node/TS project filestsconfig.json
Error Handling
| Error | Cause | Fix |
|---|---|---|
| Missing dev dependency | |
| Plugin not in list | missing or invalid | Verify field matches folder name |
| Ribbon icon missing | Invalid icon name | Use a Lucide icon name (sparkles, file-text, search, etc.) |
| Settings not persisting | Forgot | Always await in onChange |
command greyed out | No active editor | Open a markdown note first |
| Build fails with external error | Forgot to externalize obsidian | Check array in esbuild config |
Examples
Minimal manifest.json for community submission:
{ "id": "my-obsidian-plugin", "name": "My Obsidian Plugin", "version": "1.0.0", "minAppVersion": "1.0.0", "description": "Does one useful thing.", "author": "Your Name", "authorUrl": "https://github.com/yourname", "isDesktopOnly": false }
Adding a hotkey-enabled command:
this.addCommand({ id: "toggle-sidebar", name: "Toggle custom sidebar", // Users can assign a hotkey in Settings > Hotkeys callback: () => this.toggleSidebar(), });
Resources
- Obsidian Sample Plugin -- official starter
- Obsidian Plugin API Reference
- Lucide Icons -- icon names for
addRibbonIcon - esbuild Documentation
Next Steps
- Add custom views and modals: see
obsidian-core-workflow-b - Set up hot-reload development: see
obsidian-local-dev-loop - Apply production patterns: see
obsidian-sdk-patterns