obsidian-plugin-development

Build, modify, and debug Obsidian plugins using the TypeScript API. Use this skill when working with Obsidian plugin source code, the obsidian npm package, plugin UI (views, modals, settings, commands, ribbons), vault file operations, editor manipulation, workspace management, metadata cache, events, markdown rendering, or the Obsidian CLI. Covers plugin lifecycle, best practices, common patterns, and the full TypeScript API surface.

290 stars

Best use case

obsidian-plugin-development is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

Build, modify, and debug Obsidian plugins using the TypeScript API. Use this skill when working with Obsidian plugin source code, the obsidian npm package, plugin UI (views, modals, settings, commands, ribbons), vault file operations, editor manipulation, workspace management, metadata cache, events, markdown rendering, or the Obsidian CLI. Covers plugin lifecycle, best practices, common patterns, and the full TypeScript API surface.

Teams using obsidian-plugin-development should expect a more consistent output, faster repeated execution, less prompt rewriting.

When to use this skill

  • You want a reusable workflow that can be run more than once with consistent structure.

When not to use this skill

  • You only need a quick one-off answer and do not need a reusable workflow.
  • You cannot install or maintain the underlying files, dependencies, or repository context.

Installation

Claude Code / Cursor / Codex

$curl -o ~/.claude/skills/obsidian-plugin-development/SKILL.md --create-dirs "https://raw.githubusercontent.com/allenhutchison/obsidian-gemini/main/.agents/skills/obsidian-plugin-development/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/obsidian-plugin-development/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How obsidian-plugin-development Compares

Feature / Agentobsidian-plugin-developmentStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

Build, modify, and debug Obsidian plugins using the TypeScript API. Use this skill when working with Obsidian plugin source code, the obsidian npm package, plugin UI (views, modals, settings, commands, ribbons), vault file operations, editor manipulation, workspace management, metadata cache, events, markdown rendering, or the Obsidian CLI. Covers plugin lifecycle, best practices, common patterns, and the full TypeScript API surface.

Where can I find the source code?

You can find the source code on GitHub using the link provided at the top of the page.

Related Guides

SKILL.md Source

# Obsidian Plugin Development

## When to use this skill

Use this skill when:

- Creating or modifying an Obsidian plugin
- Working with the `obsidian` npm package TypeScript API
- Building plugin UI: views, modals, settings tabs, commands, ribbon icons, context menus, status bar
- Performing vault file operations (read, create, modify, delete, rename)
- Manipulating the editor (cursor, selection, transactions)
- Working with the workspace (leaves, tabs, splits, sidebars)
- Using MetadataCache for frontmatter, links, tags, headings
- Handling events (vault, workspace, editor, metadata changes)
- Rendering markdown programmatically
- Making HTTP requests from a plugin (use `requestUrl`, not `fetch`)
- Using the Obsidian CLI for automation, scripting, or developer tooling
- Debugging or testing an Obsidian plugin

## Plugin anatomy

An Obsidian plugin consists of:

```
my-plugin/
  ├── main.ts          # Entry point: default export extending Plugin
  ├── manifest.json    # Plugin metadata (id, name, version, minAppVersion)
  ├── styles.css       # Optional: plugin styles
  ├── package.json     # npm dependencies
  ├── tsconfig.json    # TypeScript config
  └── esbuild.config.mjs  # Build config (or rollup/vite)
```

### manifest.json

```json
{
	"id": "my-plugin",
	"name": "My Plugin",
	"version": "1.0.0",
	"minAppVersion": "1.0.0",
	"description": "Description of plugin",
	"author": "Author Name",
	"authorUrl": "https://example.com",
	"isDesktopOnly": false
}
```

### Main plugin class

```typescript
import { Plugin } from 'obsidian';

export default class MyPlugin extends Plugin {
	settings: MySettings;

	async onload() {
		// Called when plugin is activated
		await this.loadSettings();
		this.addSettingTab(new MySettingTab(this.app, this));
		this.addCommand({ id: 'my-cmd', name: 'My Command', callback: () => {} });
		this.addRibbonIcon('icon-name', 'Tooltip', () => {});
		this.registerView('view-type', (leaf) => new MyView(leaf));
		this.registerEvent(this.app.vault.on('modify', (file) => {}));
	}

	onunload() {
		// Called when plugin is deactivated - cleanup happens automatically
		// for anything registered via this.register*() or this.add*()
	}

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

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

## Core API classes

The `app` object (available as `this.app` in Plugin subclasses) provides access to the entire API:

| Property            | Class           | Purpose                                 |
| ------------------- | --------------- | --------------------------------------- |
| `app.vault`         | `Vault`         | File CRUD operations                    |
| `app.workspace`     | `Workspace`     | Panes, tabs, views, layout              |
| `app.metadataCache` | `MetadataCache` | Cached file metadata, link resolution   |
| `app.fileManager`   | `FileManager`   | High-level file operations, frontmatter |

### Vault - File operations

```typescript
// Read
const content = await this.app.vault.read(file); // async read
const cached = await this.app.vault.cachedRead(file); // faster, may be stale

// Create
const newFile = await this.app.vault.create('path/file.md', 'content');
await this.app.vault.createFolder('path/folder');

// Modify
await this.app.vault.modify(file, 'new content');
await this.app.vault.append(file, '\nappended text');

// Atomic read-modify-write
await this.app.vault.process(file, (data) => {
	return data.replace('old', 'new');
});

// Delete and rename
await this.app.vault.delete(file);
await this.app.vault.trash(file, true); // system trash
await this.app.vault.rename(file, 'new/path.md');

// Find files
const file = this.app.vault.getAbstractFileByPath('notes/note.md');
if (file instanceof TFile) {
	/* use it */
}
const allMd = this.app.vault.getMarkdownFiles();
const allFiles = this.app.vault.getFiles();
```

**IMPORTANT**: Always use `vault.getAbstractFileByPath()` and check `instanceof TFile`/`TFolder`. Never construct TFile/TFolder objects directly.

### Workspace - Views, leaves, layout

```typescript
// Get active editor/file
const view = this.app.workspace.getActiveViewOfType(MarkdownView);
const file = this.app.workspace.getActiveFile();

// Open files
const leaf = this.app.workspace.getLeaf('tab'); // 'tab' | 'split' | 'window'
await leaf.openFile(file);
await this.app.workspace.openLinkText('Note Name', '', true);

// Find views
const leaves = this.app.workspace.getLeavesOfType('my-view');
this.app.workspace.iterateAllLeaves((leaf) => {
	/* ... */
});

// Sidebar leaves
const rightLeaf = this.app.workspace.getRightLeaf(false);

// Layout readiness
this.app.workspace.onLayoutReady(() => {
	/* safe to access UI */
});
```

### MetadataCache - Parsed file metadata

```typescript
const cache = this.app.metadataCache.getFileCache(file);
if (cache) {
	cache.headings; // HeadingCache[]
	cache.tags; // TagCache[]
	cache.links; // LinkCache[]
	cache.embeds; // EmbedCache[]
	cache.frontmatter; // FrontMatterCache
	cache.sections; // SectionCache[]
}

// Resolve a wikilink to a TFile
const target = this.app.metadataCache.getFirstLinkpathDest('Note', 'source/path.md');
```

### FileManager - High-level file operations

```typescript
// Process frontmatter (atomic read-modify-write for YAML)
await this.app.fileManager.processFrontMatter(file, (fm) => {
	fm.tags = ['tag1', 'tag2'];
	fm.modified = new Date().toISOString();
});

// Rename with metadata updates (updates links across vault)
await this.app.fileManager.renameFile(file, 'new/path.md');
```

### Editor - Text manipulation

```typescript
const view = this.app.workspace.getActiveViewOfType(MarkdownView);
if (view) {
	const editor = view.editor;

	// Cursor and selection
	const cursor = editor.getCursor(); // { line, ch }
	const selection = editor.getSelection();
	editor.replaceSelection('replacement');
	editor.setSelection(anchor, head);

	// Content
	const value = editor.getValue();
	const line = editor.getLine(lineNum);
	editor.replaceRange('text', from, to);

	// Batch operations
	editor.transaction({ changes: [{ from, to, text: 'new' }] });

	// Scroll
	editor.scrollIntoView({ from, to }, true);
}
```

## UI components

### Commands

```typescript
this.addCommand({
	id: 'unique-command-id',
	name: 'Human-readable name',
	callback: () => {
		/* runs always */
	},
	// OR for editor commands:
	editorCallback: (editor: Editor, view: MarkdownView) => {
		/* runs when editor active */
	},
	// OR conditional:
	checkCallback: (checking: boolean) => {
		if (canRun) {
			if (!checking) {
				doStuff();
			}
			return true;
		}
		return false;
	},
	hotkeys: [{ modifiers: ['Mod', 'Shift'], key: 'p' }],
});
```

### Modals

```typescript
class MyModal extends Modal {
	constructor(app: App) {
		super(app);
	}

	onOpen() {
		this.setTitle('My Modal');
		this.contentEl.createEl('p', { text: 'Content here' });
		new Setting(this.contentEl).setName('Option').addToggle((t) => t.setValue(true).onChange((v) => {}));
	}

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

// Usage: new MyModal(this.app).open();
```

### Settings tabs

```typescript
class MySettingTab extends PluginSettingTab {
	plugin: MyPlugin;
	constructor(app: App, plugin: MyPlugin) {
		super(app, plugin);
		this.plugin = plugin;
	}

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

		new Setting(containerEl)
			.setName('Setting name')
			.setDesc('Description')
			.addText((text) =>
				text
					.setPlaceholder('placeholder')
					.setValue(this.plugin.settings.value)
					.onChange(async (v) => {
						this.plugin.settings.value = v;
						await this.plugin.saveSettings();
					})
			);

		// Setting control types: addText, addTextArea, addToggle, addDropdown,
		// addSlider, addButton, addExtraButton, addColorPicker, addSearch
	}
}
```

### Custom views

```typescript
class MyView extends ItemView {
  getViewType() { return 'my-view-type'; }
  getDisplayText() { return 'My View'; }
  getIcon() { return 'bot'; }

  async onOpen() {
    const container = this.containerEl.children[1];
    container.empty();
    container.createEl('h4', { text: 'My View' });
    this.addAction('refresh-cw', 'Refresh', () => { /* header action */ });
  }

  async onClose() { /* cleanup */ }
}

// Register in onload:
this.registerView('my-view-type', (leaf) => new MyView(leaf));

// Activate:
async activateView() {
  let leaf = this.app.workspace.getLeavesOfType('my-view-type')[0];
  if (!leaf) {
    leaf = this.app.workspace.getRightLeaf(false)!;
    await leaf.setViewState({ type: 'my-view-type', active: true });
  }
  this.app.workspace.revealLeaf(leaf);
}
```

### Other UI

```typescript
// Ribbon icon
this.addRibbonIcon('dice', 'Tooltip text', (evt) => {});

// Status bar
const statusEl = this.addStatusBarItem();
statusEl.setText('Status text');

// Notice (toast)
new Notice('Message to user');
new Notice('Persistent message', 0); // 0 = no auto-dismiss

// Context menu
this.registerEvent(
	this.app.workspace.on('file-menu', (menu, file) => {
		menu.addItem((item) => {
			item
				.setTitle('My action')
				.setIcon('icon')
				.onClick(() => {});
		});
	})
);

