[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M793QE9Q5Z95VNF2KBBP90J8":3,"skill-same:01M793QE9Q5Z95VNF2KBBP90J8":56},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":18,"publishedAt":19,"linkOnly":20,"markdownContent":21,"markdownBody":22,"frontMatter":23,"refName":33,"commitSha":34,"skillDirectory":35,"forksCount":36,"repositoryHtmlUrl":37,"repositoryHomepageUrl":6,"ownerHtmlUrl":38,"ownerBlogUrl":39,"ownerDescription":40,"updatedAt":41,"install":42,"resources":55},"01M793QE9Q5Z95VNF2KBBP90J8","obsidian-data-handling",null,"Implement vault data backup, sync, and recovery strategies.\nUse when building backup features, implementing data export,\nor handling vault synchronization in your plugin.\nTrigger with phrases like \"obsidian backup\", \"obsidian sync\",\n\"obsidian data export\", \"vault backup strategy\".","jukeyman\u002Fjukeyman-skills","jukeyman","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F142960420?v=4","01M78FR4Y3DHMMCBCC0GXXNB3F","RICK JEFFERSON","01M78FR4Z8RVQRAD31Y5J7V6EB",1,"MIT","https:\u002F\u002Fgithub.com\u002Fjukeyman\u002Fjukeyman-skills\u002Fblob\u002Fmain\u002Fskills\u002Fsaas-packs--obsidian-pack--obsidian-data-handling\u002FSKILL.md",[],[],"2026-11-15 14:19:17",false,"---\nname: obsidian-data-handling\ndescription: |\n  Implement vault data backup, sync, and recovery strategies.\n  Use when building backup features, implementing data export,\n  or handling vault synchronization in your plugin.\n  Trigger with phrases like \"obsidian backup\", \"obsidian sync\",\n  \"obsidian data export\", \"vault backup strategy\".\nallowed-tools: Read, Write, Edit\nversion: 1.0.0\nlicense: MIT\nauthor: Jeremy Longshore \u003Cjeremy@intentsolutions.io>\ncompatible-with: claude-code, codex, openclaw\ntags: [saas, obsidian, backup]\n---\n# Obsidian Data Handling\n\n## Overview\nData management patterns for Obsidian plugins: plugin config with loadData\u002FsaveData, vault file I\u002FO, frontmatter parsing via metadataCache, handling renames and deletes, cross-device sync considerations, and IndexedDB fallback for large datasets.\n\n## Prerequisites\n- Working Obsidian plugin (`export default class extends Plugin`)\n- Understanding of Obsidian's `Vault` and `MetadataCache` APIs\n- TypeScript compilation configured\n\n## Instructions\n\n### Step 1: Plugin Config with loadData \u002F saveData\n\nObsidian stores plugin data in `.obsidian\u002Fplugins\u002F\u003Cplugin-id>\u002Fdata.json`. Use `loadData()` and `saveData()` — never read that file directly.\n\n```typescript\ninterface PluginConfig {\n  version: number;\n  apiEndpoint: string;\n  syncInterval: number;\n  excludedFolders: string[];\n}\n\nconst DEFAULT_CONFIG: PluginConfig = {\n  version: 1,\n  apiEndpoint: 'https:\u002F\u002Fapi.example.com',\n  syncInterval: 300,\n  excludedFolders: [],\n};\n\nexport default class DataPlugin extends Plugin {\n  config: PluginConfig;\n\n  async onload() {\n    await this.loadConfig();\n  }\n\n  async loadConfig() {\n    const saved = await this.loadData();\n    this.config = Object.assign({}, DEFAULT_CONFIG, saved);\n\n    \u002F\u002F Migrate from older config versions\n    if (this.config.version \u003C 1) {\n      this.config.excludedFolders = [];\n      this.config.version = 1;\n      await this.saveConfig();\n    }\n  }\n\n  async saveConfig() {\n    await this.saveData(this.config);\n  }\n}\n```\n\n`loadData()` returns `null` on first run — `Object.assign` onto defaults handles this cleanly.\n\n### Step 2: Reading and Writing Vault Files\n\n```typescript\nimport { TFile, TFolder, normalizePath } from 'obsidian';\n\n\u002F\u002F Read a markdown file\nasync readNote(path: string): Promise\u003Cstring | null> {\n  const file = this.app.vault.getAbstractFileByPath(normalizePath(path));\n  if (file instanceof TFile) {\n    return await this.app.vault.read(file);\n  }\n  return null;\n}\n\n\u002F\u002F Write or create a markdown file\nasync writeNote(path: string, content: string): Promise\u003CTFile> {\n  const normalized = normalizePath(path);\n  const existing = this.app.vault.getAbstractFileByPath(normalized);\n\n  if (existing instanceof TFile) {\n    await this.app.vault.modify(existing, content);\n    return existing;\n  }\n\n  \u002F\u002F Ensure parent folder exists\n  const dir = normalized.substring(0, normalized.lastIndexOf('\u002F'));\n  if (dir && !this.app.vault.getAbstractFileByPath(dir)) {\n    await this.app.vault.createFolder(dir);\n  }\n\n  return await this.app.vault.create(normalized, content);\n}\n\n\u002F\u002F Append to a file (e.g., a log or journal)\nasync appendToNote(path: string, text: string): Promise\u003Cvoid> {\n  const file = this.app.vault.getAbstractFileByPath(normalizePath(path));\n  if (file instanceof TFile) {\n    await this.app.vault.append(file, '\\n' + text);\n  }\n}\n```\n\nUse `vault.cachedRead()` instead of `vault.read()` when you don't need the absolute latest content — it avoids hitting disk on every call.\n\n### Step 3: Working with Frontmatter via MetadataCache\n\nNever parse YAML frontmatter manually. Obsidian's `metadataCache` keeps a parsed cache of every file's frontmatter.\n\n```typescript\nimport { TFile, CachedMetadata } from 'obsidian';\n\n\u002F\u002F Read frontmatter from a file\ngetFrontmatter(file: TFile): Record\u003Cstring, any> | null {\n  const cache: CachedMetadata | null = this.app.metadataCache.getFileCache(file);\n  return cache?.frontmatter ?? null;\n}\n\n\u002F\u002F Update frontmatter using processFrontMatter (Obsidian 1.4+)\nasync setStatus(file: TFile, status: string): Promise\u003Cvoid> {\n  await this.app.fileManager.processFrontMatter(file, (fm) => {\n    fm.status = status;\n    fm.updated = new Date().toISOString();\n  });\n}\n\n\u002F\u002F Bulk query: find all files with a specific tag\ngetFilesWithTag(tag: string): TFile[] {\n  const files: TFile[] = [];\n  for (const file of this.app.vault.getMarkdownFiles()) {\n    const cache = this.app.metadataCache.getFileCache(file);\n    const tags = cache?.tags?.map(t => t.tag) ?? [];\n    const fmTags = cache?.frontmatter?.tags ?? [];\n    if (tags.includes(tag) || fmTags.includes(tag.replace('#', ''))) {\n      files.push(file);\n    }\n  }\n  return files;\n}\n```\n\n`processFrontMatter` handles YAML serialization correctly — it preserves comments and formatting, and is the only safe way to update frontmatter programmatically.\n\n### Step 4: Handling File Renames and Deletes\n\nPlugins that index file paths must update their state when files move or disappear.\n\n```typescript\nasync onload() {\n  \u002F\u002F Track renames to update internal references\n  this.registerEvent(\n    this.app.vault.on('rename', (file, oldPath) => {\n      if (file instanceof TFile) {\n        this.onFileRenamed(file, oldPath);\n      }\n    })\n  );\n\n  \u002F\u002F Clean up when files are deleted\n  this.registerEvent(\n    this.app.vault.on('delete', (file) => {\n      if (file instanceof TFile) {\n        this.onFileDeleted(file.path);\n      }\n    })\n  );\n}\n\nprivate onFileRenamed(file: TFile, oldPath: string) {\n  \u002F\u002F Update any stored path references\n  if (this.config.pinnedFiles?.includes(oldPath)) {\n    const idx = this.config.pinnedFiles.indexOf(oldPath);\n    this.config.pinnedFiles[idx] = file.path;\n    this.saveConfig();\n  }\n}\n\nprivate onFileDeleted(path: string) {\n  \u002F\u002F Remove from any indexes\n  if (this.config.pinnedFiles?.includes(path)) {\n    this.config.pinnedFiles = this.config.pinnedFiles.filter(p => p !== path);\n    this.saveConfig();\n  }\n}\n```\n\nAlways use `registerEvent` — it automatically cleans up the listener when the plugin unloads.\n\n### Step 5: Cross-Device Sync Considerations\n\nObsidian vaults synced via iCloud, Dropbox, or Obsidian Sync introduce eventual consistency issues.\n\n```typescript\n\u002F\u002F Problem: two devices modify data.json simultaneously\n\u002F\u002F Solution: merge-friendly data structures\n\ninterface SyncSafeConfig {\n  \u002F\u002F Use a map keyed by unique IDs instead of arrays\n  \u002F\u002F Maps merge better than arrays across sync conflicts\n  items: Record\u003Cstring, { value: string; updatedAt: number }>;\n}\n\n\u002F\u002F Timestamp-based last-write-wins merge\nmergeConfigs(local: SyncSafeConfig, remote: SyncSafeConfig): SyncSafeConfig {\n  const merged: SyncSafeConfig = { items: {} };\n  const allKeys = new Set([\n    ...Object.keys(local.items),\n    ...Object.keys(remote.items),\n  ]);\n\n  for (const key of allKeys) {\n    const l = local.items[key];\n    const r = remote.items[key];\n    if (!l) merged.items[key] = r;\n    else if (!r) merged.items[key] = l;\n    else merged.items[key] = l.updatedAt >= r.updatedAt ? l : r;\n  }\n  return merged;\n}\n```\n\nGuidelines for sync-friendly plugins:\n- Avoid storing file paths in `data.json` — they differ across devices with different vault locations\n- Use file content hashes or frontmatter IDs for identity instead of paths\n- Keep `data.json` small — large files cause sync conflicts and slow sync\n\n### Step 6: IndexedDB Fallback for Large Datasets\n\nWhen plugin data exceeds what's practical for `data.json` (more than ~1MB), use IndexedDB.\n\n```typescript\nclass PluginDatabase {\n  private db: IDBDatabase | null = null;\n  private dbName: string;\n\n  constructor(pluginId: string) {\n    this.dbName = `obsidian-${pluginId}`;\n  }\n\n  async open(): Promise\u003Cvoid> {\n    return new Promise((resolve, reject) => {\n      const request = indexedDB.open(this.dbName, 1);\n\n      request.onupgradeneeded = (event) => {\n        const db = (event.target as IDBOpenDBRequest).result;\n        if (!db.objectStoreNames.contains('cache')) {\n          db.createObjectStore('cache', { keyPath: 'id' });\n        }\n      };\n\n      request.onsuccess = (event) => {\n        this.db = (event.target as IDBOpenDBRequest).result;\n        resolve();\n      };\n\n      request.onerror = () => reject(request.error);\n    });\n  }\n\n  async put(id: string, data: any): Promise\u003Cvoid> {\n    if (!this.db) throw new Error('Database not open');\n    return new Promise((resolve, reject) => {\n      const tx = this.db!.transaction('cache', 'readwrite');\n      tx.objectStore('cache').put({ id, data, updatedAt: Date.now() });\n      tx.oncomplete = () => resolve();\n      tx.onerror = () => reject(tx.error);\n    });\n  }\n\n  async get(id: string): Promise\u003Cany | null> {\n    if (!this.db) throw new Error('Database not open');\n    return new Promise((resolve, reject) => {\n      const tx = this.db!.transaction('cache', 'readonly');\n      const request = tx.objectStore('cache').get(id);\n      request.onsuccess = () => resolve(request.result?.data ?? null);\n      request.onerror = () => reject(request.error);\n    });\n  }\n\n  close() {\n    this.db?.close();\n    this.db = null;\n  }\n}\n\n\u002F\u002F Usage in plugin\nasync onload() {\n  this.db = new PluginDatabase(this.manifest.id);\n  await this.db.open();\n}\n\nonunload() {\n  this.db?.close();\n}\n```\n\nIndexedDB is per-device and does not sync across devices. Use it for caches and derived data that can be rebuilt, not for primary user data.\n\n## Output\n- Plugin config loading with version migration\n- Safe vault file read\u002Fwrite\u002Fappend operations\n- Frontmatter access via metadataCache\n- Rename and delete event handlers\n- Sync-friendly data structures\n- IndexedDB storage for large datasets\n\n## Error Handling\n| Issue | Cause | Solution |\n|-------|-------|----------|\n| `loadData()` returns null | First run, no data.json yet | `Object.assign` onto defaults |\n| Frontmatter returns undefined | File not yet indexed by cache | Listen for `metadataCache.on('resolved')` |\n| File write fails | Parent folder doesn't exist | Create folder with `vault.createFolder()` first |\n| Settings lost after sync | Concurrent writes from two devices | Use merge-friendly data structures with timestamps |\n| data.json too large \u002F slow | Storing too much data | Move large data to IndexedDB |\n| stale cache after modify | `cachedRead` returns old content | Use `vault.read()` when freshness matters |\n\n## Examples\n\n### Export All Notes with Tag to JSON\n```typescript\nasync exportTaggedNotes(tag: string): Promise\u003Cstring> {\n  const files = this.getFilesWithTag(tag);\n  const notes = await Promise.all(\n    files.map(async (f) => ({\n      path: f.path,\n      content: await this.app.vault.read(f),\n      frontmatter: this.getFrontmatter(f),\n    }))\n  );\n  return JSON.stringify(notes, null, 2);\n}\n```\n\n### Atomic Config Update\n```typescript\nasync updateConfig\u003CK extends keyof PluginConfig>(\n  key: K,\n  value: PluginConfig[K]\n): Promise\u003Cvoid> {\n  this.config[key] = value;\n  await this.saveConfig();\n}\n```\n\n## Resources\n- [Obsidian Vault API](https:\u002F\u002Fdocs.obsidian.md\u002FReference\u002FTypeScript+API\u002FVault)\n- [Obsidian FileManager API](https:\u002F\u002Fdocs.obsidian.md\u002FReference\u002FTypeScript+API\u002FFileManager)\n- [MetadataCache API](https:\u002F\u002Fdocs.obsidian.md\u002FReference\u002FTypeScript+API\u002FMetadataCache)\n- [Obsidian Sync](https:\u002F\u002Fhelp.obsidian.md\u002FObsidian+Sync)\n\n## Next Steps\nFor team access control patterns, see `obsidian-enterprise-rbac`. For performance with large vaults, see `obsidian-rate-limits`.\n","# Obsidian Data Handling\n\n## Overview\nData management patterns for Obsidian plugins: plugin config with loadData\u002FsaveData, vault file I\u002FO, frontmatter parsing via metadataCache, handling renames and deletes, cross-device sync considerations, and IndexedDB fallback for large datasets.\n\n## Prerequisites\n- Working Obsidian plugin (`export default class extends Plugin`)\n- Understanding of Obsidian's `Vault` and `MetadataCache` APIs\n- TypeScript compilation configured\n\n## Instructions\n\n### Step 1: Plugin Config with loadData \u002F saveData\n\nObsidian stores plugin data in `.obsidian\u002Fplugins\u002F\u003Cplugin-id>\u002Fdata.json`. Use `loadData()` and `saveData()` — never read that file directly.\n\n```typescript\ninterface PluginConfig {\n  version: number;\n  apiEndpoint: string;\n  syncInterval: number;\n  excludedFolders: string[];\n}\n\nconst DEFAULT_CONFIG: PluginConfig = {\n  version: 1,\n  apiEndpoint: 'https:\u002F\u002Fapi.example.com',\n  syncInterval: 300,\n  excludedFolders: [],\n};\n\nexport default class DataPlugin extends Plugin {\n  config: PluginConfig;\n\n  async onload() {\n    await this.loadConfig();\n  }\n\n  async loadConfig() {\n    const saved = await this.loadData();\n    this.config = Object.assign({}, DEFAULT_CONFIG, saved);\n\n    \u002F\u002F Migrate from older config versions\n    if (this.config.version \u003C 1) {\n      this.config.excludedFolders = [];\n      this.config.version = 1;\n      await this.saveConfig();\n    }\n  }\n\n  async saveConfig() {\n    await this.saveData(this.config);\n  }\n}\n```\n\n`loadData()` returns `null` on first run — `Object.assign` onto defaults handles this cleanly.\n\n### Step 2: Reading and Writing Vault Files\n\n```typescript\nimport { TFile, TFolder, normalizePath } from 'obsidian';\n\n\u002F\u002F Read a markdown file\nasync readNote(path: string): Promise\u003Cstring | null> {\n  const file = this.app.vault.getAbstractFileByPath(normalizePath(path));\n  if (file instanceof TFile) {\n    return await this.app.vault.read(file);\n  }\n  return null;\n}\n\n\u002F\u002F Write or create a markdown file\nasync writeNote(path: string, content: string): Promise\u003CTFile> {\n  const normalized = normalizePath(path);\n  const existing = this.app.vault.getAbstractFileByPath(normalized);\n\n  if (existing instanceof TFile) {\n    await this.app.vault.modify(existing, content);\n    return existing;\n  }\n\n  \u002F\u002F Ensure parent folder exists\n  const dir = normalized.substring(0, normalized.lastIndexOf('\u002F'));\n  if (dir && !this.app.vault.getAbstractFileByPath(dir)) {\n    await this.app.vault.createFolder(dir);\n  }\n\n  return await this.app.vault.create(normalized, content);\n}\n\n\u002F\u002F Append to a file (e.g., a log or journal)\nasync appendToNote(path: string, text: string): Promise\u003Cvoid> {\n  const file = this.app.vault.getAbstractFileByPath(normalizePath(path));\n  if (file instanceof TFile) {\n    await this.app.vault.append(file, '\\n' + text);\n  }\n}\n```\n\nUse `vault.cachedRead()` instead of `vault.read()` when you don't need the absolute latest content — it avoids hitting disk on every call.\n\n### Step 3: Working with Frontmatter via MetadataCache\n\nNever parse YAML frontmatter manually. Obsidian's `metadataCache` keeps a parsed cache of every file's frontmatter.\n\n```typescript\nimport { TFile, CachedMetadata } from 'obsidian';\n\n\u002F\u002F Read frontmatter from a file\ngetFrontmatter(file: TFile): Record\u003Cstring, any> | null {\n  const cache: CachedMetadata | null = this.app.metadataCache.getFileCache(file);\n  return cache?.frontmatter ?? null;\n}\n\n\u002F\u002F Update frontmatter using processFrontMatter (Obsidian 1.4+)\nasync setStatus(file: TFile, status: string): Promise\u003Cvoid> {\n  await this.app.fileManager.processFrontMatter(file, (fm) => {\n    fm.status = status;\n    fm.updated = new Date().toISOString();\n  });\n}\n\n\u002F\u002F Bulk query: find all files with a specific tag\ngetFilesWithTag(tag: string): TFile[] {\n  const files: TFile[] = [];\n  for (const file of this.app.vault.getMarkdownFiles()) {\n    const cache = this.app.metadataCache.getFileCache(file);\n    const tags = cache?.tags?.map(t => t.tag) ?? [];\n    const fmTags = cache?.frontmatter?.tags ?? [];\n    if (tags.includes(tag) || fmTags.includes(tag.replace('#', ''))) {\n      files.push(file);\n    }\n  }\n  return files;\n}\n```\n\n`processFrontMatter` handles YAML serialization correctly — it preserves comments and formatting, and is the only safe way to update frontmatter programmatically.\n\n### Step 4: Handling File Renames and Deletes\n\nPlugins that index file paths must update their state when files move or disappear.\n\n```typescript\nasync onload() {\n  \u002F\u002F Track renames to update internal references\n  this.registerEvent(\n    this.app.vault.on('rename', (file, oldPath) => {\n      if (file instanceof TFile) {\n        this.onFileRenamed(file, oldPath);\n      }\n    })\n  );\n\n  \u002F\u002F Clean up when files are deleted\n  this.registerEvent(\n    this.app.vault.on('delete', (file) => {\n      if (file instanceof TFile) {\n        this.onFileDeleted(file.path);\n      }\n    })\n  );\n}\n\nprivate onFileRenamed(file: TFile, oldPath: string) {\n  \u002F\u002F Update any stored path references\n  if (this.config.pinnedFiles?.includes(oldPath)) {\n    const idx = this.config.pinnedFiles.indexOf(oldPath);\n    this.config.pinnedFiles[idx] = file.path;\n    this.saveConfig();\n  }\n}\n\nprivate onFileDeleted(path: string) {\n  \u002F\u002F Remove from any indexes\n  if (this.config.pinnedFiles?.includes(path)) {\n    this.config.pinnedFiles = this.config.pinnedFiles.filter(p => p !== path);\n    this.saveConfig();\n  }\n}\n```\n\nAlways use `registerEvent` — it automatically cleans up the listener when the plugin unloads.\n\n### Step 5: Cross-Device Sync Considerations\n\nObsidian vaults synced via iCloud, Dropbox, or Obsidian Sync introduce eventual consistency issues.\n\n```typescript\n\u002F\u002F Problem: two devices modify data.json simultaneously\n\u002F\u002F Solution: merge-friendly data structures\n\ninterface SyncSafeConfig {\n  \u002F\u002F Use a map keyed by unique IDs instead of arrays\n  \u002F\u002F Maps merge better than arrays across sync conflicts\n  items: Record\u003Cstring, { value: string; updatedAt: number }>;\n}\n\n\u002F\u002F Timestamp-based last-write-wins merge\nmergeConfigs(local: SyncSafeConfig, remote: SyncSafeConfig): SyncSafeConfig {\n  const merged: SyncSafeConfig = { items: {} };\n  const allKeys = new Set([\n    ...Object.keys(local.items),\n    ...Object.keys(remote.items),\n  ]);\n\n  for (const key of allKeys) {\n    const l = local.items[key];\n    const r = remote.items[key];\n    if (!l) merged.items[key] = r;\n    else if (!r) merged.items[key] = l;\n    else merged.items[key] = l.updatedAt >= r.updatedAt ? l : r;\n  }\n  return merged;\n}\n```\n\nGuidelines for sync-friendly plugins:\n- Avoid storing file paths in `data.json` — they differ across devices with different vault locations\n- Use file content hashes or frontmatter IDs for identity instead of paths\n- Keep `data.json` small — large files cause sync conflicts and slow sync\n\n### Step 6: IndexedDB Fallback for Large Datasets\n\nWhen plugin data exceeds what's practical for `data.json` (more than ~1MB), use IndexedDB.\n\n```typescript\nclass PluginDatabase {\n  private db: IDBDatabase | null = null;\n  private dbName: string;\n\n  constructor(pluginId: string) {\n    this.dbName = `obsidian-${pluginId}`;\n  }\n\n  async open(): Promise\u003Cvoid> {\n    return new Promise((resolve, reject) => {\n      const request = indexedDB.open(this.dbName, 1);\n\n      request.onupgradeneeded = (event) => {\n        const db = (event.target as IDBOpenDBRequest).result;\n        if (!db.objectStoreNames.contains('cache')) {\n          db.createObjectStore('cache', { keyPath: 'id' });\n        }\n      };\n\n      request.onsuccess = (event) => {\n        this.db = (event.target as IDBOpenDBRequest).result;\n        resolve();\n      };\n\n      request.onerror = () => reject(request.error);\n    });\n  }\n\n  async put(id: string, data: any): Promise\u003Cvoid> {\n    if (!this.db) throw new Error('Database not open');\n    return new Promise((resolve, reject) => {\n      const tx = this.db!.transaction('cache', 'readwrite');\n      tx.objectStore('cache').put({ id, data, updatedAt: Date.now() });\n      tx.oncomplete = () => resolve();\n      tx.onerror = () => reject(tx.error);\n    });\n  }\n\n  async get(id: string): Promise\u003Cany | null> {\n    if (!this.db) throw new Error('Database not open');\n    return new Promise((resolve, reject) => {\n      const tx = this.db!.transaction('cache', 'readonly');\n      const request = tx.objectStore('cache').get(id);\n      request.onsuccess = () => resolve(request.result?.data ?? null);\n      request.onerror = () => reject(request.error);\n    });\n  }\n\n  close() {\n    this.db?.close();\n    this.db = null;\n  }\n}\n\n\u002F\u002F Usage in plugin\nasync onload() {\n  this.db = new PluginDatabase(this.manifest.id);\n  await this.db.open();\n}\n\nonunload() {\n  this.db?.close();\n}\n```\n\nIndexedDB is per-device and does not sync across devices. Use it for caches and derived data that can be rebuilt, not for primary user data.\n\n## Output\n- Plugin config loading with version migration\n- Safe vault file read\u002Fwrite\u002Fappend operations\n- Frontmatter access via metadataCache\n- Rename and delete event handlers\n- Sync-friendly data structures\n- IndexedDB storage for large datasets\n\n## Error Handling\n| Issue | Cause | Solution |\n|-------|-------|----------|\n| `loadData()` returns null | First run, no data.json yet | `Object.assign` onto defaults |\n| Frontmatter returns undefined | File not yet indexed by cache | Listen for `metadataCache.on('resolved')` |\n| File write fails | Parent folder doesn't exist | Create folder with `vault.createFolder()` first |\n| Settings lost after sync | Concurrent writes from two devices | Use merge-friendly data structures with timestamps |\n| data.json too large \u002F slow | Storing too much data | Move large data to IndexedDB |\n| stale cache after modify | `cachedRead` returns old content | Use `vault.read()` when freshness matters |\n\n## Examples\n\n### Export All Notes with Tag to JSON\n```typescript\nasync exportTaggedNotes(tag: string): Promise\u003Cstring> {\n  const files = this.getFilesWithTag(tag);\n  const notes = await Promise.all(\n    files.map(async (f) => ({\n      path: f.path,\n      content: await this.app.vault.read(f),\n      frontmatter: this.getFrontmatter(f),\n    }))\n  );\n  return JSON.stringify(notes, null, 2);\n}\n```\n\n### Atomic Config Update\n```typescript\nasync updateConfig\u003CK extends keyof PluginConfig>(\n  key: K,\n  value: PluginConfig[K]\n): Promise\u003Cvoid> {\n  this.config[key] = value;\n  await this.saveConfig();\n}\n```\n\n## Resources\n- [Obsidian Vault API](https:\u002F\u002Fdocs.obsidian.md\u002FReference\u002FTypeScript+API\u002FVault)\n- [Obsidian FileManager API](https:\u002F\u002Fdocs.obsidian.md\u002FReference\u002FTypeScript+API\u002FFileManager)\n- [MetadataCache API](https:\u002F\u002Fdocs.obsidian.md\u002FReference\u002FTypeScript+API\u002FMetadataCache)\n- [Obsidian Sync](https:\u002F\u002Fhelp.obsidian.md\u002FObsidian+Sync)\n\n## Next Steps\nFor team access control patterns, see `obsidian-enterprise-rbac`. For performance with large vaults, see `obsidian-rate-limits`.\n",{"name":5,"tags":24,"author":28,"license":15,"version":29,"description":30,"allowed-tools":31,"compatible-with":32},[25,26,27],"saas","obsidian","backup","Jeremy Longshore \u003Cjeremy@intentsolutions.io>","1.0.0","Implement vault data backup, sync, and recovery strategies.\nUse when building backup features, implementing data export,\nor handling vault synchronization in your plugin.\nTrigger with phrases like \"obsidian backup\", \"obsidian sync\",\n\"obsidian data export\", \"vault backup strategy\".\n","Read, Write, Edit","claude-code, codex, openclaw","main","99825ad3f3bcc6597ddcc7294a14e6e922f2b67b","skills\u002Fsaas-packs--obsidian-pack--obsidian-data-handling",0,"https:\u002F\u002Fgithub.com\u002Fjukeyman\u002Fjukeyman-skills","https:\u002F\u002Fgithub.com\u002Fjukeyman","https:\u002F\u002FRJBUSINESSSOLUTIONS.ORG","Credit and Finance Expert | Developer\r\n\r\nI'm Rick Jefferson, a seasoned credit and finance expert with over a decade of experience in financial literacy,","2026-07-13 07:03:29",{"source":8,"skillName":5,"commands":43,"prompt":53,"zipAvailable":54},[44,47,50],{"tool":45,"command":46},"npx","npx skills add jukeyman\u002Fjukeyman-skills --skill obsidian-data-handling",{"tool":48,"command":49},"bunx","bunx skills add jukeyman\u002Fjukeyman-skills --skill obsidian-data-handling",{"tool":51,"command":52},"pnpm","pnpm dlx skills add jukeyman\u002Fjukeyman-skills --skill obsidian-data-handling","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fjukeyman\u002Fjukeyman-skills\u002Fblob\u002Fmain\u002Fskills\u002Fsaas-packs--obsidian-pack--obsidian-data-handling\u002FSKILL.md",true,[],[57,64,71,78,85,93],{"skillNo":58,"name":59,"nameZh":6,"description":60,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":61,"categories":62,"keywords":63,"publishedAt":19},"01M79ANS2SM329NEP5ZCX106RR","zustand-store-creator","Create zustand store creator operations. Auto-activating skill for Frontend Development.\nTriggers on: zustand store creator, zustand store creator\nPart of the Frontend Development skill category. Use when working with zustand store creator functionality. Trigger with phrases like \"zustand store creator\", \"zustand creator\", \"zustand\".","https:\u002F\u002Fgithub.com\u002Fjukeyman\u002Fjukeyman-skills\u002Fblob\u002Fmain\u002Fskills\u002Fzustand-store-creator\u002FSKILL.md",[],[],{"skillNo":65,"name":66,"nameZh":6,"description":67,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":68,"categories":69,"keywords":70,"publishedAt":19},"01M79AMZKP4YDEZ7EHG1Y7PP74","zero-trust-config-helper","Configure with zero trust config helper operations. Auto-activating skill for Security Advanced.\nTriggers on: zero trust config helper, zero trust config helper\nPart of the Security Advanced skill category. Use when configuring systems or services. Trigger with phrases like \"zero trust config helper\", \"zero helper\", \"zero\".","https:\u002F\u002Fgithub.com\u002Fjukeyman\u002Fjukeyman-skills\u002Fblob\u002Fmain\u002Fskills\u002Fzero-trust-config-helper\u002FSKILL.md",[],[],{"skillNo":72,"name":73,"nameZh":6,"description":74,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":75,"categories":76,"keywords":77,"publishedAt":19},"01M79AMJSBCEJQNBTYNAHYN5HM","zapier-integration-helper","Assist with zapier integration helper operations. Auto-activating skill for Business Automation.\nTriggers on: zapier integration helper, zapier integration helper\nPart of the Business Automation skill category. Use when working with APIs or building integrations. Trigger with phrases like \"zapier integration helper\", \"zapier helper\", \"zapier\".","https:\u002F\u002Fgithub.com\u002Fjukeyman\u002Fjukeyman-skills\u002Fblob\u002Fmain\u002Fskills\u002Fzapier-integration-helper\u002FSKILL.md",[],[],{"skillNo":79,"name":80,"nameZh":6,"description":81,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":82,"categories":83,"keywords":84,"publishedAt":19},"01M79AHJZGVMARS13VFQYRR1MP","yaml-config-validator","Validate yaml config validator operations. Auto-activating skill for DevOps Basics.\nTriggers on: yaml config validator, yaml config validator\nPart of the DevOps Basics skill category. Use when configuring systems or services. Trigger with phrases like \"yaml config validator\", \"yaml validator\", \"yaml\".","https:\u002F\u002Fgithub.com\u002Fjukeyman\u002Fjukeyman-skills\u002Fblob\u002Fmain\u002Fskills\u002Fyaml-config-validator\u002FSKILL.md",[],[],{"skillNo":86,"name":87,"nameZh":6,"description":88,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":89,"sourceHtmlUrl":90,"categories":91,"keywords":92,"publishedAt":19},"01M79AHGDZRN0020RDQSQDPAM8","xvary-stock-research","Thesis-driven equity analysis from public SEC EDGAR and market data; \u002Fanalyze, \u002Fscore, \u002Fcompare workflows with bundled Python tools (Claude Code, Cursor, Codex).","https:\u002F\u002Fgithub.com\u002Fjukeyman\u002Fjukeyman-skills\u002Fblob\u002Fmain\u002Fskills\u002Fxvary-stock-research\u002FLICENSE","https:\u002F\u002Fgithub.com\u002Fjukeyman\u002Fjukeyman-skills\u002Fblob\u002Fmain\u002Fskills\u002Fxvary-stock-research\u002FSKILL.md",[],[],{"skillNo":94,"name":95,"nameZh":6,"description":96,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":97,"categories":98,"keywords":99,"publishedAt":19},"01M79AHCYJ9FV9RMHQ24HDRN33","xss-vulnerability-scanner","Scan xss vulnerability scanner operations. Auto-activating skill for Security Fundamentals.\nTriggers on: xss vulnerability scanner, xss vulnerability scanner\nPart of the Security Fundamentals skill category. Use when working with xss vulnerability scanner functionality. Trigger with phrases like \"xss vulnerability scanner\", \"xss scanner\", \"xss\".","https:\u002F\u002Fgithub.com\u002Fjukeyman\u002Fjukeyman-skills\u002Fblob\u002Fmain\u002Fskills\u002Fxss-vulnerability-scanner\u002FSKILL.md",[],[]]