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:
- An explicit
--vaultargument provided via CLI - The
CLAUDE_OBSIDIAN_VAULTenvironment variable - The nearest workspace-config file (
.claude-obsidian.json) - 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_rootinpaths.pyimplements a precedence-based discovery algorithm that rejects the plugin tree by default viaallow_plugin_root=False.assert_not_plugin_treeraisesVaultSelectionErrorwith specific codes (PLUGIN_ROOT_IS_NOT_VAULT,PLUGIN_TREE_IS_NOT_VAULT) when boundaries are violated.build_vault_bundleinvault_ops.pyoperates 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.
How does the system handle symbolic links that point outside the vault?
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →