From obsidian-pack
Diagnoses and fixes common Obsidian plugin errors including null workspace access, load failures from manifests, and TypeScript pitfalls. Includes copy-paste fixes and build checks.
npx claudepluginhub jeremylongshore/claude-code-plugins-plus-skills --plugin obsidian-packThis skill is limited to using the following tools:
Diagnostic guide for the six most frequent Obsidian plugin development errors, with root causes and copy-paste fixes.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Checks Next.js compilation errors using a running Turbopack dev server after code edits. Fixes actionable issues before reporting complete. Replaces `next build`.
Diagnostic guide for the six most frequent Obsidian plugin development errors, with root causes and copy-paste fixes.
Accessing app.workspace.activeLeaf or app.workspace.getActiveViewOfType() before the layout is initialized returns null.
// BROKEN: accessing workspace immediately in onload
async onload() {
const view = this.app.workspace.getActiveViewOfType(MarkdownView);
// TypeError: Cannot read properties of null (reading 'editor')
view.editor.replaceSelection('hello');
}
// FIXED: wait for layout-ready event
async onload() {
this.app.workspace.onLayoutReady(() => {
const view = this.app.workspace.getActiveViewOfType(MarkdownView);
if (view) {
view.editor.replaceSelection('hello');
}
});
}
For commands that need workspace access later (not at load time), guard with a null check:
this.addCommand({
id: 'my-command',
name: 'Do Something',
checkCallback: (checking: boolean) => {
const view = this.app.workspace.getActiveViewOfType(MarkdownView);
if (view) {
if (!checking) {
// safe to use view.editor here
view.editor.replaceSelection('inserted');
}
return true;
}
return false;
}
});
This error appears in the console when Obsidian cannot parse your built main.js or your manifest.json is invalid.
Check 1: Build output exists and compiles cleanly
set -euo pipefail
npm run build 2>&1
# Look for TypeScript errors in output
ls -la main.js # Must exist in plugin root
Check 2: manifest.json has all required fields
{
"id": "my-plugin",
"name": "My Plugin",
"version": "1.0.0",
"minAppVersion": "0.15.0",
"description": "Does something useful",
"author": "Your Name",
"isDesktopOnly": false
}
Missing id, name, version, or minAppVersion causes a silent load failure. The id must match the folder name under .obsidian/plugins/.
Check 3: Default export
// BROKEN: named export
export class MyPlugin extends Plugin { ... }
// FIXED: default export required
export default class MyPlugin extends Plugin { ... }
Obsidian auto-loads styles.css from the plugin root directory. It must be named exactly styles.css (not style.css, not in a subdirectory).
set -euo pipefail
# Verify all three required files exist in plugin root
ls -la styles.css manifest.json main.js
If you use a CSS preprocessor, ensure the build outputs to ./styles.css:
{
"scripts": {
"build:css": "sass src/styles.scss styles.css",
"build": "npm run build:css && node esbuild.config.mjs"
}
}
Common gotcha: the file must be styles.css (plural), not style.css.
Commands registered outside onload() or after the plugin is enabled won't appear in the command palette.
// BROKEN: adding command in a separate method called conditionally
async onload() {
await this.loadSettings();
// command never added because registerCommands is not called
}
registerCommands() {
this.addCommand({ id: 'test', name: 'Test', callback: () => {} });
}
// FIXED: add all commands directly in onload
async onload() {
await this.loadSettings();
this.addCommand({
id: 'test',
name: 'Test',
callback: () => {
new Notice('Working!');
}
});
}
If a command should only be available when a markdown file is open, use editorCallback instead of callback — Obsidian automatically hides it when no editor is active:
this.addCommand({
id: 'editor-only',
name: 'Editor Only Command',
editorCallback: (editor: Editor) => {
editor.replaceSelection('inserted text');
}
});
vault.read() and vault.cachedRead() throw if the file doesn't exist. Always check first.
// BROKEN: assumes file exists
async readConfig() {
const content = await this.app.vault.adapter.read('config.json');
return JSON.parse(content);
}
// FIXED: check existence, handle missing file
async readConfig(): Promise<MyConfig> {
const path = 'config.json';
const exists = await this.app.vault.adapter.exists(path);
if (!exists) {
return { ...DEFAULT_CONFIG };
}
try {
const content = await this.app.vault.adapter.read(path);
return JSON.parse(content);
} catch (e) {
console.error('Failed to parse config:', e);
return { ...DEFAULT_CONFIG };
}
}
For vault files (TFile objects), use getAbstractFileByPath:
const file = this.app.vault.getAbstractFileByPath('notes/target.md');
if (file instanceof TFile) {
const content = await this.app.vault.read(file);
// process content
} else {
new Notice('File not found: notes/target.md');
}
The most common settings bug: modifying the settings object without calling saveData.
// BROKEN: settings change lost on restart
this.settings.theme = 'dark';
// forgot to call saveData!
// FIXED: always save after modifying
this.settings.theme = 'dark';
await this.saveData(this.settings);
In settings tabs, save on every change:
new Setting(containerEl)
.setName('Theme')
.addDropdown(dropdown => dropdown
.addOption('light', 'Light')
.addOption('dark', 'Dark')
.setValue(this.plugin.settings.theme)
.onChange(async (value) => {
this.plugin.settings.theme = value;
await this.plugin.saveSettings(); // calls this.saveData(this.settings)
}));
Load settings with defaults to prevent undefined fields after plugin updates:
async loadSettings() {
// loadData() returns null on first run — Object.assign handles this safely
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
}
Object.assign merges saved data over defaults, so new fields added in later versions get their default value instead of undefined.
| Error | Cause | Solution |
|---|---|---|
TypeError: Cannot read properties of null | Workspace not ready | Use onLayoutReady or null-check |
Plugin failed to load | Build error or bad manifest | Check console, verify manifest.json fields |
| CSS has no effect | Wrong filename or path | Must be styles.css in plugin root |
| Command missing from palette | Not added in onload() | Move addCommand into onload |
Error: ENOENT on vault read | File doesn't exist | Check with adapter.exists() first |
| Settings reset on restart | Missing saveData call | Call saveData after every mutation |
When a plugin fails to load, check these in order:
main.js, manifest.json, and styles.css exist in plugin foldermanifest.json has id, name, version, minAppVersionmain.ts uses export default classnpm run build and reload Obsidian (Ctrl/Cmd+R)// Add to your plugin class for temporary debugging
private debug(msg: string, ...args: any[]) {
if (this.settings.debugMode) {
console.log(`[${this.manifest.id}] ${msg}`, ...args);
}
}
For comprehensive debugging workflows, see obsidian-debug-bundle.