// Editor context menu
this.registerEvent(
	this.app.workspace.on('editor-menu', (menu, editor, info) => {
		menu.addItem((item) => {
			item.setTitle('Editor action').onClick(() => {});
		});
	})
);

// HTML elements (Obsidian extends HTMLElement)
const div = containerEl.createDiv({ cls: 'my-class', text: 'Hello' });
const el = containerEl.createEl('span', { cls: 'highlight', attr: { 'data-id': '1' } });
el.empty(); // remove all children
el.addClass('new-class');
el.toggleClass('active', true);

// Icons (Lucide icons built in)
import { setIcon } from 'obsidian';
setIcon(element, 'lucide-icon-name'); // e.g. 'bot', 'settings', 'file-text'
```

## Events

Always use `this.registerEvent()` to ensure automatic cleanup on plugin unload:

```typescript
// Vault events
this.registerEvent(this.app.vault.on('create', (file) => {}));
this.registerEvent(this.app.vault.on('modify', (file) => {}));
this.registerEvent(this.app.vault.on('delete', (file) => {}));
this.registerEvent(this.app.vault.on('rename', (file, oldPath) => {}));

// Workspace events
this.registerEvent(this.app.workspace.on('file-open', (file) => {}));
this.registerEvent(this.app.workspace.on('active-leaf-change', (leaf) => {}));
this.registerEvent(this.app.workspace.on('layout-change', () => {}));
this.registerEvent(this.app.workspace.on('editor-change', (editor, info) => {}));

// MetadataCache events
this.registerEvent(this.app.metadataCache.on('changed', (file, data, cache) => {}));
this.registerEvent(this.app.metadataCache.on('resolved', () => {}));

// Timed intervals
this.registerInterval(window.setInterval(() => {}, 60000));

// DOM events (auto-cleaned up)
this.registerDomEvent(document, 'click', (evt) => {});
```

## Network requests

Always use `requestUrl` instead of `fetch` for cross-platform compatibility (desktop + mobile):

```typescript
import { requestUrl } from 'obsidian';

const response = await requestUrl({
	url: 'https://api.example.com/data',
	method: 'POST',
	headers: { Authorization: 'Bearer TOKEN' },
	contentType: 'application/json',
	body: JSON.stringify({ key: 'value' }),
});
const data = response.json; // parsed JSON
const text = response.text; // raw text
const status = response.status;
```

## Markdown rendering

```typescript
import { MarkdownRenderer } from 'obsidian';

await MarkdownRenderer.render(
	this.app,
	'# Heading\nSome **bold** and a [[link]].',
	containerEl,
	'', // sourcePath for resolving links
	this // Component for lifecycle management
);
```

## Key best practices

1. **Always use Obsidian API over low-level alternatives**: `vault.getMarkdownFiles()` not `vault.adapter.list()`, `fileManager.processFrontMatter()` not manual YAML parsing, `app.metadataCache` not re-parsing files
2. **Register everything for cleanup**: Use `this.registerEvent()`, `this.registerInterval()`, `this.registerDomEvent()`, `this.addCommand()`, `this.addSettingTab()`, `this.registerView()` - they all auto-clean on unload
3. **Use `requestUrl` not `fetch`**: Required for mobile compatibility
4. **Wait for layout**: Use `this.app.workspace.onLayoutReady()` before accessing UI
5. **Check `instanceof`**: Always verify `TFile` vs `TFolder` when using `getAbstractFileByPath()`
6. **Use CSS variables**: Use Obsidian's theme variables for styling, test in both light and dark themes
7. **Use `setIcon()`**: Use `import { setIcon } from 'obsidian'` with Lucide icon names
8. **Debounce frequent operations**: Use `import { debounce } from 'obsidian'` for editor changes or API calls
9. **Use normalized paths**: Obsidian uses forward slashes on all platforms
10. **Never block the main thread**: Use `async/await` for file operations, API calls, and heavy computations

## Obsidian CLI

Obsidian 1.12+ includes a CLI for terminal-based vault interaction. See [the CLI reference](references/cli-reference.md) for complete command documentation.

Key developer commands:

- `obsidian dev:eval code="expression"` - Execute JavaScript in the app console
- `obsidian dev:console` - View captured console messages
- `obsidian dev:dom query="selector"` - Query DOM elements
- `obsidian dev:css query="selector"` - Inspect CSS with source locations
- `obsidian dev:screenshot` - Capture app screenshots
- `obsidian dev:mobile` - Toggle mobile emulation
- `obsidian dev:errors` - Display captured errors
- `obsidian plugin:reload` - Reload plugins during development

## Further reading

- [TypeScript API Reference](references/api-reference.md) - Complete class and interface definitions
- [CLI Reference](references/cli-reference.md) - Full Obsidian CLI command documentation
- [Best Practices](references/best-practices.md) - Detailed patterns and anti-patterns

Related Skills

obsidian-properties

290
from allenhutchison/obsidian-gemini

Work with Obsidian note properties (frontmatter). Activate this skill when users want to add, modify, or organize properties, understand property types, format YAML frontmatter, or use properties with templates, search, or Bases.

obsidian-bases

290
from allenhutchison/obsidian-gemini

Create and configure Obsidian Bases — database-like views of notes. Activate this skill when users want to create bases, write filters, formulas, or set up table/cards/list/map views.

obsidian-cli

290
from allenhutchison/obsidian-gemini

Use the Obsidian CLI to debug, inspect, and test Obsidian plugins during development. Covers plugin reloading, console inspection, runtime evaluation, and common debugging recipes for the gemini-scribe plugin.

vault-semantic-search

290
from allenhutchison/obsidian-gemini

Search vault notes by meaning using semantic search (RAG). Activate this skill when users want to find notes by concept or topic rather than exact keywords, or when keyword search tools return poor results.

recall-sessions

290
from allenhutchison/obsidian-gemini

Search past agent conversations to recall prior discussions, decisions, and context. Activate this skill when users ask about previous conversations, want to resume past work, or reference earlier decisions.

image-generation

290
from allenhutchison/obsidian-gemini

Generate images from text descriptions and save them to the vault. Activate this skill when users want to create illustrations, diagrams, visual content, or any AI-generated images.

gemini-scribe-help

290
from allenhutchison/obsidian-gemini

Answer questions about Gemini Scribe plugin features, settings, and usage. Activate this skill when users ask how to use the plugin, configure settings, or troubleshoot issues.

deep-research

290
from allenhutchison/obsidian-gemini

Conduct comprehensive, multi-source research and generate cited reports. Activate this skill when users want in-depth research on a topic, need synthesis across web and vault sources, or want a structured research report saved to their vault.

audio-transcription

290
from allenhutchison/obsidian-gemini

Transcribe audio and video files into structured notes. Activate this skill when users want to transcribe recordings, meetings, podcasts, voice memos, or any audio/video content in their vault.

ui-ux-guidelines

290
from allenhutchison/obsidian-gemini

UI/UX best practices for obsidian-gemini plugin development. Covers modal sizing, text overflow, message formatting, collapsible UI, animations, icons, file chips, session state, CSS containment, and theme compatibility. Use this skill when building or modifying UI components.

release-process

290
from allenhutchison/obsidian-gemini

Full release workflow for obsidian-gemini: update release notes, run checks, bump version with npm, create a GitHub release, and verify. Use this skill when preparing a new plugin release.

gemini-api-dev

290
from allenhutchison/obsidian-gemini

Use this skill when building applications with Gemini models, Gemini API, working with multimodal content (text, images, audio, video), implementing function calling, using structured outputs, or needing current model specifications. Covers SDK usage (google-genai for Python, @google/genai for JavaScript/TypeScript, com.google.genai:google-genai for Java, google.golang.org/genai for Go), model selection, and API capabilities.