# How Claude-Obsidian Implements Safe Concurrency During Vault Mutations

> Discover how Claude-Obsidian ensures safe concurrency during vault mutations. Learn about its MutationLock, atomic directory creation, inode verification, and bundle hashing for a robust one-writer protocol.

- Repository: [Agrici.Daniel/claude-obsidian](https://github.com/AgriciDaniel/claude-obsidian)
- Tags: internals
- Published: 2026-08-25

---

**Claude-Obsidian guarantees safe concurrency during vault mutations by implementing a MutationLock that combines atomic directory creation, inode verification, and atomic bundle hashing to enforce a strict "one-writer" protocol across processes.**

Claude-Obsidian is an open-source automation tool for Obsidian vaults that manages complex file mutations through transactional operations. When multiple agents or background processes attempt simultaneous modifications, conventional file locking often falls short against race conditions and filesystem edge cases. The repository solves this through a multi-layered safety system implemented in [`claude_obsidian/transaction.py`](https://github.com/AgriciDaniel/claude-obsidian/blob/main/claude_obsidian/transaction.py), ensuring that only one process can commit changes while maintaining integrity against crashes and malicious path manipulation.

## The MutationLock Foundation

The concurrency architecture centers on the `MutationLock` class, which creates an exclusive lock in the hidden metadata directory `.vault-meta/mutation.lock`. Rather than using simple advisory locks, this implementation leverages filesystem atomicity to guarantee exclusive access across operating systems.

### Atomic Directory Creation

Lock acquisition begins with a POSIX-atomic directory creation operation. In the `acquire()` method starting at line 1818, the code invokes `_open_lock_root_fd()` to attempt directory creation using `os.mkdir`. On POSIX-compliant filesystems, `mkdir` is atomic—if multiple processes race to create the same directory, exactly one succeeds. This primitive eliminates initialization races without requiring additional coordination mechanisms.

### Ownership Verification and Stale-Lock Reaping

After atomic acquisition, the process establishes ownership by writing an [`owner.json`](https://github.com/AgriciDaniel/claude-obsidian/blob/main/owner.json) file containing its PID, start timestamp, and a cryptographically random token. This metadata enables both ownership verification and automatic crash recovery. The `_may_reap()` method (lines 1675-1697) implements stale-lock detection logic: when `force_stale_lock=True` and the recorded process has remained dead longer than the `stale_after` threshold (default 1 hour), the method permits forced reclamation. This prevents permanent deadlocks while requiring explicit opt-in to avoid accidentally terminating live operations.

## Namespace Integrity Protection

Process-level locking prevents concurrent writers, but Claude-Obsidian adds filesystem-level protections against directory substitution and path traversal attacks.

### Inode Identity Verification

The `assert_runtime_namespace_current` method (lines 1642-1654) maintains continuous verification that the public path `.vault-meta` still resolves to the same inode pinned at lock acquisition. Helper functions like `_lock_entry_matches` validate directory identity throughout the transaction lifecycle. If external manipulation—such as a symlink attack—changes the filesystem namespace, the system raises a critical error and aborts the mutation.

### File Descriptor Confinement

To eliminate time-of-check-to-time-of-use (TOCTOU) vulnerabilities, the lock provides duplicated file descriptors through `duplicate_root_fd()` and `duplicate_parent_fd()` (lines 1720-1740). Child worker processes receive these descriptors and perform all I/O operations through them, never re-resolving public paths through string-based lookups. This confinement ensures that read and write operations remain bound to the exact namespace verified at lock acquisition, even if the external path environment changes.

## Transaction-Level Atomicity

Exclusive access prevents concurrent writes, but transaction-level safety ensures consistency between planning and execution phases. The system implements optimistic concurrency control through content-addressed verification.

Before committing any mutation, the `build_vault_bundle` method (lines 30-44 in [`claude_obsidian/vault_ops.py`](https://github.com/AgriciDaniel/claude-obsidian/blob/main/claude_obsidian/vault_ops.py)) calculates `_safe_hash` values for every target file and encapsulates them in a JSON transaction bundle. During the apply phase, the system verifies that current file hashes match the recorded expectations. If any concurrent process modified a file between planning and application, the hash mismatch triggers immediate abort, preventing partial or inconsistent updates.

This bundle-based approach integrates with the CLI interface in [`scripts/claude-obsidian.py`](https://github.com/AgriciDaniel/claude-obsidian/blob/main/scripts/claude-obsidian.py):

```bash

# Dry-run validation

claude-obsidian transaction inspect --bundle planned_changes.json

# Atomic application (only if hashes match)

claude-obsidian transaction apply --bundle planned_changes.json

```

## The Complete Concurrency Protocol

These mechanisms combine into a five-phase strict "one-writer" protocol:

1. **Acquire**: Obtain `MutationLock` via atomic directory creation in `.vault-meta/mutation.lock`; only the holder accesses the locked metadata namespace.
2. **Plan**: Compute expected hashes for all target files using `_safe_hash` and construct the transaction bundle.
3. **Inspect**: Execute dry-run validation through `transaction inspect` to verify no external process modified the vault.
4. **Apply**: Commit the bundle atomically via `transaction apply`, applying changes only if all hash verifications pass.
5. **Release**: Clean up the lock directory and file descriptors, safely releasing the advisory lock.

If any phase detects a conflict—changed inode, divergent hash, or stale lock—the operation aborts immediately, preserving the vault's consistency. This architecture allows parallel agents to draft changes concurrently while ensuring that only a single orchestrator can atomically commit them, eliminating race conditions and enabling deterministic, recoverable vault mutations.

## Summary

- **Atomic directory creation** via `os.mkdir` in `acquire()` guarantees exclusive lock acquisition without race conditions during initialization.
- **Ownership verification** through [`owner.json`](https://github.com/AgriciDaniel/claude-obsidian/blob/main/owner.json) and `_may_reap()` enables automatic recovery from crashed processes after a configurable 1-hour timeout.
- **Inode verification** via `assert_runtime_namespace_current` (lines 1642-1654) detects symlink attacks and directory substitution attempts.
- **File descriptor confinement** through `duplicate_root_fd()` and `duplicate_parent_fd()` prevents path traversal attacks during long-running operations.
- **Atomic bundle verification** using `build_vault_bundle` and `_safe_hash` ensures consistency between transaction planning and execution phases.

## Frequently Asked Questions

### What happens if a process crashes while holding the MutationLock?

The system implements automatic stale-lock recovery through the `_may_reap()` method (lines 1675-1697). If the process recorded in [`owner.json`](https://github.com/AgriciDaniel/claude-obsidian/blob/main/owner.json) has been dead longer than the `stale_after` duration (default 1 hour) and `force_stale_lock=True` is specified, a new process can forcibly acquire the lock. This prevents permanent deadlocks while requiring explicit opt-in to avoid accidentally interrupting live operations that may appear unresponsive.

### How does Claude-Obsidian prevent symlink attacks during vault mutations?

The `assert_runtime_namespace_current` method (lines 1642-1654) continuously verifies that `.vault-meta` resolves to the same inode pinned at lock acquisition. Functions like `_lock_entry_matches` validate directory identity throughout the transaction. If external manipulation changes the filesystem namespace—such as replacing the directory with a symlink—the system detects the inode mismatch and aborts the operation before any writes occur.

### Can multiple processes read the vault while one holds the MutationLock?

Yes, the MutationLock explicitly protects write operations and metadata mutations within `.vault-meta/mutation.lock`. While the lock holder exclusively controls transaction bundle application, other processes can read vault contents concurrently. However, the file descriptor duplication methods ensure that only the lock holder performs writes, and the hash verification system detects any external modifications that occur between planning and application phases.

### What is the difference between `transaction inspect` and `transaction apply`?

The `transaction inspect` command performs a dry-run validation of the planned mutation bundle without modifying the vault, verifying that expected hashes from `_safe_hash` match current file states. The `transaction apply` command executes the atomic bundle, applying changes only if all hash verifications pass. This separation enables safe validation through the CLI in [`scripts/claude-obsidian.py`](https://github.com/AgriciDaniel/claude-obsidian/blob/main/scripts/claude-obsidian.py) before committing irreversible changes to the vault.