# How to Apply a Transaction Bundle in Claude-Obsidian: CLI and Python Guide

> Learn to apply a transaction bundle in Claude-Obsidian using CLI commands or Python. Safely mutate your vault with rollback guarantees.

- Repository: [Agrici.Daniel/claude-obsidian](https://github.com/AgriciDaniel/claude-obsidian)
- Tags: how-to-guide
- Published: 2026-08-28

---

**Apply a transaction bundle in Claude-Obsidian by running `claude-obsidian apply` with the `--approved-plan-sha256` flag, or call `apply_bundle()` from [`claude_obsidian/transaction.py`](https://github.com/AgriciDaniel/claude-obsidian/blob/main/claude_obsidian/transaction.py) with the vault path, bundle file, and approval hash to atomically mutate your vault with guaranteed rollback safety.**

Claude-Obsidian (AgriciDaniel/claude-obsidian) treats every knowledge-changing operation as a **transaction bundle**—a JSON document that describes file mutations, cryptographic digests, and approval hashes. Applying this bundle is the final step that actually commits changes to your vault while ensuring atomicity, reproducibility, and cross-platform safety.

## Understanding the Transaction Bundle Architecture

A transaction bundle is a JSON artifact that follows the `claude-obsidian.transaction.v1` schema. It encapsulates the **operation type**, target files, expected content digests, and a cryptographic approval hash generated during inspection. When you apply a bundle, the system validates every precondition before touching disk, then performs atomic writes to prevent partial updates.

The core logic resides in [`claude_obsidian/transaction.py`](https://github.com/AgriciDaniel/claude-obsidian/blob/main/claude_obsidian/transaction.py) inside the `apply_bundle` function. This implementation orchestrates a 12-step safety protocol that guards against plan drift, concurrent mutations, and platform-specific vulnerabilities.

## The Apply Bundle Safety Workflow

Before any file is modified, `apply_bundle` executes a series of guarded validations:

1. **Platform Guard** – `_require_write_platform()` immediately aborts on native Windows systems that lack directory-descriptor confinement, preventing symlink attacks.

2. **Vault Validation** – The target path must exist as a directory; otherwise a `TransactionValidationError` is raised.

3. **Bundle Loading** – The bundle is parsed via `_load_bundle` and must carry the `claude-obsidian.transaction.v1` schema identifier.

4. **Operation ID Sanitization** – `safe_operation_id` enforces a filesystem-safe identifier using only alphanumerics, hyphens, dots, and underscores, capped at 128 bytes.

5. **Approval Verification** – If `approved_plan_sha256` is supplied, the bundle’s pre-lock approval hash (produced by `inspect_bundle`) must match exactly, preventing "plan-drift" between review and execution.

6. **Mutation Lock** – A `MutationLock` is acquired on the vault root using directory file descriptors (on WSL/Linux/macOS), ensuring no directory swap can occur during the operation.

7. **Runtime Store** – `_RuntimeStore.from_lock` creates a temporary workspace at `.vault-meta/transactions/<operation-id>/` to hold journals, backups, and intermediate files.

8. **Idempotency Check** – If the operation ID already contains a completed [`changed-paths.json`](https://github.com/AgriciDaniel/claude-obsidian/blob/main/changed-paths.json), the previous result is returned immediately, making re-application safe.

9. **Metadata Expansion** – `_expand_managed_metadata` injects automatic writes for internal bookkeeping files such as [`.raw/.manifest.json`](https://github.com/AgriciDaniel/claude-obsidian/blob/main/.raw/.manifest.json).

10. **Write Preparation** – Each `PreparedWrite` is inspected for portable path rules, duplicate key checks, and hash consistency.

11. **Atomic File Writes** – `_atomic_vault_write` writes each file to a temporary `.txn-*` name and then calls `os.replace()`, guaranteeing that observers never see partially written files.

12. **Journal Finalization** – Upon success, [`journal.json`](https://github.com/AgriciDaniel/claude-obsidian/blob/main/journal.json) and [`changed-paths.json`](https://github.com/AgriciDaniel/claude-obsidian/blob/main/changed-paths.json) are written to the runtime store, and the mutation lock is released.

If any step fails, the function calls `_confined_vault_unlink` to roll back partially created files and raises a specific `TransactionError` subclass.

## How to Apply a Transaction Bundle via CLI

The command-line interface in [`claude_obsidian/cli.py`](https://github.com/AgriciDaniel/claude-obsidian/blob/main/claude_obsidian/cli.py) wraps the Python API with interactive safety checks. You must first inspect the bundle to obtain its approval hash, then apply it with that hash to prove the bundle has not changed since review.

Inspect the bundle to retrieve the approval hash:

```bash
claude-obsidian inspect --bundle my-change.json --vault /path/to/vault

```

Apply the bundle using the hash returned by the inspect command:

```bash
claude-obsidian apply \
  --bundle my-change.json \
  --approved-plan-sha256 <approval-hash-from-inspect> \
  --vault /path/to/vault

```

The `--approved-plan-sha256` flag is mandatory unless you are initializing a new vault during setup. The CLI ultimately invokes `apply_bundle(vault, bundle_path, approved_plan_sha256=...)`.

## Programmatic Application with Python API

For automation or plugin development, import `apply_bundle` directly from [`claude_obsidian/transaction.py`](https://github.com/AgriciDaniel/claude-obsidian/blob/main/claude_obsidian/transaction.py). You may optionally call `inspect_bundle` first to extract metadata programmatically.

```python
from pathlib import Path
from claude_obsidian.transaction import apply_bundle, inspect_bundle

vault = Path("/path/to/vault")
bundle_path = Path("my-change.json")

# Optional: inspect first to extract the approval hash

inspection = inspect_bundle(vault, bundle_path)
approval_hash = inspection["approval_sha256"]

# Apply the bundle atomically

result = apply_bundle(
    vault_root=vault,
    bundle_or_path=bundle_path,
    approved_plan_sha256=approval_hash,
)

print("Transaction status:", result["status"])
print("Changed files:", result["changed_paths"])

```

Key arguments for `apply_bundle`:

- **vault_root** (`Path`): Path to the target vault directory.
- **bundle_or_path** (`Path | dict`): Either a loaded bundle dictionary or a path to a `.json` file.
- **approved_plan_sha256** (`str | None`): The SHA-256 hash from `inspect_bundle`; omit only during initial vault setup.
- **timeout** / **stale_after** (`int`): Lock acquisition timeout (default 10s) and lock expiration (default 1h).
- **fail_after** (`int | None`): Testing hook to simulate crashes after N writes.
- **progress** (`Callable`): Callback receiving `(stage, count)` tuples for UI feedback.

## Handling Transaction Errors and Recovery

The transaction system distinguishes between validation failures, conflicts, and recovery errors. Always wrap `apply_bundle` in a try-except block to handle these specific cases:

```python
from claude_obsidian.transaction import (
    apply_bundle,
    TransactionValidationError,
    TransactionConflict,
    TransactionRecoveryError,
)

try:
    apply_bundle(vault, bundle_path, approved_plan_sha256=approval_hash)
except TransactionValidationError as exc:
    print(f"Invalid bundle or vault: {exc.code}")
except TransactionConflict as exc:
    print(f"Operation ID reuse or state drift: {exc.code}")
except TransactionRecoveryError as exc:
    print(f"Rollback failed: {exc.code}")

```

Each exception carries a machine-readable `code` attribute (e.g., `"PLAN_CHANGED"`, `"OPERATION_ID_REUSED"`) that is also logged in the transaction journal for forensic debugging. If a failure occurs after partial writes, the system automatically invokes rollback procedures to restore the vault to its pre-transaction state.

Source files implementing this behavior include [`claude_obsidian/vault_ops.py`](https://github.com/AgriciDaniel/claude-obsidian/blob/main/claude_obsidian/vault_ops.py) for high-level vault initialization, and the test suite in [`tests/test_transaction.py`](https://github.com/AgriciDaniel/claude-obsidian/blob/main/tests/test_transaction.py) which verifies idempotency and plan-approval enforcement. Windows compatibility tests in [`tests/test_windows_compat.py`](https://github.com/AgriciDaniel/claude-obsidian/blob/main/tests/test_windows_compat.py) ensure that `apply_bundle` refuses to run on unsupported native Windows platforms.

## Summary

- **Transaction bundles** are immutable JSON descriptions of vault changes that require an approval hash to apply.
- The `apply_bundle` function in [`claude_obsidian/transaction.py`](https://github.com/AgriciDaniel/claude-obsidian/blob/main/claude_obsidian/transaction.py) executes a 12-step safety protocol including platform guards, mutation locks, and atomic file writes.
- Use `claude-obsidian inspect` followed by `claude-obsidian apply` for CLI workflows, or import `apply_bundle` for Python automation.
- Always provide `approved_plan_sha256` except during initial vault setup to prevent plan-drift attacks.
- The system guarantees **atomicity** via temporary `.txn-*` files and `os.replace`, with automatic rollback on any failure.

## Frequently Asked Questions

### What is a transaction bundle in Claude-Obsidian?

A transaction bundle is a JSON document conforming to the `claude-obsidian.transaction.v1` schema that describes a set of file write operations, their expected content digests, and metadata. It serves as a portable, verifiable plan for mutating an Obsidian vault, enabling atomic and reproducible changes across different machines.

### Why does `apply_bundle` require an approval hash?

The `approved_plan_sha256` parameter prevents "plan-drift" by ensuring the bundle file has not been modified between the time you reviewed it (using `inspect_bundle`) and the time you apply it. If the hash mismatches, `TransactionConflict` is raised immediately before any disk mutation occurs, protecting against tampering or accidental edits.

### What happens if a transaction fails mid-way?

If any step in the 12-step workflow fails, `apply_bundle` catches the error, invokes `_confined_vault_unlink` to remove any partially written `.txn-*` files, and raises a specific `TransactionError` subclass. The vault remains in its original state due to the atomic write strategy that uses temporary files and `os.replace` only after all validations pass.

### Can I use Claude-Obsidian on native Windows?

No. The `_require_write_platform()` guard in [`claude_obsidian/transaction.py`](https://github.com/AgriciDaniel/claude-obsidian/blob/main/claude_obsidian/transaction.py) explicitly aborts on native Windows because the platform lacks directory-file-descriptor confinement required for safe mutation locking. You must run Claude-Obsidian on WSL, Linux, or macOS to apply transaction bundles.