How Claude-Obsidian Separates Vault Data from Plugin Installation

Claude-Obsidian enforces strict isolation between user knowledge vaults and its own plugin installation through canonical path comparison and explicit rejection logic that prevents any vault operation from touching the plugin tree.

The open-source tool claude-obsidian (available at AgriciDaniel/claude-obsidian) implements a rigorous architectural boundary between its executable code and user-generated content. Understanding how claude-obsidian separates vault data from plugin installation is critical for contributors and power users who need to guarantee that automated operations never corrupt the tool's own files or mistakenly treat the repository root as a knowledge vault.

Vault Discovery with Explicit Plugin-Tree Exclusion

In claude_obsidian/paths.py, the function resolve_vault_root orchestrates vault discovery while maintaining a hard boundary against the plugin installation directory.

The resolution follows a strict precedence chain:

  1. An explicit --vault argument provided via CLI
  2. The CLAUDE_OBSIDIAN_VAULT environment variable
  3. The nearest workspace-config file (.claude-obsidian.json)
  4. The nearest initialized vault directory

By default, the function sets allow_plugin_root=False, which automatically triggers the assert_not_plugin_tree validation before returning any path to the caller.

The Plugin-Root Guard Clause

The helper assert_not_plugin_tree canonicalizes both the candidate vault path and the known plugin root, then compares their portable keys. If the candidate matches the plugin root exactly, the function raises VaultSelectionError with code PLUGIN_ROOT_IS_NOT_VAULT. If the candidate is a descendant of the plugin tree, it raises PLUGIN_TREE_IS_NOT_VAULT instead.

Hard Enforcement via Canonical Path Validation

Path containment is not merely a convention in this codebase; it is enforced through filesystem-level canonicalization that prevents ambiguous boundary definitions.

Portable Key Comparison

Rather than performing simple string comparisons, the system converts paths to portable keys (normalized, absolute representations) before evaluation. This prevents bypass attempts using symlinks or relative path traversal sequences that might otherwise confuse the boundary detection logic.

Mutable Operation Protection

This validation layer guarantees that mutable operations—such as write locks, ledger updates, or file creation—are never performed inside the plugin's template or example hierarchies. The error is raised during the vault resolution phase, before any command execution or file system mutation begins.

Vault Initialization Without Contamination

Once a vault root passes the separation checks, build_vault_bundle in claude_obsidian/vault_ops.py generates the required directory structure.

This function receives a path that has already been validated by resolve_vault_root, ensuring that initialization creates wiki/, .obsidian/, .raw/, and the workspace config .claude-obsidian.json only within the user vault. It cannot touch the plugin installation directory because the vault path is confirmed to be outside the plugin tree prior to the function's execution.

Path Containment Helpers

Additional boundary protection is provided by utility functions in paths.py that runtime-verify all file operations:

  • assert_within: Confirms that any target path remains inside the confirmed vault root before read or write operations execute.
  • is_initialized_vault: Validates that a directory contains the markers of a legitimate vault without traversing into plugin directories.
  • is_adoptable_vault: Determines if an existing directory can be adopted as a vault while maintaining the plugin-tree boundary.

Together, these utilities form a defense-in-depth strategy that contains all vault-specific operations within the user-designated zone.

Implementation Example

The following patterns demonstrate the separation enforcement in practice:

from claude_obsidian.paths import resolve_vault_root, VaultSelectionError
from pathlib import Path

# Attempt to resolve a vault from the current working directory

# The plugin_root parameter explicitly defines the forbidden zone

try:
    selection = resolve_vault_root(
        explicit=None,
        start=Path.cwd(),
        plugin_root=Path(__file__).parent.parent,  # Repository root

    )
    print(f"Vault found at: {selection.root}")
except VaultSelectionError as exc:
    print(f"Resolution failed: {exc.code} – {exc}")
from claude_obsidian.vault_ops import build_vault_bundle

# Generate vault structure only after validation

# This operates exclusively on the previously validated vault path

bundle = build_vault_bundle(
    root=selection.root,
    operation_id="init-001",
    operation_type="init",
    generated_at="2024-01-01T00:00:00Z",
    adopt=False,
    force=False,
)
print(bundle["writes"])  # File list restricted to vault directory

Summary

  • resolve_vault_root in paths.py implements a precedence-based discovery algorithm that rejects the plugin tree by default via allow_plugin_root=False.
  • assert_not_plugin_tree raises VaultSelectionError with specific codes (PLUGIN_ROOT_IS_NOT_VAULT, PLUGIN_TREE_IS_NOT_VAULT) when boundaries are violated.
  • build_vault_bundle in vault_ops.py operates only on pre-validated vault paths, ensuring initialization never contaminates the plugin installation.
  • Path containment helpers (assert_within, is_initialized_vault, is_adoptable_vault) provide runtime verification that all operations stay within the vault boundary.
  • The architecture guarantees that the plugin code remains isolated from user-generated knowledge vaults through canonical path comparison and early validation errors.

Frequently Asked Questions

Can I force claude-obsidian to use its own installation directory as a vault?

Yes, but only by explicitly setting allow_plugin_root=True when calling resolve_vault_root. The CLI does not expose this flag for safety reasons; it is reserved for internal testing scenarios. Without this override, the system raises VaultSelectionError with code PLUGIN_ROOT_IS_NOT_VAULT immediately upon detection.

What happens if I try to initialize a vault inside the plugin templates folder?

The initialization will fail before creating any files. The assert_not_plugin_tree function canonicalizes the target path, detects that it resides within the plugin tree, and raises VaultSelectionError with code PLUGIN_TREE_IS_NOT_VAULT. This prevents accidental corruption of the tool's template hierarchy or example vaults distributed with the repository.

The path containment helpers use canonical path resolution (portable keys) rather than string comparison. This means symlinks are resolved to their absolute targets before boundary checks occur, preventing escape attempts that could reach the plugin installation directory or other restricted system areas.

Which source files contain the core separation logic?

The primary isolation logic resides in claude_obsidian/paths.py, specifically within the resolve_vault_root and assert_not_plugin_tree functions. The vault operation logic in claude_obsidian/vault_ops.py relies on these validations but does not itself contain boundary enforcement code, ensuring single-responsibility separation between path validation and vault mutation.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →