How claude-obsidian Ensures Transactional Integrity for Vault Mutations

claude-obsidian guarantees atomic, recoverable vault mutations through a layered transaction protocol that combines process-level locking, cryptographic pre-validation, and deterministic rollback mechanisms.

The open-source tool AgriciDaniel/claude-obsidian implements a robust transactional system for Obsidian vault operations, ensuring that every modification follows an all-or-nothing commit model. By treating each vault change as a recoverable, operation-level transaction, the system prevents partial writes, detects external interference, and maintains data consistency even during system failures.

Core Mechanisms for Transactional Integrity

Process-Level Mutation Locking

Before any write operation begins, the system establishes an exclusive mutation lock using directory-descriptor confinement. In claude_obsidian/transaction.py, the functions _require_lock_dirfd_support() and _open_lock_root_fd() (lines 66-78) open a locked directory descriptor that cannot be swapped by other processes during the transaction lifecycle.

This lock is held for the entire transaction duration, preventing concurrent interference from external tools or parallel processes. The directory descriptor acts as a single-process gatekeeper, ensuring that no other mutation can begin until the current transaction either commits fully or rolls back completely.

Pre-Condition Hash Validation

Every transaction begins by capturing the current cryptographic state of target files. The system reads each file planned for modification and records its SHA-256 digest and file mode using _safe_file_state (lines 44-52). These hashes serve as pre-conditions that must remain unchanged throughout the transaction.

If any monitored file changes between the initial read and the final write, the system raises a TransactionConflict exception and immediately aborts. This optimistic concurrency control prevents the "lost update" problem and ensures that external modifications do not corrupt the planned operation sequence.

Atomic File Replacement Strategy

Individual file writes leverage atomic replacement to eliminate partial state exposure. The _atomic_vault_write function (lines 37-45) creates temporary files with the .txn-… prefix, opening them with O_EXCL|O_NOFOLLOW flags to prevent symlink attacks and accidental overwrites.

Once the temporary file contains the complete new content, the system calls os.replace() to perform an atomic swap into the target location. This mechanism guarantees that readers always see either the previous valid state or the new complete state—never a partially-written file, even if the process crashes mid-write.

Deterministic Rollback and Recovery

For every write operation, the system stores the original hash and mode in a transaction journal located at .vault-meta/transactions. If any step fails during commit, the journal enables deterministic replay via _confined_vault_unlink and related helpers (lines 90-100).

The recovery process verifies each original file against its stored SHA-256 hash before restoration, ensuring that rollback operations themselves do not introduce corruption. This creates a safe unwind path that returns the vault to its exact pre-transaction state, regardless of how many files were modified before the failure occurred.

Safety and Validation Guarantees

Path Validation and Reserved Directory Protection

The system enforces strict path hygiene to prevent aliasing attacks and metadata corruption. In claude_obsidian/transaction.py (lines 60-78 and 88-100), the code defines _RESERVED_WRITE_PATHS that forbid writes to internal metadata directories. Every target path undergoes portability validation to ensure:

  • No Windows-illegal characters exist in filenames
  • Canonical Unicode NFC normalization
  • Length limits compliance for cross-platform compatibility

These checks eliminate naming attacks that could otherwise bypass security boundaries or corrupt the vault's internal structure.

Cryptographic Plan Approval

Each transaction bundle receives a cryptographic fingerprint before execution. The plan_approval_sha256 function (lines 124-132) computes a SHA-256 hash binding the operation type, vault identity, and exact set of planned writes. This hash is stored in the transaction journal and verified before execution.

By requiring plan approval, the system ensures that the operation set cannot be tampered with between user consent and actual execution. The approval hash acts as a tamper-evident seal over the entire transaction plan.

Cross-Platform Safety Checks

On platforms lacking directory-descriptor support (such as native Windows without WSL), the system explicitly rejects mutation attempts. The _require_write_platform function (lines 108-116) detects capabilities and falls back to read-only inspection mode when directory locking is unavailable.

This platform-aware behavior maintains the same safety guarantees across operating systems by preventing execution on hosts that cannot support the required atomicity primitives.

Implementation Example: Executing a Safe Transaction

The following Python pattern demonstrates how to prepare and execute a transaction with full integrity checks:

from pathlib import Path
from claude_obsidian.transaction import (
    plan_approval_sha256,
    _atomic_vault_write,
    safe_transactions_root,
)

vault = Path("/path/to/vault")
bundle = {"operation": "save", "content": "Hello world"}
writes = [
    # PreparedWrite contains file metadata and content hashes

    type("PreparedWrite", (), {
        "relative_path": "wiki/page.md",
        "mode": 0o644,
        "original_sha256": "a3f5...",
        "original_mode": 0o644,
        "content_sha256": "e3b0c442...",
        "new_mode": 0o644,
        "data": b"Hello world\n",
    })
]

# Generate approval hash for tamper detection

approval_hash = plan_approval_sha256(vault, bundle, writes)

# Apply writes atomically

for w in writes:
    _atomic_vault_write(vault, w.relative_path, w.data, mode=w.new_mode)

# Record to journal for potential rollback

journal_dir = safe_transactions_root(vault, create=True)

# Journal entry contains approval_hash and write metadata...

Recovery and Error Handling

When transactions fail, the journal enables automatic recovery:

from claude_obsidian.transaction import TransactionRecoveryError, _confined_vault_unlink

try:
    # ... execute writes ...

    pass
except Exception:
    for w in writes:
        try:
            _confined_vault_unlink(
                vault, 
                w.relative_path, 
                expected_sha256=w.original_sha256
            )
        except TransactionRecoveryError:
            # Log and continue; system maintains eventual consistency

            pass
    raise

Summary

  • Process-level locking via directory descriptors prevents concurrent mutations during transaction execution.
  • SHA-256 pre-condition hashes detect external file modifications before they corrupt the transaction.
  • Atomic replacement using os.replace() ensures readers never observe partially written files.
  • Deterministic rollback through the .vault-meta/transactions journal restores original states using verified hashes.
  • Path validation blocks writes to reserved metadata directories and enforces cross-platform filename safety.
  • Cryptographic plan approval binds user consent to specific operations, preventing tampering between approval and execution.

Frequently Asked Questions

What happens if the system crashes during a vault transaction?

If a crash occurs mid-transaction, the next vault operation detects the incomplete journal in .vault-meta/transactions and automatically initiates recovery. The system replays the journal entries, verifying each original file against its stored SHA-256 hash before restoration, ensuring the vault returns to exactly its pre-transaction state without manual intervention.

How does claude-obsidian handle concurrent modifications from external tools?

External modifications are detected through pre-condition hash validation. Before committing any write, the system re-checks the SHA-256 hash of files recorded during the planning phase. If TransactionConflict is raised due to hash mismatches, the entire transaction aborts, preventing the external change from being overwritten and avoiding mixed-state corruption.

Why does the system use directory file descriptors instead of traditional file locks?

Directory file descriptors provide confinement guarantees that traditional file locks cannot. Functions like _open_lock_root_fd() ensure the process maintains a handle to the specific vault directory inode, preventing Time-of-Check to Time-of-Use (TOCTOU) attacks where an attacker might swap the vault directory itself between validation and write operations.

Where are transaction journals stored and how long are they retained?

Journals are stored in the .vault-meta/transactions directory within the vault root. According to the implementation in safe_transactions_root, these files persist until explicitly cleaned up by the recovery system after successful transaction completion. Failed transactions leave journals in place to enable recovery on the next vault access, ensuring no data loss occurs even across process restarts.

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 →