Claude-code-plugins-plus obsidian-common-errors
git clone https://github.com/jeremylongshore/claude-code-plugins-plus-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-common-errors" ~/.claude/skills/jeremylongshore-claude-code-plugins-plus-obsidian-common-errors && rm -rf "$T"
plugins/saas-packs/obsidian-pack/skills/obsidian-common-errors/SKILL.mdObsidian Common Errors
Overview
Diagnostic guide for the six most frequent Obsidian plugin development errors, with root causes and copy-paste fixes.
Prerequisites
- Obsidian plugin development environment set up
- Access to Developer Console (Ctrl/Cmd+Shift+I)
- Plugin source code access
Instructions
Step 1: "Cannot read properties of null" — Workspace Not Ready
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; } });
Step 2: "Plugin failed to load" — Syntax or Manifest Errors
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 { ... }
Step 3: CSS Not Loading — Wrong styles.css Path
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.
Step 4: Commands Not Showing in Palette
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'); } });
Step 5: Vault Read Errors — File Doesn't Exist
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'); }
Step 6: Settings Not Persisting — Missing saveData Call
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.
Output
- Identified error matched to one of the six categories
- Root cause explanation
- Working code fix applied to plugin source
Error Handling
| Error | Cause | Solution |
|---|---|---|
| Workspace not ready | Use or null-check |
| Build error or bad manifest | Check console, verify fields |
| CSS has no effect | Wrong filename or path | Must be in plugin root |
| Command missing from palette | Not added in | Move into |
on vault read | File doesn't exist | Check with first |
| Settings reset on restart | Missing call | Call after every mutation |
Examples
Quick Diagnostic Checklist
When a plugin fails to load, check these in order:
- Open Developer Console (Ctrl/Cmd+Shift+I) and look for red errors
- Verify
,main.js
, andmanifest.json
exist in plugin folderstyles.css - Confirm
hasmanifest.json
,id
,name
,versionminAppVersion - Confirm
usesmain.tsexport default class - Rebuild with
and reload Obsidian (Ctrl/Cmd+R)npm run build
Debug Logging Pattern
// 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); } }
Resources
Next Steps
For comprehensive debugging workflows, see
obsidian-debug-bundle.