# How the Plan Approval Hash Is Generated in Claude-Obsidian: SHA-256 Binding Explained

> Discover how the plan approval hash is generated in Claude-Obsidian. Learn about the SHA-256 binding process using transaction.py for secure transaction integrity.

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

---

**The plan approval hash is generated by the `plan_approval_sha256` function in [`claude_obsidian/transaction.py`](https://github.com/AgriciDaniel/claude-obsidian/blob/main/claude_obsidian/transaction.py), which creates a deterministic SHA-256 digest from canonical JSON containing the vault's filesystem identity, the expanded operation bundle, and prepared write metadata.**

Claude-obsidian implements a cryptographic safety mechanism to ensure that approved file operations can only be applied to the exact vault and contents they were reviewed against. This mechanism centers on the **plan approval hash**, a SHA-256 digest computed through a multi-step canonicalization process that binds the operation bundle to specific filesystem state and content hashes.

## The Core Function: `plan_approval_sha256`

The hash generation logic resides in [`claude_obsidian/transaction.py`](https://github.com/AgriciDaniel/claude-obsidian/blob/main/claude_obsidian/transaction.py) at lines 1287–1308 within the `plan_approval_sha256` function. This function orchestrates the collection of identity metadata, bundle contents, and write projections before serializing them into a format suitable for cryptographic hashing.

The function accepts four primary parameters:

- `vault_root` – The absolute path or user-defined label for the target vault
- `vault_identity` – A JSON object describing the vault's filesystem identity (device and inode)
- `expanded_bundle` – The reviewed operation bundle containing all planned changes
- `prepared_writes` – A projection of pending file writes with original and new SHA-256 values

## Step-by-Step Hash Generation Process

The generation follows three distinct phases to ensure deterministic, collision-resistant output.

### Step 1: Collecting Canonical Identity and Bundle Data

First, the function assembles a structured data object containing four critical fields:

- **`vault_root`** – Captures the absolute pathname or user-provided vault label
- **`vault_identity`** – A JSON object containing `device`, `inode`, and `state` flags describing the vault's filesystem location; if not provided, `_vault_object_identity` generates this automatically
- **`expanded_bundle_sha256`** – The SHA-256 hash of the expanded operation bundle, computed via `_canonical_json_hash` (which delegates to `bundle_sha256`)
- **`prepared_writes`** – A projection of every pending write generated by `_prepared_projection`, including relative paths, intended modes, original SHA-256 hashes, and new content hashes

These fields are assembled into a single JSON object with the fixed schema identifier `"claude-obsidian.plan-approval.v3"`, ensuring version compatibility.

### Step 2: Canonical JSON Serialization

The `bundle_sha256` helper function (lines 1011–1022 in [`transaction.py`](https://github.com/AgriciDaniel/claude-obsidian/blob/main/transaction.py)) handles serialization with strict parameters to guarantee byte-for-byte consistency:

```python
json.dumps(
    data,
    sort_keys=True,           # Deterministic key ordering

    separators=(",", ":"),      # Minimal whitespace (no spaces after delimiters)

    ensure_ascii=False,         # Unicode support without escaping

    allow_nan=False            # Strict JSON compliance

)

```

This canonicalization eliminates formatting variations that could produce different hashes across Python versions or platforms.

### Step 3: SHA-256 Calculation via `sha256_bytes`

Finally, the serialized UTF-8 byte string passes to `sha256_bytes`, imported from [`claude_obsidian/json_utils.py`](https://github.com/AgriciDaniel/claude-obsidian/blob/main/claude_obsidian/json_utils.py). This utility computes the cryptographic hash and returns the hexadecimal digest string that serves as the **plan approval hash**.

## Cryptographic Binding and Replay Protection

Because the hash incorporates the vault's device and inode identifiers alongside exact file content hashes, any change to the vault location, file contents, or intended permissions produces a completely different digest. This prevents accidental or malicious replay of old approvals against modified vaults or different filesystem locations.

The `vault_identity` field specifically protects against vault path renaming or migration attacks, while the `prepared_writes` array ensures that the exact byte-level changes reviewed are the only ones that can be applied.

## Implementation Example: Generating and Validating the Hash

```python
from pathlib import Path
from claude_obsidian.transaction import (
    plan_approval_sha256,
    inspect_bundle,
)

# Reviewed operation bundle and prepared writes

expanded_bundle = {"ops": [...]}  # The reviewed plan

prepared_writes = [
    {
        "relative_path": "wiki/page.md",
        "mode": 0o100644,
        "original_sha256": "a1b2c3...",
        "original_mode": 0o100644,
        "content_sha256": "d4e5f6...",
        "new_mode": 0o100644,
    },
    # Additional writes...

]

# Generate approval hash

vault_path = Path("/path/to/vault")
approval_hash = plan_approval_sha256(
    vault_root=vault_path,
    expanded_bundle=expanded_bundle,
    prepared_writes=prepared_writes,
)

print(f"Plan approval SHA-256: {approval_hash}")

# Later validation during application

bundle_info = inspect_bundle(vault_path, operation)
assert bundle_info["approval_sha256"] == approval_hash, "Approval mismatch detected"

```

## Key Source Files and Their Roles

- **[`claude_obsidian/transaction.py`](https://github.com/AgriciDaniel/claude-obsidian/blob/main/claude_obsidian/transaction.py)** – Contains `plan_approval_sha256`, `bundle_sha256`, and helper functions `_vault_object_identity` and `_prepared_projection`
- **[`claude_obsidian/json_utils.py`](https://github.com/AgriciDaniel/claude-obsidian/blob/main/claude_obsidian/json_utils.py)** – Provides `sha256_bytes` for final cryptographic hash computation
- **[`claude_obsidian/vault_ops.py`](https://github.com/AgriciDaniel/claude-obsidian/blob/main/claude_obsidian/vault_ops.py)** – Implements filesystem identity extraction used in `vault_identity` generation
- **[`tests/test_transaction.py`](https://github.com/AgriciDaniel/claude-obsidian/blob/main/tests/test_transaction.py)** – Validates that approval hashes correctly bind to vault identity and detect file mode changes

## Summary

- The **plan approval hash** is generated by `plan_approval_sha256` in [`claude_obsidian/transaction.py`](https://github.com/AgriciDaniel/claude-obsidian/blob/main/claude_obsidian/transaction.py) using a three-step canonicalization process
- The hash binds to **vault filesystem identity** (device/inode), preventing approval reuse across different vault locations
- **Canonical JSON serialization** with `sort_keys=True` and minimal separators ensures deterministic, cross-platform hash consistency
- The hash includes **SHA-256 values of both original and new file contents**, guaranteeing that only reviewed changes can be applied
- Validation occurs through `inspect_bundle`, which compares stored hashes against recomputed values during plan execution

## Frequently Asked Questions

### What specific data fields are included in the plan approval hash?

The hash includes four canonical fields: the `vault_root` path, a `vault_identity` JSON object containing device and inode information, the `expanded_bundle_sha256` of the operation plan, and the `prepared_writes` array containing path, mode, and content hashes for every affected file. These are serialized under the schema identifier `"claude-obsidian.plan-approval.v3"`.

### How does claude-obsidian ensure the hash is deterministic across different systems?

The `bundle_sha256` function enforces deterministic output by using `json.dumps()` with `sort_keys=True` to ensure consistent key ordering, `separators=(",", ":")` to eliminate whitespace variance, and `ensure_ascii=False` with `allow_nan=False` for strict, predictable JSON encoding. This canonicalization guarantees that identical data produces identical byte sequences for hashing regardless of the host platform.

### Where is the plan approval hash validated during plan execution?

Validation occurs when `inspect_bundle` retrieves the stored approval hash from the operation metadata and compares it against a freshly computed hash of the current vault state and bundle contents. This check ensures the vault identity matches and the file contents haven't changed since the original review, effectively preventing the application of stale or tampered plans.

### What happens if file contents or vault paths change after the hash is generated?

Any modification to the vault path (which changes device/inode), file contents (altering SHA-256 values), or intended file modes will cause the `plan_approval_sha256` function to produce a different digest. When `inspect_bundle` validates the approval, this mismatch triggers an assertion failure or error, halting the operation and protecting against unintended modifications or replay attacks.