# How claude-obsidian Generates a Canonical Hash for Operation Approval Using `plan_approval_sha256`

> Discover how claude-obsidian generates a canonical hash for operation approval with plan_approval_sha256. Learn how it ensures exact reviewed changes are executed for secure transactions.

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

---

**The `plan_approval_sha256` function in [`claude_obsidian/transaction.py`](https://github.com/AgriciDaniel/claude-obsidian/blob/main/claude_obsidian/transaction.py) creates a deterministic SHA‑256 hash by combining the vault’s stable identity, a canonical JSON representation of the operation plan, and a projection of every file write, ensuring only the exact reviewed changes can be executed.**

Every write operation in claude-obsidian requires explicit user approval bound to a **cryptographic hash**. This mechanism prevents accidental or malicious modifications between the review step and the actual file system mutation. The canonical approval hash is produced by a multi-step pipeline that deterministically serializes the vault state, operation intent, and pending changes.

## Vault Identity Resolution

Before hashing, claude-obsidian establishes a stable anchor to the target vault. In [`claude_obsidian/transaction.py`](https://github.com/AgriciDaniel/claude-obsidian/blob/main/claude_obsidian/transaction.py), the helper **`_vault_object_identity`** (lines 1012–1016) generates a unique identifier derived from the vault directory’s device and inode, or falls back to absent-root metadata if the path does not yet exist. This **vault identity** ensures the approval hash is valid only for the specific vault instance on the specific machine, preventing the same plan from being accidentally applied to a different vault.

## Canonical JSON Serialization of the Operation Plan

The operation plan—referred to as the expanded bundle—is converted to a deterministic byte representation. The function calls **`_canonical_json_hash`**, which delegates to **`bundle_sha256`** (lines 1025–1027). 

`bundle_sha256` performs strict serialization:
- Sorts all JSON keys alphabetically
- Uses consistent separators (no trailing whitespace)
- Encodes the result as UTF‑8 before feeding it to `sha256_bytes`

This guarantees that logically identical plans always produce identical hashes regardless of key insertion order or formatting variations.

## Projection of Prepared File Writes

To capture exact byte-level changes, claude-obsidian projects each pending write into a minimal dictionary structure (lines 1029–1040). Each entry contains:
- **Relative path** from the vault root
- **File mode** (permissions)
- **Original SHA‑256** hash of the file before modification
- **New content SHA‑256** hash after modification

This projection ensures that any alteration—whether to file contents, permissions, or path—invalidates the approval hash, thwarting replay attacks where a plan is re-executed against different data.

## Assembly of the Approval Bundle

`plan_approval_sha256` assembles a final JSON object (lines 1087–1095) containing:

```python
{
    "schema": "claude-obsidian.plan-approval.v3",  # fixed identifier

    "vault_root": str(vault_root),                # canonical path or user label

    "vault_identity": vault_identity,             # from step 1

    "expanded_bundle_sha256": bundle_hash,        # from step 2

    "prepared_writes": [writes_projection]        # from step 3

}

```

The assembled object is hashed once more via `bundle_sha256` to produce the **canonical approval hash**. The fixed schema version (`claude-obsidian.plan-approval.v3`) enables future backward compatibility checks while ensuring the current format is unambiguous.

## Runtime Verification and Security Guarantees

When `apply_bundle` executes the transaction, it recomputes the approval hash from the current vault state and compares it against the stored `approval_sha256` using **`hmac.compare_digest`** (lines 4494–4498). This constant-time comparison prevents timing side-channels.

If any component differs—the vault identity (indicating a moved or copied vault), the expanded bundle (indicating logic changes), or the prepared writes (indicating modified file contents or modes)—the comparison fails. The system raises a **`TransactionValidationError`** and aborts the operation, enforcing that only the exact reviewed plan can modify the vault.

## Practical Implementation Examples

### Generating an Approval Hash

```python
from pathlib import Path
from claude_obsidian.transaction import plan_approval_sha256

vault_root = Path("/home/user/obsidian-vault")
expanded_bundle = {"operation": "rename", "target": "daily-notes/2024-01-01.md"}
prepared_writes = []  # List of PreparedWrite objects populated earlier

approval_hash = plan_approval_sha256(
    vault_root=vault_root,
    expanded_bundle=expanded_bundle,
    prepared_writes=prepared_writes,
)
print(f"Canonical approval SHA-256: {approval_hash}")

```

### Applying with Verification

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

vault = Path("/home/user/obsidian-vault")
operation_id = "rename-op-42"

# Review phase: compute and store the hash

review_data = inspect_bundle(vault, operation_id)
stored_hash = review_data["approval_sha256"]

# Execution phase: apply only if hashes match

try:
    apply_bundle(vault, operation_id, approved_plan_sha256=stored_hash)
    print("Transaction applied successfully.")
except TransactionValidationError as e:
    print(f"Approval mismatch: {e}")

```

## Summary

- **`plan_approval_sha256`** in [`claude_obsidian/transaction.py`](https://github.com/AgriciDaniel/claude-obsidian/blob/main/claude_obsidian/transaction.py) creates a deterministic SHA‑256 hash that uniquely identifies a reviewed operation.
- The hash binds together **vault identity** (device/inode), **canonical JSON** of the plan (sorted keys, UTF‑8), and **projected file writes** (path, mode, content hashes).
- The approval bundle uses schema version `claude-obsidian.plan-approval.v3` for forward compatibility.
- At runtime, `hmac.compare_digest` verifies the stored hash against a live recomputation; any deviation triggers a `TransactionValidationError`.

## Frequently Asked Questions

### What is the purpose of the `plan_approval_sha256` function?

`plan_approval_sha256` generates a cryptographic commitment to a specific set of file operations. By hashing the vault identity, operation logic, and exact byte-level changes, it creates an unforgeable token that proves the user reviewed exactly these changes before execution.

### How does claude-obsidian prevent replay attacks using the canonical hash?

The hash includes SHA‑256 digests of the original and new file contents, file modes, and the vault’s inode-based identity. If an attacker attempts to replay the approval token against different file contents or a copied vault, the recomputed hash diverges from the stored `approval_sha256`, causing `apply_bundle` to reject the operation with a `TransactionValidationError`.

### What happens if the vault files change between inspection and application?

If any target file is modified, renamed, or permission-changed after inspection but before application, the **prepared writes** projection no longer matches the current disk state. When `apply_bundle` recomputes the canonical hash, the mismatch is detected via `hmac.compare_digest`, and the transaction aborts, forcing a fresh review.

### Which schema version does the approval bundle use?

The approval bundle explicitly declares schema `"claude-obsidian.plan-approval.v3"` inside the hashed JSON object. This identifier, located in [`claude_obsidian/transaction.py`](https://github.com/AgriciDaniel/claude-obsidian/blob/main/claude_obsidian/transaction.py) (lines 1087–1095), ensures that future versions of the tool can distinguish legacy approvals from current ones and maintain backward compatibility or enforce migrations as needed.